hakalab-mcp 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1497 @@
1
+ # Ejemplos de Integración IDE - Hakalab-MCP
2
+
3
+ **Ejemplos prácticos de código para integración con plugins de IntelliJ IDEA y PyCharm**
4
+
5
+ ## 📋 Contenido
6
+
7
+ 1. [Configuración del Cliente MCP](#configuración-del-cliente-mcp)
8
+ 2. [Implementación de Herramientas](#implementación-de-herramientas)
9
+ 3. [UI Components](#ui-components)
10
+ 4. [AI Agent Integration](#ai-agent-integration)
11
+ 5. [Code Generation](#code-generation)
12
+ 6. [Test Execution](#test-execution)
13
+
14
+ ---
15
+
16
+ ## 🔧 Configuración del Cliente MCP
17
+
18
+ ### MCPClient.kt
19
+ ```kotlin
20
+ package com.hakalab.mcp.client
21
+
22
+ import com.fasterxml.jackson.databind.ObjectMapper
23
+ import com.fasterxml.jackson.module.kotlin.KotlinModule
24
+ import kotlinx.coroutines.*
25
+ import java.io.*
26
+ import java.util.concurrent.TimeUnit
27
+
28
+ class MCPClient(private val config: MCPConfig) {
29
+ private val objectMapper = ObjectMapper().registerModule(KotlinModule())
30
+ private var process: Process? = null
31
+ private var writer: BufferedWriter? = null
32
+ private var reader: BufferedReader? = null
33
+ private var requestId = 0
34
+
35
+ suspend fun connect(): Boolean = withContext(Dispatchers.IO) {
36
+ try {
37
+ val processBuilder = ProcessBuilder(config.command, *config.args.toTypedArray())
38
+ processBuilder.environment().putAll(config.env)
39
+
40
+ process = processBuilder.start()
41
+ writer = process!!.outputStream.bufferedWriter()
42
+ reader = process!!.inputStream.bufferedReader()
43
+
44
+ // Initialize MCP connection
45
+ val initRequest = MCPRequest(
46
+ jsonrpc = "2.0",
47
+ id = getNextRequestId(),
48
+ method = "initialize",
49
+ params = mapOf(
50
+ "protocolVersion" to "2024-11-05",
51
+ "capabilities" to mapOf(
52
+ "tools" to emptyMap<String, Any>()
53
+ ),
54
+ "clientInfo" to mapOf(
55
+ "name" to "hakalab-ide-plugin",
56
+ "version" to "1.0.0"
57
+ )
58
+ )
59
+ )
60
+
61
+ sendRequest(initRequest)
62
+ val response = readResponse()
63
+ response.error == null
64
+ } catch (e: Exception) {
65
+ false
66
+ }
67
+ }
68
+
69
+ suspend fun callTool(toolName: String, arguments: Map<String, Any>): MCPResponse {
70
+ return withContext(Dispatchers.IO) {
71
+ val request = MCPRequest(
72
+ jsonrpc = "2.0",
73
+ id = getNextRequestId(),
74
+ method = "tools/call",
75
+ params = mapOf(
76
+ "name" to toolName,
77
+ "arguments" to arguments
78
+ )
79
+ )
80
+
81
+ sendRequest(request)
82
+ readResponse()
83
+ }
84
+ }
85
+
86
+ suspend fun listTools(): List<MCPTool> {
87
+ return withContext(Dispatchers.IO) {
88
+ val request = MCPRequest(
89
+ jsonrpc = "2.0",
90
+ id = getNextRequestId(),
91
+ method = "tools/list",
92
+ params = emptyMap<String, Any>()
93
+ )
94
+
95
+ sendRequest(request)
96
+ val response = readResponse()
97
+
98
+ response.result?.get("tools")?.let { tools ->
99
+ objectMapper.convertValue(tools, Array<MCPTool>::class.java).toList()
100
+ } ?: emptyList()
101
+ }
102
+ }
103
+
104
+ private fun sendRequest(request: MCPRequest) {
105
+ val json = objectMapper.writeValueAsString(request)
106
+ writer?.write(json)
107
+ writer?.newLine()
108
+ writer?.flush()
109
+ }
110
+
111
+ private fun readResponse(): MCPResponse {
112
+ val line = reader?.readLine() ?: throw IOException("No response from MCP server")
113
+ return objectMapper.readValue(line, MCPResponse::class.java)
114
+ }
115
+
116
+ private fun getNextRequestId(): String = (++requestId).toString()
117
+
118
+ fun disconnect() {
119
+ writer?.close()
120
+ reader?.close()
121
+ process?.destroyForcibly()
122
+ }
123
+ }
124
+
125
+ data class MCPConfig(
126
+ val command: String = "npx",
127
+ val args: List<String> = listOf("-y", "hakalab-mcp"),
128
+ val env: Map<String, String> = mapOf(
129
+ "NODE_ENV" to "production",
130
+ "DEBUG" to "false"
131
+ ),
132
+ val timeout: Long = 30000
133
+ )
134
+
135
+ data class MCPRequest(
136
+ val jsonrpc: String,
137
+ val id: String,
138
+ val method: String,
139
+ val params: Map<String, Any>
140
+ )
141
+
142
+ data class MCPResponse(
143
+ val jsonrpc: String,
144
+ val id: String,
145
+ val result: Map<String, Any>? = null,
146
+ val error: MCPError? = null
147
+ )
148
+
149
+ data class MCPError(
150
+ val code: Int,
151
+ val message: String,
152
+ val data: Any? = null
153
+ )
154
+
155
+ data class MCPTool(
156
+ val name: String,
157
+ val description: String,
158
+ val inputSchema: Map<String, Any>
159
+ )
160
+ ```
161
+
162
+ ---
163
+
164
+ ## 🛠️ Implementación de Herramientas
165
+
166
+ ### ToolExecutor.kt
167
+ ```kotlin
168
+ package com.hakalab.mcp.tools
169
+
170
+ import com.intellij.openapi.progress.ProgressIndicator
171
+ import com.intellij.openapi.progress.Task
172
+ import com.intellij.openapi.project.Project
173
+ import kotlinx.coroutines.runBlocking
174
+
175
+ class ToolExecutor(
176
+ private val project: Project,
177
+ private val mcpClient: MCPClient
178
+ ) {
179
+
180
+ fun executeBrowserAutomation(workflow: BrowserWorkflow) {
181
+ object : Task.Backgroundable(project, "Executing Browser Automation", true) {
182
+ override fun run(indicator: ProgressIndicator) {
183
+ runBlocking {
184
+ try {
185
+ indicator.text = "Opening browser..."
186
+ val browserResult = mcpClient.callTool("open_browser", workflow.browserConfig)
187
+
188
+ if (!browserResult.isSuccess()) {
189
+ throw Exception("Failed to open browser: ${browserResult.error?.message}")
190
+ }
191
+
192
+ workflow.steps.forEachIndexed { index, step ->
193
+ indicator.text = "Executing step ${index + 1}/${workflow.steps.size}: ${step.description}"
194
+ indicator.fraction = (index + 1).toDouble() / workflow.steps.size
195
+
196
+ val result = mcpClient.callTool(step.toolName, step.parameters)
197
+
198
+ if (!result.isSuccess()) {
199
+ throw Exception("Step failed: ${result.error?.message}")
200
+ }
201
+
202
+ // Optional: Take screenshot after each step
203
+ if (step.takeScreenshot) {
204
+ mcpClient.callTool("take_screenshot", mapOf(
205
+ "filename" to "step_${index + 1}_${step.toolName}.png"
206
+ ))
207
+ }
208
+ }
209
+
210
+ indicator.text = "Closing browser..."
211
+ mcpClient.callTool("close_browser", emptyMap())
212
+
213
+ showSuccessNotification("Browser automation completed successfully")
214
+
215
+ } catch (e: Exception) {
216
+ showErrorNotification("Browser automation failed: ${e.message}")
217
+ }
218
+ }
219
+ }
220
+ }.queue()
221
+ }
222
+
223
+ fun createTestSuite(testSuiteConfig: TestSuiteConfig) {
224
+ object : Task.Backgroundable(project, "Creating Test Suite", false) {
225
+ override fun run(indicator: ProgressIndicator) {
226
+ runBlocking {
227
+ try {
228
+ indicator.text = "Creating test suite..."
229
+
230
+ val result = mcpClient.callTool("create_test_suite", mapOf(
231
+ "feature_name" to testSuiteConfig.featureName,
232
+ "scenarios" to testSuiteConfig.scenarios,
233
+ "page_elements" to testSuiteConfig.pageElements,
234
+ "tags" to testSuiteConfig.tags
235
+ ))
236
+
237
+ if (result.isSuccess()) {
238
+ // Refresh project files
239
+ project.baseDir.refresh(false, true)
240
+ showSuccessNotification("Test suite created successfully")
241
+
242
+ // Open generated files
243
+ openGeneratedFiles(testSuiteConfig.featureName)
244
+ } else {
245
+ throw Exception(result.error?.message ?: "Unknown error")
246
+ }
247
+
248
+ } catch (e: Exception) {
249
+ showErrorNotification("Failed to create test suite: ${e.message}")
250
+ }
251
+ }
252
+ }
253
+ }.queue()
254
+ }
255
+
256
+ fun executeTests(testConfig: TestExecutionConfig) {
257
+ object : Task.Backgroundable(project, "Executing Tests", true) {
258
+ override fun run(indicator: ProgressIndicator) {
259
+ runBlocking {
260
+ try {
261
+ indicator.text = "Executing tests..."
262
+
263
+ val result = mcpClient.callTool("execute_tests", mapOf(
264
+ "tags" to testConfig.tags,
265
+ "feature_file" to testConfig.featureFile,
266
+ "parallel" to testConfig.parallel
267
+ ))
268
+
269
+ if (result.isSuccess()) {
270
+ indicator.text = "Generating Allure report..."
271
+ mcpClient.callTool("generate_allure_report", emptyMap())
272
+
273
+ showTestResults(result)
274
+ } else {
275
+ throw Exception(result.error?.message ?: "Test execution failed")
276
+ }
277
+
278
+ } catch (e: Exception) {
279
+ showErrorNotification("Test execution failed: ${e.message}")
280
+ }
281
+ }
282
+ }
283
+ }.queue()
284
+ }
285
+
286
+ private fun MCPResponse.isSuccess(): Boolean = error == null
287
+
288
+ private fun showSuccessNotification(message: String) {
289
+ // Implementation for success notification
290
+ }
291
+
292
+ private fun showErrorNotification(message: String) {
293
+ // Implementation for error notification
294
+ }
295
+
296
+ private fun openGeneratedFiles(featureName: String) {
297
+ // Implementation to open generated files in editor
298
+ }
299
+
300
+ private fun showTestResults(result: MCPResponse) {
301
+ // Implementation to show test results in tool window
302
+ }
303
+ }
304
+
305
+ data class BrowserWorkflow(
306
+ val name: String,
307
+ val browserConfig: Map<String, Any>,
308
+ val steps: List<WorkflowStep>
309
+ )
310
+
311
+ data class WorkflowStep(
312
+ val toolName: String,
313
+ val parameters: Map<String, Any>,
314
+ val description: String,
315
+ val takeScreenshot: Boolean = false
316
+ )
317
+
318
+ data class TestSuiteConfig(
319
+ val featureName: String,
320
+ val scenarios: List<Map<String, Any>>,
321
+ val pageElements: List<Map<String, Any>>,
322
+ val tags: List<String>
323
+ )
324
+
325
+ data class TestExecutionConfig(
326
+ val tags: List<String> = emptyList(),
327
+ val featureFile: String? = null,
328
+ val parallel: Boolean = false
329
+ )
330
+ ```
331
+
332
+ ---
333
+
334
+ ## 🎨 UI Components
335
+
336
+ ### MCPToolWindow.kt
337
+ ```kotlin
338
+ package com.hakalab.mcp.ui
339
+
340
+ import com.intellij.openapi.project.Project
341
+ import com.intellij.openapi.wm.ToolWindow
342
+ import com.intellij.openapi.wm.ToolWindowFactory
343
+ import com.intellij.ui.content.ContentFactory
344
+ import javax.swing.*
345
+ import java.awt.*
346
+ import java.awt.event.ActionEvent
347
+
348
+ class MCPToolWindowFactory : ToolWindowFactory {
349
+ override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
350
+ val mcpToolWindow = MCPToolWindow(project)
351
+ val content = ContentFactory.SERVICE.getInstance()
352
+ .createContent(mcpToolWindow.getContent(), "", false)
353
+ toolWindow.contentManager.addContent(content)
354
+ }
355
+ }
356
+
357
+ class MCPToolWindow(private val project: Project) {
358
+ private val mcpClient = MCPClient(MCPConfig())
359
+ private val toolExecutor = ToolExecutor(project, mcpClient)
360
+
361
+ fun getContent(): JComponent {
362
+ val panel = JPanel(BorderLayout())
363
+
364
+ // Create tabbed pane
365
+ val tabbedPane = JTabbedPane()
366
+
367
+ // Browser Automation Tab
368
+ tabbedPane.addTab("Browser Automation", createBrowserAutomationPanel())
369
+
370
+ // Test Creation Tab
371
+ tabbedPane.addTab("Test Creation", createTestCreationPanel())
372
+
373
+ // Test Execution Tab
374
+ tabbedPane.addTab("Test Execution", createTestExecutionPanel())
375
+
376
+ // Tools Tab
377
+ tabbedPane.addTab("Available Tools", createToolsPanel())
378
+
379
+ panel.add(tabbedPane, BorderLayout.CENTER)
380
+
381
+ return panel
382
+ }
383
+
384
+ private fun createBrowserAutomationPanel(): JPanel {
385
+ val panel = JPanel(GridBagLayout())
386
+ val gbc = GridBagConstraints()
387
+
388
+ // Engine selection
389
+ gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST
390
+ panel.add(JLabel("Engine:"), gbc)
391
+
392
+ gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL
393
+ val engineCombo = JComboBox(arrayOf("selenium", "playwright"))
394
+ panel.add(engineCombo, gbc)
395
+
396
+ // Browser selection
397
+ gbc.gridx = 0; gbc.gridy = 1; gbc.fill = GridBagConstraints.NONE
398
+ panel.add(JLabel("Browser:"), gbc)
399
+
400
+ gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL
401
+ val browserCombo = JComboBox(arrayOf("chrome", "firefox", "edge", "safari"))
402
+ panel.add(browserCombo, gbc)
403
+
404
+ // URL field
405
+ gbc.gridx = 0; gbc.gridy = 2; gbc.fill = GridBagConstraints.NONE
406
+ panel.add(JLabel("URL:"), gbc)
407
+
408
+ gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL
409
+ val urlField = JTextField("https://")
410
+ panel.add(urlField, gbc)
411
+
412
+ // Actions area
413
+ gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 2; gbc.fill = GridBagConstraints.BOTH
414
+ val actionsArea = JTextArea(10, 40)
415
+ actionsArea.text = """
416
+ # Example automation script:
417
+ # 1. Open browser
418
+ # 2. Navigate to URL
419
+ # 3. Fill form fields
420
+ # 4. Click submit
421
+ # 5. Take screenshot
422
+ # 6. Close browser
423
+ """.trimIndent()
424
+ panel.add(JScrollPane(actionsArea), gbc)
425
+
426
+ // Execute button
427
+ gbc.gridy = 4; gbc.fill = GridBagConstraints.NONE
428
+ val executeButton = JButton("Execute Automation")
429
+ executeButton.addActionListener { executeAutomation(engineCombo, browserCombo, urlField, actionsArea) }
430
+ panel.add(executeButton, gbc)
431
+
432
+ return panel
433
+ }
434
+
435
+ private fun createTestCreationPanel(): JPanel {
436
+ val panel = JPanel(GridBagLayout())
437
+ val gbc = GridBagConstraints()
438
+
439
+ // Feature name
440
+ gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST
441
+ panel.add(JLabel("Feature Name:"), gbc)
442
+
443
+ gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL
444
+ val featureNameField = JTextField()
445
+ panel.add(featureNameField, gbc)
446
+
447
+ // Scenarios
448
+ gbc.gridx = 0; gbc.gridy = 1; gbc.fill = GridBagConstraints.NONE
449
+ panel.add(JLabel("Scenarios:"), gbc)
450
+
451
+ gbc.gridx = 1; gbc.gridy = 1; gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1.0; gbc.weighty = 0.5
452
+ val scenariosArea = JTextArea()
453
+ scenariosArea.text = """
454
+ Scenario: User login
455
+ Given I am on the login page
456
+ When I enter valid credentials
457
+ Then I should be logged in
458
+ """.trimIndent()
459
+ panel.add(JScrollPane(scenariosArea), gbc)
460
+
461
+ // Page elements
462
+ gbc.gridy = 2
463
+ val elementsArea = JTextArea()
464
+ elementsArea.text = """
465
+ username_field,ID,username
466
+ password_field,ID,password
467
+ login_button,CSS,.login-btn
468
+ """.trimIndent()
469
+ panel.add(JScrollPane(elementsArea), gbc)
470
+
471
+ // Create button
472
+ gbc.gridy = 3; gbc.fill = GridBagConstraints.NONE; gbc.weighty = 0.0
473
+ val createButton = JButton("Create Test Suite")
474
+ createButton.addActionListener { createTestSuite(featureNameField, scenariosArea, elementsArea) }
475
+ panel.add(createButton, gbc)
476
+
477
+ return panel
478
+ }
479
+
480
+ private fun createTestExecutionPanel(): JPanel {
481
+ val panel = JPanel(GridBagLayout())
482
+ val gbc = GridBagConstraints()
483
+
484
+ // Tags filter
485
+ gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST
486
+ panel.add(JLabel("Tags:"), gbc)
487
+
488
+ gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL
489
+ val tagsField = JTextField("selenium,critical")
490
+ panel.add(tagsField, gbc)
491
+
492
+ // Feature file
493
+ gbc.gridx = 0; gbc.gridy = 1; gbc.fill = GridBagConstraints.NONE
494
+ panel.add(JLabel("Feature File:"), gbc)
495
+
496
+ gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL
497
+ val featureFileField = JTextField()
498
+ panel.add(featureFileField, gbc)
499
+
500
+ // Parallel execution
501
+ gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2
502
+ val parallelCheckbox = JCheckBox("Parallel Execution")
503
+ panel.add(parallelCheckbox, gbc)
504
+
505
+ // Execute button
506
+ gbc.gridy = 3
507
+ val executeButton = JButton("Execute Tests")
508
+ executeButton.addActionListener { executeTests(tagsField, featureFileField, parallelCheckbox) }
509
+ panel.add(executeButton, gbc)
510
+
511
+ // Results area
512
+ gbc.gridy = 4; gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1.0; gbc.weighty = 1.0
513
+ val resultsArea = JTextArea()
514
+ resultsArea.isEditable = false
515
+ panel.add(JScrollPane(resultsArea), gbc)
516
+
517
+ return panel
518
+ }
519
+
520
+ private fun createToolsPanel(): JPanel {
521
+ val panel = JPanel(BorderLayout())
522
+
523
+ // Tools list
524
+ val toolsModel = DefaultListModel<String>()
525
+ val toolsList = JList(toolsModel)
526
+
527
+ // Load available tools
528
+ SwingUtilities.invokeLater {
529
+ loadAvailableTools(toolsModel)
530
+ }
531
+
532
+ panel.add(JScrollPane(toolsList), BorderLayout.CENTER)
533
+
534
+ // Tool details
535
+ val detailsArea = JTextArea()
536
+ detailsArea.isEditable = false
537
+ panel.add(JScrollPane(detailsArea), BorderLayout.SOUTH)
538
+
539
+ // Show tool details on selection
540
+ toolsList.addListSelectionListener { e ->
541
+ if (!e.valueIsAdjusting) {
542
+ val selectedTool = toolsList.selectedValue
543
+ if (selectedTool != null) {
544
+ showToolDetails(selectedTool, detailsArea)
545
+ }
546
+ }
547
+ }
548
+
549
+ return panel
550
+ }
551
+
552
+ private fun executeAutomation(
553
+ engineCombo: JComboBox<String>,
554
+ browserCombo: JComboBox<String>,
555
+ urlField: JTextField,
556
+ actionsArea: JTextArea
557
+ ) {
558
+ val workflow = BrowserWorkflow(
559
+ name = "Custom Automation",
560
+ browserConfig = mapOf(
561
+ "engine" to engineCombo.selectedItem,
562
+ "browser" to browserCombo.selectedItem
563
+ ),
564
+ steps = parseAutomationSteps(actionsArea.text, urlField.text)
565
+ )
566
+
567
+ toolExecutor.executeBrowserAutomation(workflow)
568
+ }
569
+
570
+ private fun createTestSuite(
571
+ featureNameField: JTextField,
572
+ scenariosArea: JTextArea,
573
+ elementsArea: JTextArea
574
+ ) {
575
+ val config = TestSuiteConfig(
576
+ featureName = featureNameField.text,
577
+ scenarios = parseScenarios(scenariosArea.text),
578
+ pageElements = parsePageElements(elementsArea.text),
579
+ tags = listOf("automated", "ui")
580
+ )
581
+
582
+ toolExecutor.createTestSuite(config)
583
+ }
584
+
585
+ private fun executeTests(
586
+ tagsField: JTextField,
587
+ featureFileField: JTextField,
588
+ parallelCheckbox: JCheckBox
589
+ ) {
590
+ val config = TestExecutionConfig(
591
+ tags = tagsField.text.split(",").map { it.trim() },
592
+ featureFile = featureFileField.text.takeIf { it.isNotBlank() },
593
+ parallel = parallelCheckbox.isSelected
594
+ )
595
+
596
+ toolExecutor.executeTests(config)
597
+ }
598
+
599
+ private fun loadAvailableTools(model: DefaultListModel<String>) {
600
+ // Load tools asynchronously
601
+ }
602
+
603
+ private fun showToolDetails(toolName: String, detailsArea: JTextArea) {
604
+ // Show tool details
605
+ }
606
+
607
+ private fun parseAutomationSteps(actionsText: String, url: String): List<WorkflowStep> {
608
+ // Parse automation steps from text
609
+ return listOf(
610
+ WorkflowStep("navigate_to_url", mapOf("url" to url), "Navigate to URL")
611
+ )
612
+ }
613
+
614
+ private fun parseScenarios(scenariosText: String): List<Map<String, Any>> {
615
+ // Parse scenarios from text
616
+ return emptyList()
617
+ }
618
+
619
+ private fun parsePageElements(elementsText: String): List<Map<String, Any>> {
620
+ // Parse page elements from CSV-like text
621
+ return emptyList()
622
+ }
623
+ }
624
+ ```
625
+
626
+ ---
627
+
628
+ ## 🤖 AI Agent Integration
629
+
630
+ ### AIAssistant.kt
631
+ ```kotlin
632
+ package com.hakalab.mcp.ai
633
+
634
+ import com.intellij.openapi.editor.Editor
635
+ import com.intellij.openapi.project.Project
636
+ import com.intellij.psi.PsiFile
637
+
638
+ class AIAssistant(
639
+ private val project: Project,
640
+ private val mcpClient: MCPClient
641
+ ) {
642
+
643
+ fun generateTestFromCode(psiFile: PsiFile, editor: Editor): String {
644
+ val codeContext = analyzeCode(psiFile, editor)
645
+
646
+ return when (codeContext.type) {
647
+ CodeType.WEB_PAGE -> generateWebPageTest(codeContext)
648
+ CodeType.API_ENDPOINT -> generateAPITest(codeContext)
649
+ CodeType.FORM_HANDLER -> generateFormTest(codeContext)
650
+ else -> generateGenericTest(codeContext)
651
+ }
652
+ }
653
+
654
+ fun suggestAutomationSteps(context: String): List<AutomationSuggestion> {
655
+ val suggestions = mutableListOf<AutomationSuggestion>()
656
+
657
+ // Analyze context and suggest appropriate MCP tools
658
+ when {
659
+ context.contains("login", ignoreCase = true) -> {
660
+ suggestions.addAll(getLoginAutomationSuggestions())
661
+ }
662
+ context.contains("form", ignoreCase = true) -> {
663
+ suggestions.addAll(getFormAutomationSuggestions())
664
+ }
665
+ context.contains("navigation", ignoreCase = true) -> {
666
+ suggestions.addAll(getNavigationAutomationSuggestions())
667
+ }
668
+ context.contains("test", ignoreCase = true) -> {
669
+ suggestions.addAll(getTestCreationSuggestions())
670
+ }
671
+ }
672
+
673
+ return suggestions
674
+ }
675
+
676
+ fun generatePageObject(htmlContent: String): PageObjectCode {
677
+ val elements = extractElementsFromHTML(htmlContent)
678
+
679
+ val pageObjectCode = buildString {
680
+ appendLine("from selenium.webdriver.common.by import By")
681
+ appendLine()
682
+ appendLine("class GeneratedPage:")
683
+ appendLine(" def __init__(self, browser):")
684
+ appendLine(" self.browser = browser")
685
+ appendLine()
686
+
687
+ elements.forEach { element ->
688
+ appendLine(" ${element.name} = (By.${element.locatorType}, \"${element.locator}\")")
689
+ }
690
+ }
691
+
692
+ return PageObjectCode(
693
+ className = "GeneratedPage",
694
+ code = pageObjectCode,
695
+ elements = elements
696
+ )
697
+ }
698
+
699
+ fun optimizeSelector(originalSelector: String, context: ElementContext): SelectorOptimization {
700
+ val alternatives = generateSelectorAlternatives(originalSelector, context)
701
+
702
+ return SelectorOptimization(
703
+ original = originalSelector,
704
+ recommended = alternatives.first(),
705
+ alternatives = alternatives,
706
+ reasoning = "Optimized for stability and performance"
707
+ )
708
+ }
709
+
710
+ private fun analyzeCode(psiFile: PsiFile, editor: Editor): CodeContext {
711
+ // Analyze the code to understand its purpose and structure
712
+ return CodeContext(
713
+ type = CodeType.WEB_PAGE,
714
+ elements = emptyList(),
715
+ actions = emptyList(),
716
+ metadata = emptyMap()
717
+ )
718
+ }
719
+
720
+ private fun generateWebPageTest(context: CodeContext): String {
721
+ return buildString {
722
+ appendLine("Feature: ${context.getFeatureName()}")
723
+ appendLine(" Automated test for web page functionality")
724
+ appendLine()
725
+ appendLine(" Scenario: Basic page interaction")
726
+ appendLine(" Given I open the browser")
727
+ appendLine(" When I navigate to the page")
728
+ appendLine(" Then I should see the expected elements")
729
+ }
730
+ }
731
+
732
+ private fun getLoginAutomationSuggestions(): List<AutomationSuggestion> {
733
+ return listOf(
734
+ AutomationSuggestion(
735
+ tool = "open_browser",
736
+ description = "Open browser for login testing",
737
+ parameters = mapOf("browser" to "chrome", "engine" to "playwright"),
738
+ priority = 1
739
+ ),
740
+ AutomationSuggestion(
741
+ tool = "navigate_to_url",
742
+ description = "Navigate to login page",
743
+ parameters = mapOf("url" to "https://example.com/login"),
744
+ priority = 2
745
+ ),
746
+ AutomationSuggestion(
747
+ tool = "fill_input",
748
+ description = "Fill username field",
749
+ parameters = mapOf("selector" to "#username", "text" to "testuser"),
750
+ priority = 3
751
+ ),
752
+ AutomationSuggestion(
753
+ tool = "fill_input",
754
+ description = "Fill password field",
755
+ parameters = mapOf("selector" to "#password", "text" to "password123"),
756
+ priority = 4
757
+ ),
758
+ AutomationSuggestion(
759
+ tool = "click_element",
760
+ description = "Click login button",
761
+ parameters = mapOf("selector" to "button[type='submit']"),
762
+ priority = 5
763
+ ),
764
+ AutomationSuggestion(
765
+ tool = "take_screenshot",
766
+ description = "Capture login result",
767
+ parameters = mapOf("filename" to "login-result.png"),
768
+ priority = 6
769
+ )
770
+ )
771
+ }
772
+
773
+ private fun getFormAutomationSuggestions(): List<AutomationSuggestion> {
774
+ return listOf(
775
+ AutomationSuggestion(
776
+ tool = "fill_input",
777
+ description = "Fill form fields",
778
+ parameters = mapOf("selector" to "input[name='field']", "text" to "value"),
779
+ priority = 1
780
+ ),
781
+ AutomationSuggestion(
782
+ tool = "select_option",
783
+ description = "Select dropdown option",
784
+ parameters = mapOf("selector" to "select[name='dropdown']", "value" to "option1"),
785
+ priority = 2
786
+ ),
787
+ AutomationSuggestion(
788
+ tool = "click_element",
789
+ description = "Submit form",
790
+ parameters = mapOf("selector" to "input[type='submit']"),
791
+ priority = 3
792
+ )
793
+ )
794
+ }
795
+
796
+ private fun getNavigationAutomationSuggestions(): List<AutomationSuggestion> {
797
+ return listOf(
798
+ AutomationSuggestion(
799
+ tool = "navigate_to_url",
800
+ description = "Navigate to page",
801
+ parameters = mapOf("url" to "https://example.com"),
802
+ priority = 1
803
+ ),
804
+ AutomationSuggestion(
805
+ tool = "click_element",
806
+ description = "Click navigation link",
807
+ parameters = mapOf("selector" to "a[href='/page']"),
808
+ priority = 2
809
+ ),
810
+ AutomationSuggestion(
811
+ tool = "go_back",
812
+ description = "Navigate back",
813
+ parameters = emptyMap(),
814
+ priority = 3
815
+ )
816
+ )
817
+ }
818
+
819
+ private fun getTestCreationSuggestions(): List<AutomationSuggestion> {
820
+ return listOf(
821
+ AutomationSuggestion(
822
+ tool = "create_test_suite",
823
+ description = "Create comprehensive test suite",
824
+ parameters = mapOf(
825
+ "feature_name" to "New Feature",
826
+ "scenarios" to listOf(mapOf("name" to "Basic scenario"))
827
+ ),
828
+ priority = 1
829
+ ),
830
+ AutomationSuggestion(
831
+ tool = "create_page_object",
832
+ description = "Create page object model",
833
+ parameters = mapOf(
834
+ "page_name" to "HomePage",
835
+ "elements" to listOf(mapOf("name" to "element", "type" to "ID", "locator" to "id"))
836
+ ),
837
+ priority = 2
838
+ )
839
+ )
840
+ }
841
+
842
+ private fun extractElementsFromHTML(htmlContent: String): List<PageElement> {
843
+ // Parse HTML and extract interactive elements
844
+ return emptyList()
845
+ }
846
+
847
+ private fun generateSelectorAlternatives(selector: String, context: ElementContext): List<String> {
848
+ // Generate alternative selectors based on context
849
+ return listOf(selector)
850
+ }
851
+ }
852
+
853
+ data class CodeContext(
854
+ val type: CodeType,
855
+ val elements: List<String>,
856
+ val actions: List<String>,
857
+ val metadata: Map<String, Any>
858
+ ) {
859
+ fun getFeatureName(): String = metadata["featureName"] as? String ?: "Generated Feature"
860
+ }
861
+
862
+ enum class CodeType {
863
+ WEB_PAGE, API_ENDPOINT, FORM_HANDLER, GENERIC
864
+ }
865
+
866
+ data class AutomationSuggestion(
867
+ val tool: String,
868
+ val description: String,
869
+ val parameters: Map<String, Any>,
870
+ val priority: Int
871
+ )
872
+
873
+ data class PageObjectCode(
874
+ val className: String,
875
+ val code: String,
876
+ val elements: List<PageElement>
877
+ )
878
+
879
+ data class PageElement(
880
+ val name: String,
881
+ val locatorType: String,
882
+ val locator: String
883
+ )
884
+
885
+ data class ElementContext(
886
+ val tagName: String,
887
+ val attributes: Map<String, String>,
888
+ val parentContext: String
889
+ )
890
+
891
+ data class SelectorOptimization(
892
+ val original: String,
893
+ val recommended: String,
894
+ val alternatives: List<String>,
895
+ val reasoning: String
896
+ )
897
+ ```
898
+
899
+ ---
900
+
901
+ ## 🔧 Code Generation
902
+
903
+ ### CodeGenerator.kt
904
+ ```kotlin
905
+ package com.hakalab.mcp.codegen
906
+
907
+ import com.intellij.openapi.command.WriteCommandAction
908
+ import com.intellij.openapi.editor.Editor
909
+ import com.intellij.openapi.project.Project
910
+ import com.intellij.psi.PsiFile
911
+ import com.intellij.psi.PsiFileFactory
912
+
913
+ class CodeGenerator(private val project: Project) {
914
+
915
+ fun generateBDDFeature(config: FeatureConfig): String {
916
+ return buildString {
917
+ // Feature header
918
+ if (config.tags.isNotEmpty()) {
919
+ appendLine(config.tags.joinToString(" ") { "@$it" })
920
+ }
921
+ appendLine("Feature: ${config.name}")
922
+ appendLine(" ${config.description}")
923
+ appendLine()
924
+
925
+ // Background (if any)
926
+ config.background?.let { background ->
927
+ appendLine(" Background:")
928
+ background.steps.forEach { step ->
929
+ appendLine(" $step")
930
+ }
931
+ appendLine()
932
+ }
933
+
934
+ // Scenarios
935
+ config.scenarios.forEach { scenario ->
936
+ if (scenario.tags.isNotEmpty()) {
937
+ appendLine(" ${scenario.tags.joinToString(" ") { "@$it" }}")
938
+ }
939
+ appendLine(" Scenario: ${scenario.name}")
940
+ if (scenario.description.isNotEmpty()) {
941
+ appendLine(" ${scenario.description}")
942
+ }
943
+ scenario.steps.forEach { step ->
944
+ appendLine(" $step")
945
+ }
946
+ appendLine()
947
+ }
948
+ }
949
+ }
950
+
951
+ fun generateStepDefinitions(feature: String, language: Language = Language.PYTHON): String {
952
+ val steps = extractStepsFromFeature(feature)
953
+
954
+ return when (language) {
955
+ Language.PYTHON -> generatePythonSteps(steps)
956
+ Language.JAVA -> generateJavaSteps(steps)
957
+ Language.JAVASCRIPT -> generateJavaScriptSteps(steps)
958
+ }
959
+ }
960
+
961
+ fun generatePageObject(elements: List<PageElement>, language: Language = Language.PYTHON): String {
962
+ return when (language) {
963
+ Language.PYTHON -> generatePythonPageObject(elements)
964
+ Language.JAVA -> generateJavaPageObject(elements)
965
+ Language.JAVASCRIPT -> generateJavaScriptPageObject(elements)
966
+ }
967
+ }
968
+
969
+ fun generateAutomationScript(workflow: AutomationWorkflow): String {
970
+ return buildString {
971
+ appendLine("# Generated automation script")
972
+ appendLine("# Workflow: ${workflow.name}")
973
+ appendLine()
974
+
975
+ // Imports
976
+ appendLine("import asyncio")
977
+ appendLine("from hakalab_mcp_client import MCPClient")
978
+ appendLine()
979
+
980
+ // Main function
981
+ appendLine("async def main():")
982
+ appendLine(" client = MCPClient()")
983
+ appendLine(" await client.connect()")
984
+ appendLine()
985
+
986
+ // Workflow steps
987
+ workflow.steps.forEach { step ->
988
+ appendLine(" # ${step.description}")
989
+ appendLine(" result = await client.call_tool('${step.toolName}', {")
990
+ step.parameters.forEach { (key, value) ->
991
+ appendLine(" '$key': ${formatValue(value)},")
992
+ }
993
+ appendLine(" })")
994
+ appendLine(" print(f'${step.toolName}: {result}')")
995
+ appendLine()
996
+ }
997
+
998
+ appendLine(" await client.disconnect()")
999
+ appendLine()
1000
+ appendLine("if __name__ == '__main__':")
1001
+ appendLine(" asyncio.run(main())")
1002
+ }
1003
+ }
1004
+
1005
+ fun insertCodeIntoEditor(editor: Editor, code: String, insertAtCursor: Boolean = true) {
1006
+ WriteCommandAction.runWriteCommandAction(project) {
1007
+ val document = editor.document
1008
+ val offset = if (insertAtCursor) editor.caretModel.offset else document.textLength
1009
+ document.insertString(offset, code)
1010
+ }
1011
+ }
1012
+
1013
+ fun createNewFile(fileName: String, content: String, directory: String = ""): PsiFile {
1014
+ val fileType = getFileTypeFromExtension(fileName)
1015
+
1016
+ return WriteCommandAction.computeAndWait<PsiFile, Exception> {
1017
+ PsiFileFactory.getInstance(project).createFileFromText(fileName, fileType, content)
1018
+ }
1019
+ }
1020
+
1021
+ private fun generatePythonSteps(steps: List<StepInfo>): String {
1022
+ return buildString {
1023
+ appendLine("from behave import given, when, then")
1024
+ appendLine("from assertpy import assert_that")
1025
+ appendLine("import asyncio")
1026
+ appendLine("from hakalab_mcp_client import MCPClient")
1027
+ appendLine()
1028
+
1029
+ steps.forEach { step ->
1030
+ val stepType = step.type.lowercase()
1031
+ val functionName = step.text
1032
+ .replace(Regex("[^\\w\\s]"), "")
1033
+ .replace(Regex("\\s+"), "_")
1034
+ .lowercase()
1035
+
1036
+ appendLine("@$stepType('${step.text}')")
1037
+ appendLine("def ${functionName}(context):")
1038
+ appendLine(" \"\"\"")
1039
+ appendLine(" Step: ${step.text}")
1040
+ appendLine(" \"\"\"")
1041
+ appendLine(" # TODO: Implement step logic using MCP tools")
1042
+ appendLine(" # Example:")
1043
+ appendLine(" # result = await context.mcp_client.call_tool('click_element', {")
1044
+ appendLine(" # 'selector': '#element-id'")
1045
+ appendLine(" # })")
1046
+ appendLine(" pass")
1047
+ appendLine()
1048
+ }
1049
+ }
1050
+ }
1051
+
1052
+ private fun generateJavaSteps(steps: List<StepInfo>): String {
1053
+ return buildString {
1054
+ appendLine("import io.cucumber.java.en.*;")
1055
+ appendLine("import static org.junit.Assert.*;")
1056
+ appendLine()
1057
+ appendLine("public class StepDefinitions {")
1058
+ appendLine()
1059
+
1060
+ steps.forEach { step ->
1061
+ val methodName = step.text
1062
+ .replace(Regex("[^\\w\\s]"), "")
1063
+ .replace(Regex("\\s+"), "_")
1064
+ .lowercase()
1065
+
1066
+ appendLine(" @${step.type}(\"${step.text}\")")
1067
+ appendLine(" public void ${methodName}() {")
1068
+ appendLine(" // TODO: Implement step logic using MCP tools")
1069
+ appendLine(" // Example MCP tool call would go here")
1070
+ appendLine(" }")
1071
+ appendLine()
1072
+ }
1073
+
1074
+ appendLine("}")
1075
+ }
1076
+ }
1077
+
1078
+ private fun generateJavaScriptSteps(steps: List<StepInfo>): String {
1079
+ return buildString {
1080
+ appendLine("const { Given, When, Then } = require('@cucumber/cucumber');")
1081
+ appendLine("const { MCPClient } = require('hakalab-mcp-client');")
1082
+ appendLine()
1083
+
1084
+ steps.forEach { step ->
1085
+ val stepType = step.type
1086
+ appendLine("${stepType}('${step.text}', async function () {")
1087
+ appendLine(" // TODO: Implement step logic using MCP tools")
1088
+ appendLine(" // const result = await this.mcpClient.callTool('tool_name', {});")
1089
+ appendLine("});")
1090
+ appendLine()
1091
+ }
1092
+ }
1093
+ }
1094
+
1095
+ private fun generatePythonPageObject(elements: List<PageElement>): String {
1096
+ return buildString {
1097
+ appendLine("from selenium.webdriver.common.by import By")
1098
+ appendLine()
1099
+ appendLine("class GeneratedPage:")
1100
+ appendLine(" def __init__(self, browser):")
1101
+ appendLine(" self.browser = browser")
1102
+ appendLine()
1103
+
1104
+ elements.forEach { element ->
1105
+ appendLine(" ${element.name} = (By.${element.locatorType}, \"${element.locator}\")")
1106
+ }
1107
+
1108
+ appendLine()
1109
+ appendLine(" def wait_for_page_load(self):")
1110
+ appendLine(" # Wait for page to load completely")
1111
+ appendLine(" pass")
1112
+ }
1113
+ }
1114
+
1115
+ private fun generateJavaPageObject(elements: List<PageElement>): String {
1116
+ return buildString {
1117
+ appendLine("import org.openqa.selenium.By;")
1118
+ appendLine("import org.openqa.selenium.WebDriver;")
1119
+ appendLine("import org.openqa.selenium.WebElement;")
1120
+ appendLine()
1121
+ appendLine("public class GeneratedPage {")
1122
+ appendLine(" private WebDriver driver;")
1123
+ appendLine()
1124
+ appendLine(" public GeneratedPage(WebDriver driver) {")
1125
+ appendLine(" this.driver = driver;")
1126
+ appendLine(" }")
1127
+ appendLine()
1128
+
1129
+ elements.forEach { element ->
1130
+ appendLine(" private By ${element.name}Locator = By.${element.locatorType.lowercase()}(\"${element.locator}\");")
1131
+ appendLine()
1132
+ appendLine(" public WebElement get${element.name.capitalize()}() {")
1133
+ appendLine(" return driver.findElement(${element.name}Locator);")
1134
+ appendLine(" }")
1135
+ appendLine()
1136
+ }
1137
+
1138
+ appendLine("}")
1139
+ }
1140
+ }
1141
+
1142
+ private fun generateJavaScriptPageObject(elements: List<PageElement>): String {
1143
+ return buildString {
1144
+ appendLine("class GeneratedPage {")
1145
+ appendLine(" constructor(page) {")
1146
+ appendLine(" this.page = page;")
1147
+ appendLine(" }")
1148
+ appendLine()
1149
+
1150
+ elements.forEach { element ->
1151
+ val selectorMethod = when (element.locatorType.uppercase()) {
1152
+ "ID" -> "page.locator('#${element.locator}')"
1153
+ "CSS_SELECTOR" -> "page.locator('${element.locator}')"
1154
+ "XPATH" -> "page.locator('xpath=${element.locator}')"
1155
+ else -> "page.locator('${element.locator}')"
1156
+ }
1157
+
1158
+ appendLine(" get ${element.name}() {")
1159
+ appendLine(" return this.$selectorMethod;")
1160
+ appendLine(" }")
1161
+ appendLine()
1162
+ }
1163
+
1164
+ appendLine("}")
1165
+ appendLine()
1166
+ appendLine("module.exports = GeneratedPage;")
1167
+ }
1168
+ }
1169
+
1170
+ private fun extractStepsFromFeature(feature: String): List<StepInfo> {
1171
+ val steps = mutableListOf<StepInfo>()
1172
+ val stepPattern = Regex("^\\s*(Given|When|Then|And|But)\\s+(.+)$", RegexOption.MULTILINE)
1173
+
1174
+ stepPattern.findAll(feature).forEach { match ->
1175
+ val type = match.groupValues[1]
1176
+ val text = match.groupValues[2]
1177
+ steps.add(StepInfo(type, text))
1178
+ }
1179
+
1180
+ return steps
1181
+ }
1182
+
1183
+ private fun formatValue(value: Any): String {
1184
+ return when (value) {
1185
+ is String -> "'$value'"
1186
+ is Number -> value.toString()
1187
+ is Boolean -> value.toString()
1188
+ else -> "'$value'"
1189
+ }
1190
+ }
1191
+
1192
+ private fun getFileTypeFromExtension(fileName: String): com.intellij.openapi.fileTypes.FileType {
1193
+ // Return appropriate file type based on extension
1194
+ return com.intellij.openapi.fileTypes.PlainTextFileType.INSTANCE
1195
+ }
1196
+ }
1197
+
1198
+ data class FeatureConfig(
1199
+ val name: String,
1200
+ val description: String,
1201
+ val tags: List<String>,
1202
+ val background: BackgroundConfig?,
1203
+ val scenarios: List<ScenarioConfig>
1204
+ )
1205
+
1206
+ data class BackgroundConfig(
1207
+ val steps: List<String>
1208
+ )
1209
+
1210
+ data class ScenarioConfig(
1211
+ val name: String,
1212
+ val description: String,
1213
+ val tags: List<String>,
1214
+ val steps: List<String>
1215
+ )
1216
+
1217
+ data class StepInfo(
1218
+ val type: String,
1219
+ val text: String
1220
+ )
1221
+
1222
+ data class AutomationWorkflow(
1223
+ val name: String,
1224
+ val description: String,
1225
+ val steps: List<WorkflowStep>
1226
+ )
1227
+
1228
+ enum class Language {
1229
+ PYTHON, JAVA, JAVASCRIPT
1230
+ }
1231
+ ```
1232
+
1233
+ ---
1234
+
1235
+ ## 🧪 Test Execution
1236
+
1237
+ ### TestRunner.kt
1238
+ ```kotlin
1239
+ package com.hakalab.mcp.testing
1240
+
1241
+ import com.intellij.execution.configurations.ConfigurationFactory
1242
+ import com.intellij.execution.configurations.ConfigurationType
1243
+ import com.intellij.execution.configurations.RunConfiguration
1244
+ import com.intellij.execution.configurations.RunConfigurationBase
1245
+ import com.intellij.openapi.project.Project
1246
+ import kotlinx.coroutines.runBlocking
1247
+
1248
+ class MCPTestRunner(
1249
+ private val project: Project,
1250
+ private val mcpClient: MCPClient
1251
+ ) {
1252
+
1253
+ fun runSingleTest(testFile: String): TestResult {
1254
+ return runBlocking {
1255
+ try {
1256
+ val result = mcpClient.callTool("execute_tests", mapOf(
1257
+ "feature_file" to testFile
1258
+ ))
1259
+
1260
+ if (result.error == null) {
1261
+ // Generate report
1262
+ mcpClient.callTool("generate_allure_report", emptyMap())
1263
+
1264
+ TestResult.success(
1265
+ testFile = testFile,
1266
+ output = result.result?.get("message") as? String ?: "Test completed",
1267
+ duration = 0L // TODO: Extract duration from result
1268
+ )
1269
+ } else {
1270
+ TestResult.failure(
1271
+ testFile = testFile,
1272
+ error = result.error.message,
1273
+ output = result.error.data as? String ?: ""
1274
+ )
1275
+ }
1276
+ } catch (e: Exception) {
1277
+ TestResult.failure(
1278
+ testFile = testFile,
1279
+ error = e.message ?: "Unknown error",
1280
+ output = e.stackTraceToString()
1281
+ )
1282
+ }
1283
+ }
1284
+ }
1285
+
1286
+ fun runTestSuite(tags: List<String> = emptyList(), parallel: Boolean = false): TestSuiteResult {
1287
+ return runBlocking {
1288
+ try {
1289
+ val result = mcpClient.callTool("execute_tests", mapOf(
1290
+ "tags" to tags,
1291
+ "parallel" to parallel
1292
+ ))
1293
+
1294
+ if (result.error == null) {
1295
+ // Generate report
1296
+ val reportResult = mcpClient.callTool("generate_allure_report", emptyMap())
1297
+
1298
+ TestSuiteResult.success(
1299
+ totalTests = 0, // TODO: Extract from result
1300
+ passedTests = 0,
1301
+ failedTests = 0,
1302
+ skippedTests = 0,
1303
+ duration = 0L,
1304
+ reportPath = extractReportPath(reportResult)
1305
+ )
1306
+ } else {
1307
+ TestSuiteResult.failure(
1308
+ error = result.error.message,
1309
+ output = result.error.data as? String ?: ""
1310
+ )
1311
+ }
1312
+ } catch (e: Exception) {
1313
+ TestSuiteResult.failure(
1314
+ error = e.message ?: "Unknown error",
1315
+ output = e.stackTraceToString()
1316
+ )
1317
+ }
1318
+ }
1319
+ }
1320
+
1321
+ fun validateTestFiles(): ValidationResult {
1322
+ return runBlocking {
1323
+ try {
1324
+ val result = mcpClient.callTool("sanitize_tests", emptyMap())
1325
+
1326
+ if (result.error == null) {
1327
+ ValidationResult.success(
1328
+ message = result.result?.get("message") as? String ?: "All tests are valid"
1329
+ )
1330
+ } else {
1331
+ ValidationResult.failure(
1332
+ issues = parseValidationIssues(result.error.data as? String ?: "")
1333
+ )
1334
+ }
1335
+ } catch (e: Exception) {
1336
+ ValidationResult.failure(
1337
+ issues = listOf(ValidationIssue(
1338
+ file = "unknown",
1339
+ line = 0,
1340
+ message = e.message ?: "Validation failed",
1341
+ severity = ValidationSeverity.ERROR
1342
+ ))
1343
+ )
1344
+ }
1345
+ }
1346
+ }
1347
+
1348
+ fun listAvailableTests(): List<TestFileInfo> {
1349
+ return runBlocking {
1350
+ try {
1351
+ val result = mcpClient.callTool("list_test_files", mapOf(
1352
+ "file_type" to "all"
1353
+ ))
1354
+
1355
+ if (result.error == null) {
1356
+ parseTestFilesList(result.result?.get("message") as? String ?: "")
1357
+ } else {
1358
+ emptyList()
1359
+ }
1360
+ } catch (e: Exception) {
1361
+ emptyList()
1362
+ }
1363
+ }
1364
+ }
1365
+
1366
+ private fun extractReportPath(reportResult: MCPResponse): String? {
1367
+ return reportResult.result?.get("report_path") as? String
1368
+ }
1369
+
1370
+ private fun parseValidationIssues(output: String): List<ValidationIssue> {
1371
+ // Parse validation output and extract issues
1372
+ return emptyList()
1373
+ }
1374
+
1375
+ private fun parseTestFilesList(output: String): List<TestFileInfo> {
1376
+ // Parse test files list output
1377
+ return emptyList()
1378
+ }
1379
+ }
1380
+
1381
+ sealed class TestResult {
1382
+ data class Success(
1383
+ val testFile: String,
1384
+ val output: String,
1385
+ val duration: Long
1386
+ ) : TestResult()
1387
+
1388
+ data class Failure(
1389
+ val testFile: String,
1390
+ val error: String,
1391
+ val output: String
1392
+ ) : TestResult()
1393
+
1394
+ companion object {
1395
+ fun success(testFile: String, output: String, duration: Long) = Success(testFile, output, duration)
1396
+ fun failure(testFile: String, error: String, output: String) = Failure(testFile, error, output)
1397
+ }
1398
+ }
1399
+
1400
+ sealed class TestSuiteResult {
1401
+ data class Success(
1402
+ val totalTests: Int,
1403
+ val passedTests: Int,
1404
+ val failedTests: Int,
1405
+ val skippedTests: Int,
1406
+ val duration: Long,
1407
+ val reportPath: String?
1408
+ ) : TestSuiteResult()
1409
+
1410
+ data class Failure(
1411
+ val error: String,
1412
+ val output: String
1413
+ ) : TestSuiteResult()
1414
+
1415
+ companion object {
1416
+ fun success(
1417
+ totalTests: Int,
1418
+ passedTests: Int,
1419
+ failedTests: Int,
1420
+ skippedTests: Int,
1421
+ duration: Long,
1422
+ reportPath: String?
1423
+ ) = Success(totalTests, passedTests, failedTests, skippedTests, duration, reportPath)
1424
+
1425
+ fun failure(error: String, output: String) = Failure(error, output)
1426
+ }
1427
+ }
1428
+
1429
+ sealed class ValidationResult {
1430
+ data class Success(val message: String) : ValidationResult()
1431
+ data class Failure(val issues: List<ValidationIssue>) : ValidationResult()
1432
+
1433
+ companion object {
1434
+ fun success(message: String) = Success(message)
1435
+ fun failure(issues: List<ValidationIssue>) = Failure(issues)
1436
+ }
1437
+ }
1438
+
1439
+ data class ValidationIssue(
1440
+ val file: String,
1441
+ val line: Int,
1442
+ val message: String,
1443
+ val severity: ValidationSeverity
1444
+ )
1445
+
1446
+ enum class ValidationSeverity {
1447
+ ERROR, WARNING, INFO
1448
+ }
1449
+
1450
+ data class TestFileInfo(
1451
+ val name: String,
1452
+ val type: TestFileType,
1453
+ val path: String,
1454
+ val lastModified: Long
1455
+ )
1456
+
1457
+ enum class TestFileType {
1458
+ FEATURE, STEPS, PAGE_OBJECT
1459
+ }
1460
+
1461
+ // Run Configuration for MCP Tests
1462
+ class MCPTestConfiguration(
1463
+ project: Project,
1464
+ factory: ConfigurationFactory,
1465
+ name: String
1466
+ ) : RunConfigurationBase<Any>(project, factory, name) {
1467
+
1468
+ var testFile: String = ""
1469
+ var tags: String = ""
1470
+ var parallel: Boolean = false
1471
+
1472
+ override fun getConfigurationEditor() = MCPTestConfigurationEditor()
1473
+
1474
+ override fun getState(executor: com.intellij.execution.Executor, environment: com.intellij.execution.runners.ExecutionEnvironment) =
1475
+ MCPTestRunState(environment, this)
1476
+ }
1477
+
1478
+ class MCPTestConfigurationType : ConfigurationType {
1479
+ override fun getDisplayName() = "MCP Test"
1480
+ override fun getConfigurationTypeDescription() = "Run tests using Hakalab-MCP"
1481
+ override fun getIcon() = null // TODO: Add icon
1482
+ override fun getId() = "MCPTestConfiguration"
1483
+ override fun getConfigurationFactories() = arrayOf(MCPTestConfigurationFactory(this))
1484
+ }
1485
+
1486
+ class MCPTestConfigurationFactory(type: ConfigurationType) : ConfigurationFactory(type) {
1487
+ override fun createTemplateConfiguration(project: Project): RunConfiguration {
1488
+ return MCPTestConfiguration(project, this, "MCP Test")
1489
+ }
1490
+
1491
+ override fun getId() = "MCPTestConfigurationFactory"
1492
+ }
1493
+ ```
1494
+
1495
+ ---
1496
+
1497
+ Esta guía técnica proporciona todos los detalles necesarios para integrar Hakalab-MCP con plugins de IntelliJ IDEA y PyCharm, incluyendo ejemplos completos de código, configuraciones, y patrones de implementación para que la IA pueda utilizar efectivamente todas las capacidades del MCP.