From 03cfc52c8c785babdd4b0733c69d683158fe73f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:06:04 +0000 Subject: [PATCH 1/4] Initial plan From d8692ffeb109525818b567b045e196bd7e4bd85c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:21:56 +0000 Subject: [PATCH 2/4] Add MCP server integration tests Co-authored-by: Godzilla675 <131464726+Godzilla675@users.noreply.github.com> --- .../integration/McpServerIntegrationTest.kt | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 app/src/test/java/com/dark/tool_neuron/integration/McpServerIntegrationTest.kt diff --git a/app/src/test/java/com/dark/tool_neuron/integration/McpServerIntegrationTest.kt b/app/src/test/java/com/dark/tool_neuron/integration/McpServerIntegrationTest.kt new file mode 100644 index 00000000..aaf0550e --- /dev/null +++ b/app/src/test/java/com/dark/tool_neuron/integration/McpServerIntegrationTest.kt @@ -0,0 +1,218 @@ +package com.dark.tool_neuron.integration + +import com.dark.tool_neuron.models.table_schema.McpServer +import com.dark.tool_neuron.models.table_schema.McpTransportType +import com.dark.tool_neuron.service.McpToolInfo +import com.dark.tool_neuron.service.McpToolMapper +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Assert.* +import org.junit.Test + +/** + * Integration tests for MCP server functionality. + * These tests validate that the MCP client can properly connect to and interact with + * remote MCP servers like Zapier. + */ +class McpServerIntegrationTest { + + /** + * Test that McpServer can be created with the correct configuration + * for connecting to Zapier's MCP endpoint. + */ + @Test + fun createZapierMcpServerConfiguration() { + val zapierUrl = "https://mcp.zapier.com/api/v1/connect?token=example-token" + + val server = McpServer( + id = McpServer.generateId(), + name = "Zapier MCP", + url = zapierUrl, + transportType = McpTransportType.SSE, + apiKey = null, // Token is in URL + description = "Zapier MCP integration for Google Docs tools" + ) + + assertNotNull(server.id) + assertEquals("Zapier MCP", server.name) + assertEquals(zapierUrl, server.url) + assertEquals(McpTransportType.SSE, server.transportType) + assertTrue(server.isEnabled) + } + + /** + * Test parsing of MCP initialize response in SSE format. + */ + @Test + fun parseMcpInitializeResponse() { + val sseResponse = """event: message +data: {"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"zapier","title":"Zapier MCP","version":"1.0.0"}},"jsonrpc":"2.0","id":1}""" + + // Extract JSON from SSE format + val dataLine = sseResponse.lines().find { it.startsWith("data:") } + assertNotNull(dataLine) + + val jsonStr = dataLine!!.removePrefix("data:").trim() + val json = JSONObject(jsonStr) + + assertEquals("2.0", json.getString("jsonrpc")) + assertEquals(1, json.getInt("id")) + + val result = json.getJSONObject("result") + assertEquals("2024-11-05", result.getString("protocolVersion")) + + val serverInfo = result.getJSONObject("serverInfo") + assertEquals("zapier", serverInfo.getString("name")) + assertEquals("1.0.0", serverInfo.getString("version")) + } + + /** + * Test parsing of MCP tools/list response. + */ + @Test + fun parseMcpToolsListResponse() { + val sseResponse = """event: message +data: {"result":{"tools":[{"name":"google_docs_create_document_from_text","description":"Create a new document from text.","inputSchema":{"type":"object","properties":{"title":{"type":"string"}},"required":[]}}]},"jsonrpc":"2.0","id":2}""" + + val dataLine = sseResponse.lines().find { it.startsWith("data:") } + assertNotNull(dataLine) + + val jsonStr = dataLine!!.removePrefix("data:").trim() + val json = JSONObject(jsonStr) + + val result = json.getJSONObject("result") + val tools = result.getJSONArray("tools") + + assertEquals(1, tools.length()) + + val tool = tools.getJSONObject(0) + assertEquals("google_docs_create_document_from_text", tool.getString("name")) + assertEquals("Create a new document from text.", tool.getString("description")) + + val inputSchema = tool.getJSONObject("inputSchema") + assertEquals("object", inputSchema.getString("type")) + } + + /** + * Test that McpToolMapper correctly maps Zapier tools to the LLM format. + */ + @Test + fun mapZapierToolsToLlmFormat() { + val server = McpServer( + id = "zapier-1", + name = "Zapier MCP", + url = "https://mcp.zapier.com/api/v1/connect", + transportType = McpTransportType.SSE + ) + + val tools = listOf( + McpToolInfo( + name = "google_docs_create_document_from_text", + description = "Create a new document from text. Also supports limited HTML.", + inputSchema = """{"type":"object","properties":{"title":{"type":"string","description":"Document Name"},"file":{"type":"string","description":"Document Content"}},"required":["instructions"]}""" + ), + McpToolInfo( + name = "google_docs_find_a_document", + description = "Search for a specific document by name.", + inputSchema = """{"type":"object","properties":{"title":{"type":"string","description":"Document Name"}},"required":["instructions"]}""" + ) + ) + + val mapping = McpToolMapper.buildMapping(mapOf(server to tools)) + + // Check that tools JSON is valid + val toolsArray = JSONArray(mapping.toolsJson) + assertEquals(2, toolsArray.length()) + + // Check first tool + val firstTool = toolsArray.getJSONObject(0) + assertEquals("function", firstTool.getString("type")) + + val function = firstTool.getJSONObject("function") + // Tool name should be sanitized: "zapier_mcp_google_docs_create_document_from_text" + assertTrue(function.getString("name").contains("google_docs_create_document_from_text")) + assertTrue(function.has("description")) + + // Check tool registry + assertEquals(2, mapping.toolRegistry.size) + + // Check that registry maps back to original tool names + val firstEntry = mapping.toolRegistry.values.first() + assertEquals(server, firstEntry.server) + assertTrue(firstEntry.toolName.startsWith("google_docs_")) + } + + /** + * Test that tool call request is properly formatted for MCP protocol. + */ + @Test + fun formatMcpToolCallRequest() { + val toolName = "google_docs_create_document_from_text" + val arguments = JSONObject().apply { + put("instructions", "Create a document titled 'Test' with content 'Hello World'") + put("output_hint", "just the document URL") + put("title", "Test Document") + put("file", "Hello World") + } + + val request = JSONObject().apply { + put("jsonrpc", "2.0") + put("id", System.currentTimeMillis()) + put("method", "tools/call") + put("params", JSONObject().apply { + put("name", toolName) + put("arguments", arguments) + }) + } + + assertEquals("2.0", request.getString("jsonrpc")) + assertEquals("tools/call", request.getString("method")) + + val params = request.getJSONObject("params") + assertEquals(toolName, params.getString("name")) + + val args = params.getJSONObject("arguments") + assertEquals("Test Document", args.getString("title")) + assertEquals("Hello World", args.getString("file")) + } + + /** + * Test SSE transport type is correctly identified for Zapier MCP. + */ + @Test + fun identifyZapierTransportType() { + // Zapier uses SSE transport + val zapierServer = McpServer( + id = "zapier", + name = "Zapier", + url = "https://mcp.zapier.com/api/v1/connect", + transportType = McpTransportType.SSE + ) + + assertEquals(McpTransportType.SSE, zapierServer.transportType) + + // Alternative: Streamable HTTP transport + val httpServer = McpServer( + id = "custom", + name = "Custom MCP", + url = "https://api.example.com/mcp", + transportType = McpTransportType.STREAMABLE_HTTP + ) + + assertEquals(McpTransportType.STREAMABLE_HTTP, httpServer.transportType) + } + + /** + * Test that server ID generation produces unique IDs. + */ + @Test + fun generateUniqueServerIds() { + val ids = mutableSetOf() + repeat(100) { + ids.add(McpServer.generateId()) + } + + // All 100 IDs should be unique + assertEquals(100, ids.size) + } +} From 8872229375c6c27d6cee0df59169b4ab68835f65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:25:13 +0000 Subject: [PATCH 3/4] Fix MCP integration test input schemas to include required instructions field Co-authored-by: Godzilla675 <131464726+Godzilla675@users.noreply.github.com> --- .../dark/tool_neuron/integration/McpServerIntegrationTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/test/java/com/dark/tool_neuron/integration/McpServerIntegrationTest.kt b/app/src/test/java/com/dark/tool_neuron/integration/McpServerIntegrationTest.kt index aaf0550e..7d7bff99 100644 --- a/app/src/test/java/com/dark/tool_neuron/integration/McpServerIntegrationTest.kt +++ b/app/src/test/java/com/dark/tool_neuron/integration/McpServerIntegrationTest.kt @@ -109,12 +109,12 @@ data: {"result":{"tools":[{"name":"google_docs_create_document_from_text","descr McpToolInfo( name = "google_docs_create_document_from_text", description = "Create a new document from text. Also supports limited HTML.", - inputSchema = """{"type":"object","properties":{"title":{"type":"string","description":"Document Name"},"file":{"type":"string","description":"Document Content"}},"required":["instructions"]}""" + inputSchema = """{"type":"object","properties":{"instructions":{"type":"string","description":"Instructions for running this tool"},"title":{"type":"string","description":"Document Name"},"file":{"type":"string","description":"Document Content"}},"required":["instructions"]}""" ), McpToolInfo( name = "google_docs_find_a_document", description = "Search for a specific document by name.", - inputSchema = """{"type":"object","properties":{"title":{"type":"string","description":"Document Name"}},"required":["instructions"]}""" + inputSchema = """{"type":"object","properties":{"instructions":{"type":"string","description":"Instructions for running this tool"},"title":{"type":"string","description":"Document Name"}},"required":["instructions"]}""" ) ) From cdb24df2f75379c801c1c49775d8e99d163ccca9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 17 Jan 2026 20:16:43 +0000 Subject: [PATCH 4/4] Fix Streamable HTTP transport and improve MCP server tests - Fix parseResponse() to always try SSE parsing regardless of transport type - Rename test class from McpServerIntegrationTest to McpServerTest - Use exact assertions instead of permissive contains() checks - Add helper function documentation and UUID format validation - Reduce UUID test iterations from 100 to 10 for efficiency Co-authored-by: Godzilla675 <131464726+Godzilla675@users.noreply.github.com> --- .../tool_neuron/service/McpClientService.kt | 12 +-- ...verIntegrationTest.kt => McpServerTest.kt} | 99 +++++++++++-------- 2 files changed, 64 insertions(+), 47 deletions(-) rename app/src/test/java/com/dark/tool_neuron/integration/{McpServerIntegrationTest.kt => McpServerTest.kt} (70%) diff --git a/app/src/main/java/com/dark/tool_neuron/service/McpClientService.kt b/app/src/main/java/com/dark/tool_neuron/service/McpClientService.kt index 52564f98..6f319d5a 100644 --- a/app/src/main/java/com/dark/tool_neuron/service/McpClientService.kt +++ b/app/src/main/java/com/dark/tool_neuron/service/McpClientService.kt @@ -87,14 +87,14 @@ class McpClientService @Inject constructor() { } /** - * Parse response body, handling SSE format for SSE transport. - * For Streamable HTTP, returns the raw JSON body (no SSE envelope to parse). + * Parse response body, handling SSE format automatically. + * Some MCP servers return SSE-formatted responses regardless of the declared transport type, + * so we detect and parse SSE format for both transport types. */ private fun parseResponse(responseBody: String, transportType: McpTransportType): String { - return when (transportType) { - McpTransportType.SSE -> parseSseResponse(responseBody) - McpTransportType.STREAMABLE_HTTP -> responseBody // Already JSON, no SSE envelope to parse - } + // Always try to parse SSE format first, as some servers return SSE regardless of transport type + // The parseSseResponse function will return the original body if it's not SSE format + return parseSseResponse(responseBody) } /** diff --git a/app/src/test/java/com/dark/tool_neuron/integration/McpServerIntegrationTest.kt b/app/src/test/java/com/dark/tool_neuron/integration/McpServerTest.kt similarity index 70% rename from app/src/test/java/com/dark/tool_neuron/integration/McpServerIntegrationTest.kt rename to app/src/test/java/com/dark/tool_neuron/integration/McpServerTest.kt index 7d7bff99..63bb7cef 100644 --- a/app/src/test/java/com/dark/tool_neuron/integration/McpServerIntegrationTest.kt +++ b/app/src/test/java/com/dark/tool_neuron/integration/McpServerTest.kt @@ -10,11 +10,20 @@ import org.junit.Assert.* import org.junit.Test /** - * Integration tests for MCP server functionality. - * These tests validate that the MCP client can properly connect to and interact with - * remote MCP servers like Zapier. + * Unit tests for MCP server-related functionality. + * These tests validate McpToolMapper functionality, JSON parsing, + * and configuration objects without connecting to real MCP servers. */ -class McpServerIntegrationTest { +class McpServerTest { + + // Helper function to parse SSE response format. + // This is a simplified version for tests that extracts JSON from single-event SSE responses. + // The production code in McpClientService.parseSseResponse() handles multiple events and validates JSON. + private fun parseSseData(sseResponse: String): String { + val dataLine = sseResponse.lines().find { it.startsWith("data:") } + ?: return sseResponse + return dataLine.removePrefix("data:").trim() + } /** * Test that McpServer can be created with the correct configuration @@ -41,18 +50,15 @@ class McpServerIntegrationTest { } /** - * Test parsing of MCP initialize response in SSE format. + * Test parsing of MCP initialize response in SSE format using helper function. */ @Test fun parseMcpInitializeResponse() { val sseResponse = """event: message data: {"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"zapier","title":"Zapier MCP","version":"1.0.0"}},"jsonrpc":"2.0","id":1}""" - // Extract JSON from SSE format - val dataLine = sseResponse.lines().find { it.startsWith("data:") } - assertNotNull(dataLine) - - val jsonStr = dataLine!!.removePrefix("data:").trim() + // Use helper function to extract JSON from SSE format + val jsonStr = parseSseData(sseResponse) val json = JSONObject(jsonStr) assertEquals("2.0", json.getString("jsonrpc")) @@ -74,10 +80,8 @@ data: {"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{"listCh val sseResponse = """event: message data: {"result":{"tools":[{"name":"google_docs_create_document_from_text","description":"Create a new document from text.","inputSchema":{"type":"object","properties":{"title":{"type":"string"}},"required":[]}}]},"jsonrpc":"2.0","id":2}""" - val dataLine = sseResponse.lines().find { it.startsWith("data:") } - assertNotNull(dataLine) - - val jsonStr = dataLine!!.removePrefix("data:").trim() + // Use helper function to extract JSON from SSE format + val jsonStr = parseSseData(sseResponse) val json = JSONObject(jsonStr) val result = json.getJSONObject("result") @@ -124,22 +128,29 @@ data: {"result":{"tools":[{"name":"google_docs_create_document_from_text","descr val toolsArray = JSONArray(mapping.toolsJson) assertEquals(2, toolsArray.length()) - // Check first tool + // Check first tool structure val firstTool = toolsArray.getJSONObject(0) assertEquals("function", firstTool.getString("type")) val function = firstTool.getJSONObject("function") - // Tool name should be sanitized: "zapier_mcp_google_docs_create_document_from_text" - assertTrue(function.getString("name").contains("google_docs_create_document_from_text")) + // Verify exact tool name format: "zapier_mcp_google_docs_create_document_from_text" + assertEquals("zapier_mcp_google_docs_create_document_from_text", function.getString("name")) assertTrue(function.has("description")) - // Check tool registry + // Check tool registry size and contents assertEquals(2, mapping.toolRegistry.size) - // Check that registry maps back to original tool names - val firstEntry = mapping.toolRegistry.values.first() - assertEquals(server, firstEntry.server) - assertTrue(firstEntry.toolName.startsWith("google_docs_")) + // Verify exact tool name mapping in registry + val toolNames = mapping.toolRegistry.values.map { it.toolName }.toSet() + assertEquals( + setOf("google_docs_create_document_from_text", "google_docs_find_a_document"), + toolNames + ) + + // Verify all entries reference the same server + mapping.toolRegistry.values.forEach { entry -> + assertEquals(server, entry.server) + } } /** @@ -155,9 +166,10 @@ data: {"result":{"tools":[{"name":"google_docs_create_document_from_text","descr put("file", "Hello World") } + // Use fixed ID for deterministic test behavior val request = JSONObject().apply { put("jsonrpc", "2.0") - put("id", System.currentTimeMillis()) + put("id", 123L) put("method", "tools/call") put("params", JSONObject().apply { put("name", toolName) @@ -166,6 +178,7 @@ data: {"result":{"tools":[{"name":"google_docs_create_document_from_text","descr } assertEquals("2.0", request.getString("jsonrpc")) + assertEquals(123L, request.getLong("id")) assertEquals("tools/call", request.getString("method")) val params = request.getJSONObject("params") @@ -177,42 +190,46 @@ data: {"result":{"tools":[{"name":"google_docs_create_document_from_text","descr } /** - * Test SSE transport type is correctly identified for Zapier MCP. + * Test that both transport types can be assigned to McpServer. */ @Test - fun identifyZapierTransportType() { - // Zapier uses SSE transport - val zapierServer = McpServer( - id = "zapier", - name = "Zapier", - url = "https://mcp.zapier.com/api/v1/connect", + fun verifyTransportTypeAssignment() { + // SSE transport type + val sseServer = McpServer( + id = "server-sse", + name = "SSE Server", + url = "https://mcp.example.com/sse", transportType = McpTransportType.SSE ) + assertEquals(McpTransportType.SSE, sseServer.transportType) - assertEquals(McpTransportType.SSE, zapierServer.transportType) - - // Alternative: Streamable HTTP transport + // Streamable HTTP transport type val httpServer = McpServer( - id = "custom", - name = "Custom MCP", - url = "https://api.example.com/mcp", + id = "server-http", + name = "HTTP Server", + url = "https://mcp.example.com/http", transportType = McpTransportType.STREAMABLE_HTTP ) - assertEquals(McpTransportType.STREAMABLE_HTTP, httpServer.transportType) } /** - * Test that server ID generation produces unique IDs. + * Test that server ID generation produces unique UUIDs. */ @Test fun generateUniqueServerIds() { val ids = mutableSetOf() - repeat(100) { + // Generate 10 IDs to demonstrate uniqueness with reasonable confidence + repeat(10) { ids.add(McpServer.generateId()) } - // All 100 IDs should be unique - assertEquals(100, ids.size) + // All 10 IDs should be unique + assertEquals(10, ids.size) + + // Verify IDs are valid UUID format (lowercase hexadecimal) + ids.forEach { id -> + assertTrue("ID should be a valid UUID format", id.matches(Regex("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"))) + } } }