|
| 1 | +package com.dark.tool_neuron.integration |
| 2 | + |
| 3 | +import com.dark.tool_neuron.models.table_schema.McpServer |
| 4 | +import com.dark.tool_neuron.models.table_schema.McpTransportType |
| 5 | +import com.dark.tool_neuron.service.McpToolInfo |
| 6 | +import com.dark.tool_neuron.service.McpToolMapper |
| 7 | +import org.json.JSONArray |
| 8 | +import org.json.JSONObject |
| 9 | +import org.junit.Assert.* |
| 10 | +import org.junit.Test |
| 11 | + |
| 12 | +/** |
| 13 | + * Unit tests for MCP server-related functionality. |
| 14 | + * These tests validate McpToolMapper functionality, JSON parsing, |
| 15 | + * and configuration objects without connecting to real MCP servers. |
| 16 | + */ |
| 17 | +class McpServerTest { |
| 18 | + |
| 19 | + // Helper function to parse SSE response format. |
| 20 | + // This is a simplified version for tests that extracts JSON from single-event SSE responses. |
| 21 | + // The production code in McpClientService.parseSseResponse() handles multiple events and validates JSON. |
| 22 | + private fun parseSseData(sseResponse: String): String { |
| 23 | + val dataLine = sseResponse.lines().find { it.startsWith("data:") } |
| 24 | + ?: return sseResponse |
| 25 | + return dataLine.removePrefix("data:").trim() |
| 26 | + } |
| 27 | + |
| 28 | + /** |
| 29 | + * Test that McpServer can be created with the correct configuration |
| 30 | + * for connecting to Zapier's MCP endpoint. |
| 31 | + */ |
| 32 | + @Test |
| 33 | + fun createZapierMcpServerConfiguration() { |
| 34 | + val zapierUrl = "https://mcp.zapier.com/api/v1/connect?token=example-token" |
| 35 | + |
| 36 | + val server = McpServer( |
| 37 | + id = McpServer.generateId(), |
| 38 | + name = "Zapier MCP", |
| 39 | + url = zapierUrl, |
| 40 | + transportType = McpTransportType.SSE, |
| 41 | + apiKey = null, // Token is in URL |
| 42 | + description = "Zapier MCP integration for Google Docs tools" |
| 43 | + ) |
| 44 | + |
| 45 | + assertNotNull(server.id) |
| 46 | + assertEquals("Zapier MCP", server.name) |
| 47 | + assertEquals(zapierUrl, server.url) |
| 48 | + assertEquals(McpTransportType.SSE, server.transportType) |
| 49 | + assertTrue(server.isEnabled) |
| 50 | + } |
| 51 | + |
| 52 | + /** |
| 53 | + * Test parsing of MCP initialize response in SSE format using helper function. |
| 54 | + */ |
| 55 | + @Test |
| 56 | + fun parseMcpInitializeResponse() { |
| 57 | + val sseResponse = """event: message |
| 58 | +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}""" |
| 59 | + |
| 60 | + // Use helper function to extract JSON from SSE format |
| 61 | + val jsonStr = parseSseData(sseResponse) |
| 62 | + val json = JSONObject(jsonStr) |
| 63 | + |
| 64 | + assertEquals("2.0", json.getString("jsonrpc")) |
| 65 | + assertEquals(1, json.getInt("id")) |
| 66 | + |
| 67 | + val result = json.getJSONObject("result") |
| 68 | + assertEquals("2024-11-05", result.getString("protocolVersion")) |
| 69 | + |
| 70 | + val serverInfo = result.getJSONObject("serverInfo") |
| 71 | + assertEquals("zapier", serverInfo.getString("name")) |
| 72 | + assertEquals("1.0.0", serverInfo.getString("version")) |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * Test parsing of MCP tools/list response. |
| 77 | + */ |
| 78 | + @Test |
| 79 | + fun parseMcpToolsListResponse() { |
| 80 | + val sseResponse = """event: message |
| 81 | +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}""" |
| 82 | + |
| 83 | + // Use helper function to extract JSON from SSE format |
| 84 | + val jsonStr = parseSseData(sseResponse) |
| 85 | + val json = JSONObject(jsonStr) |
| 86 | + |
| 87 | + val result = json.getJSONObject("result") |
| 88 | + val tools = result.getJSONArray("tools") |
| 89 | + |
| 90 | + assertEquals(1, tools.length()) |
| 91 | + |
| 92 | + val tool = tools.getJSONObject(0) |
| 93 | + assertEquals("google_docs_create_document_from_text", tool.getString("name")) |
| 94 | + assertEquals("Create a new document from text.", tool.getString("description")) |
| 95 | + |
| 96 | + val inputSchema = tool.getJSONObject("inputSchema") |
| 97 | + assertEquals("object", inputSchema.getString("type")) |
| 98 | + } |
| 99 | + |
| 100 | + /** |
| 101 | + * Test that McpToolMapper correctly maps Zapier tools to the LLM format. |
| 102 | + */ |
| 103 | + @Test |
| 104 | + fun mapZapierToolsToLlmFormat() { |
| 105 | + val server = McpServer( |
| 106 | + id = "zapier-1", |
| 107 | + name = "Zapier MCP", |
| 108 | + url = "https://mcp.zapier.com/api/v1/connect", |
| 109 | + transportType = McpTransportType.SSE |
| 110 | + ) |
| 111 | + |
| 112 | + val tools = listOf( |
| 113 | + McpToolInfo( |
| 114 | + name = "google_docs_create_document_from_text", |
| 115 | + description = "Create a new document from text. Also supports limited HTML.", |
| 116 | + 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"]}""" |
| 117 | + ), |
| 118 | + McpToolInfo( |
| 119 | + name = "google_docs_find_a_document", |
| 120 | + description = "Search for a specific document by name.", |
| 121 | + inputSchema = """{"type":"object","properties":{"instructions":{"type":"string","description":"Instructions for running this tool"},"title":{"type":"string","description":"Document Name"}},"required":["instructions"]}""" |
| 122 | + ) |
| 123 | + ) |
| 124 | + |
| 125 | + val mapping = McpToolMapper.buildMapping(mapOf(server to tools)) |
| 126 | + |
| 127 | + // Check that tools JSON is valid |
| 128 | + val toolsArray = JSONArray(mapping.toolsJson) |
| 129 | + assertEquals(2, toolsArray.length()) |
| 130 | + |
| 131 | + // Check first tool structure |
| 132 | + val firstTool = toolsArray.getJSONObject(0) |
| 133 | + assertEquals("function", firstTool.getString("type")) |
| 134 | + |
| 135 | + val function = firstTool.getJSONObject("function") |
| 136 | + // Verify exact tool name format: "zapier_mcp_google_docs_create_document_from_text" |
| 137 | + assertEquals("zapier_mcp_google_docs_create_document_from_text", function.getString("name")) |
| 138 | + assertTrue(function.has("description")) |
| 139 | + |
| 140 | + // Check tool registry size and contents |
| 141 | + assertEquals(2, mapping.toolRegistry.size) |
| 142 | + |
| 143 | + // Verify exact tool name mapping in registry |
| 144 | + val toolNames = mapping.toolRegistry.values.map { it.toolName }.toSet() |
| 145 | + assertEquals( |
| 146 | + setOf("google_docs_create_document_from_text", "google_docs_find_a_document"), |
| 147 | + toolNames |
| 148 | + ) |
| 149 | + |
| 150 | + // Verify all entries reference the same server |
| 151 | + mapping.toolRegistry.values.forEach { entry -> |
| 152 | + assertEquals(server, entry.server) |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + /** |
| 157 | + * Test that tool call request is properly formatted for MCP protocol. |
| 158 | + */ |
| 159 | + @Test |
| 160 | + fun formatMcpToolCallRequest() { |
| 161 | + val toolName = "google_docs_create_document_from_text" |
| 162 | + val arguments = JSONObject().apply { |
| 163 | + put("instructions", "Create a document titled 'Test' with content 'Hello World'") |
| 164 | + put("output_hint", "just the document URL") |
| 165 | + put("title", "Test Document") |
| 166 | + put("file", "Hello World") |
| 167 | + } |
| 168 | + |
| 169 | + // Use fixed ID for deterministic test behavior |
| 170 | + val request = JSONObject().apply { |
| 171 | + put("jsonrpc", "2.0") |
| 172 | + put("id", 123L) |
| 173 | + put("method", "tools/call") |
| 174 | + put("params", JSONObject().apply { |
| 175 | + put("name", toolName) |
| 176 | + put("arguments", arguments) |
| 177 | + }) |
| 178 | + } |
| 179 | + |
| 180 | + assertEquals("2.0", request.getString("jsonrpc")) |
| 181 | + assertEquals(123L, request.getLong("id")) |
| 182 | + assertEquals("tools/call", request.getString("method")) |
| 183 | + |
| 184 | + val params = request.getJSONObject("params") |
| 185 | + assertEquals(toolName, params.getString("name")) |
| 186 | + |
| 187 | + val args = params.getJSONObject("arguments") |
| 188 | + assertEquals("Test Document", args.getString("title")) |
| 189 | + assertEquals("Hello World", args.getString("file")) |
| 190 | + } |
| 191 | + |
| 192 | + /** |
| 193 | + * Test that both transport types can be assigned to McpServer. |
| 194 | + */ |
| 195 | + @Test |
| 196 | + fun verifyTransportTypeAssignment() { |
| 197 | + // SSE transport type |
| 198 | + val sseServer = McpServer( |
| 199 | + id = "server-sse", |
| 200 | + name = "SSE Server", |
| 201 | + url = "https://mcp.example.com/sse", |
| 202 | + transportType = McpTransportType.SSE |
| 203 | + ) |
| 204 | + assertEquals(McpTransportType.SSE, sseServer.transportType) |
| 205 | + |
| 206 | + // Streamable HTTP transport type |
| 207 | + val httpServer = McpServer( |
| 208 | + id = "server-http", |
| 209 | + name = "HTTP Server", |
| 210 | + url = "https://mcp.example.com/http", |
| 211 | + transportType = McpTransportType.STREAMABLE_HTTP |
| 212 | + ) |
| 213 | + assertEquals(McpTransportType.STREAMABLE_HTTP, httpServer.transportType) |
| 214 | + } |
| 215 | + |
| 216 | + /** |
| 217 | + * Test that server ID generation produces unique UUIDs. |
| 218 | + */ |
| 219 | + @Test |
| 220 | + fun generateUniqueServerIds() { |
| 221 | + val ids = mutableSetOf<String>() |
| 222 | + // Generate 10 IDs to demonstrate uniqueness with reasonable confidence |
| 223 | + repeat(10) { |
| 224 | + ids.add(McpServer.generateId()) |
| 225 | + } |
| 226 | + |
| 227 | + // All 10 IDs should be unique |
| 228 | + assertEquals(10, ids.size) |
| 229 | + |
| 230 | + // Verify IDs are valid UUID format (lowercase hexadecimal) |
| 231 | + ids.forEach { id -> |
| 232 | + 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}"))) |
| 233 | + } |
| 234 | + } |
| 235 | +} |
0 commit comments