Skip to content

Commit a87425a

Browse files
authored
Merge pull request #5 from Godzilla675/copilot/connect-ai-model-to-mcp-server
Add MCP server integration tests
2 parents 524d6eb + cdb24df commit a87425a

2 files changed

Lines changed: 241 additions & 6 deletions

File tree

app/src/main/java/com/dark/tool_neuron/service/McpClientService.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,14 @@ class McpClientService @Inject constructor() {
8787
}
8888

8989
/**
90-
* Parse response body, handling SSE format for SSE transport.
91-
* For Streamable HTTP, returns the raw JSON body (no SSE envelope to parse).
90+
* Parse response body, handling SSE format automatically.
91+
* Some MCP servers return SSE-formatted responses regardless of the declared transport type,
92+
* so we detect and parse SSE format for both transport types.
9293
*/
9394
private fun parseResponse(responseBody: String, transportType: McpTransportType): String {
94-
return when (transportType) {
95-
McpTransportType.SSE -> parseSseResponse(responseBody)
96-
McpTransportType.STREAMABLE_HTTP -> responseBody // Already JSON, no SSE envelope to parse
97-
}
95+
// Always try to parse SSE format first, as some servers return SSE regardless of transport type
96+
// The parseSseResponse function will return the original body if it's not SSE format
97+
return parseSseResponse(responseBody)
9898
}
9999

100100
/**
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
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

Comments
 (0)