Skip to content

Commit cb92bcd

Browse files
committed
add test
1 parent c7f46e2 commit cb92bcd

File tree

1 file changed

+359
-1
lines changed

1 file changed

+359
-1
lines changed
Lines changed: 359 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,386 @@
11
package io.micronaut.mcp.server.context;
22

3+
import io.micronaut.context.annotation.Factory;
4+
import io.micronaut.runtime.server.EmbeddedServer;
5+
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
6+
import io.modelcontextprotocol.spec.McpSchema;
7+
import jakarta.inject.Inject;
8+
import jakarta.inject.Named;
9+
import org.json.JSONException;
10+
import io.micronaut.http.client.BlockingHttpClient;
11+
import io.micronaut.http.client.HttpClient;
12+
import io.micronaut.http.client.annotation.Client;
313
import io.micronaut.context.annotation.Property;
414
import io.micronaut.context.annotation.Requires;
515
import io.micronaut.core.async.publisher.Publishers;
616
import io.micronaut.http.HttpRequest;
17+
import io.micronaut.http.HttpResponse;
18+
import io.micronaut.http.HttpStatus;
719
import io.micronaut.security.authentication.Authentication;
820
import io.micronaut.security.filters.AuthenticationFetcher;
921
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
1022
import jakarta.inject.Singleton;
23+
import org.junit.jupiter.api.Test;
1124
import org.reactivestreams.Publisher;
25+
import org.skyscreamer.jsonassert.JSONAssert;
26+
27+
import java.util.List;
28+
29+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
30+
import static org.junit.jupiter.api.Assertions.assertEquals;
1231

1332
@Property(name = "micronaut.mcp.server.info.name", value = "mcp-server")
1433
@Property(name = "micronaut.mcp.server.info.version", value = "0.0.1")
1534
@Property(name = "micronaut.mcp.server.transport", value = "HTTP")
1635
@Property(name = "spec.name", value = "MicronautMcpTransportContextTest")
36+
@Property(name = "micronaut.server.locale-resolution.fixed", value = "es_ES")
1737
@MicronautTest
1838
class MicronautMcpTransportContextTest {
1939

40+
@Inject
41+
EmbeddedServer embeddedServer;
42+
43+
@Test
44+
void lastEventIdInContext(@Client("/") HttpClient httpClient) throws JSONException {
45+
BlockingHttpClient client = httpClient.toBlocking();
46+
HttpRequest<?> req = HttpRequest.POST("/mcp", """
47+
{
48+
"method": "tools/call",
49+
"params": {
50+
"name": "lastEventId",
51+
"arguments": {}
52+
},
53+
"jsonrpc": "2.0",
54+
"id": 20
55+
}""").header("Last-Event-ID", "4578");
56+
HttpResponse<String> response = assertDoesNotThrow(() -> client.exchange(req, String.class));
57+
assertEquals(HttpStatus.OK, response.getStatus());
58+
String responseJson = response.body();
59+
String expected = String.format("""
60+
61+
{
62+
"jsonrpc": "2.0",
63+
"id": 20,
64+
"result": {
65+
"content": [
66+
{
67+
"type": "text",
68+
"text": "4578"
69+
}
70+
],
71+
"isError": false
72+
}
73+
}""", embeddedServer.getPort());
74+
JSONAssert.assertEquals(expected, responseJson, true);
75+
}
76+
77+
@Test
78+
void sessionIdInContext(@Client("/") HttpClient httpClient) throws JSONException {
79+
BlockingHttpClient client = httpClient.toBlocking();
80+
HttpRequest<?> req = HttpRequest.POST("/mcp", """
81+
{
82+
"method": "tools/call",
83+
"params": {
84+
"name": "sessionId",
85+
"arguments": {}
86+
},
87+
"jsonrpc": "2.0",
88+
"id": 20
89+
}""").header("Mcp-Session-Id", "123456789");
90+
HttpResponse<String> response = assertDoesNotThrow(() -> client.exchange(req, String.class));
91+
assertEquals(HttpStatus.OK, response.getStatus());
92+
String responseJson = response.body();
93+
String expected = String.format("""
94+
95+
{
96+
"jsonrpc": "2.0",
97+
"id": 20,
98+
"result": {
99+
"content": [
100+
{
101+
"type": "text",
102+
"text": "123456789"
103+
}
104+
],
105+
"isError": false
106+
}
107+
}""", embeddedServer.getPort());
108+
JSONAssert.assertEquals(expected, responseJson, true);
109+
}
110+
111+
@Test
112+
void protocolVersionInContext(@Client("/") HttpClient httpClient) throws JSONException {
113+
BlockingHttpClient client = httpClient.toBlocking();
114+
HttpRequest<?> req = HttpRequest.POST("/mcp", """
115+
{
116+
"method": "tools/call",
117+
"params": {
118+
"name": "protocolVersion",
119+
"arguments": {}
120+
},
121+
"jsonrpc": "2.0",
122+
"id": 20
123+
}""").header("MCP-Protocol-Version", "2025-06-18");
124+
HttpResponse<String> response = assertDoesNotThrow(() -> client.exchange(req, String.class));
125+
assertEquals(HttpStatus.OK, response.getStatus());
126+
String responseJson = response.body();
127+
String expected = String.format("""
128+
129+
{
130+
"jsonrpc": "2.0",
131+
"id": 20,
132+
"result": {
133+
"content": [
134+
{
135+
"type": "text",
136+
"text": "2025-06-18"
137+
}
138+
],
139+
"isError": false
140+
}
141+
}""", embeddedServer.getPort());
142+
JSONAssert.assertEquals(expected, responseJson, true);
143+
}
144+
145+
@Test
146+
void hostTool(@Client("/") HttpClient httpClient) throws JSONException {
147+
BlockingHttpClient client = httpClient.toBlocking();
148+
HttpRequest<?> req = HttpRequest.POST("/mcp", """
149+
{
150+
"method": "tools/call",
151+
"params": {
152+
"name": "host",
153+
"arguments": {}
154+
},
155+
"jsonrpc": "2.0",
156+
"id": 20
157+
}""");
158+
HttpResponse<String> response = assertDoesNotThrow(() -> client.exchange(req, String.class));
159+
assertEquals(HttpStatus.OK, response.getStatus());
160+
String responseJson = response.body();
161+
String expected = String.format("""
162+
163+
{
164+
"jsonrpc": "2.0",
165+
"id": 20,
166+
"result": {
167+
"content": [
168+
{
169+
"type": "text",
170+
"text": "http://localhost:%s"
171+
}
172+
],
173+
"isError": false
174+
}
175+
}""", embeddedServer.getPort());
176+
JSONAssert.assertEquals(expected, responseJson, true);
177+
}
178+
179+
@Test
180+
void localeTool(@Client("/") HttpClient httpClient) throws JSONException {
181+
BlockingHttpClient client = httpClient.toBlocking();
182+
HttpRequest<?> req = HttpRequest.POST("/mcp", """
183+
{
184+
"method": "tools/call",
185+
"params": {
186+
"name": "locale",
187+
"arguments": {}
188+
},
189+
"jsonrpc": "2.0",
190+
"id": 20
191+
}""");
192+
HttpResponse<String> response = assertDoesNotThrow(() -> client.exchange(req, String.class));
193+
assertEquals(HttpStatus.OK, response.getStatus());
194+
String responseJson = response.body();
195+
String expected = """
196+
197+
{
198+
"jsonrpc": "2.0",
199+
"id": 20,
200+
"result": {
201+
"content": [
202+
{
203+
"type": "text",
204+
"text": "es-ES"
205+
}
206+
],
207+
"isError": false
208+
}
209+
}""";
210+
JSONAssert.assertEquals(expected, responseJson, true);
211+
}
212+
213+
@Test
214+
void userTool(@Client("/") HttpClient httpClient) throws JSONException {
215+
BlockingHttpClient client = httpClient.toBlocking();
216+
HttpRequest<?> req = HttpRequest.POST("/mcp", """
217+
{
218+
"method": "tools/call",
219+
"params": {
220+
"name": "user",
221+
"arguments": {}
222+
},
223+
"jsonrpc": "2.0",
224+
"id": 20
225+
}""");
226+
HttpResponse<String> response = assertDoesNotThrow(() -> client.exchange(req, String.class));
227+
assertEquals(HttpStatus.OK, response.getStatus());
228+
String responseJson = response.body();
229+
String expected = """
230+
231+
{
232+
"jsonrpc": "2.0",
233+
"id": 20,
234+
"result": {
235+
"content": [
236+
{
237+
"type": "text",
238+
"text": "user: sdelamo role: [ROLE_USER]"
239+
}
240+
],
241+
"isError": false
242+
}
243+
}""";
244+
JSONAssert.assertEquals(expected, responseJson, true);
245+
}
246+
247+
248+
@Requires(property = "spec.name", value = "MicronautMcpTransportContextTest")
249+
@Factory
250+
static class ToolsFactory {
251+
@Singleton
252+
McpStatelessServerFeatures.SyncToolSpecification hostTool() {
253+
return McpStatelessServerFeatures.SyncToolSpecification.builder()
254+
.tool(McpSchema.Tool.builder()
255+
.name("host")
256+
.build())
257+
.callHandler((exchange, req) -> {
258+
if (exchange instanceof MicronautMcpTransportContext context) {
259+
return McpSchema.CallToolResult.builder()
260+
.addTextContent(context.host())
261+
.build();
262+
} else {
263+
return McpSchema.CallToolResult.builder()
264+
.isError(true)
265+
.build();
266+
}
267+
})
268+
.build();
269+
}
270+
271+
@Singleton
272+
McpStatelessServerFeatures.SyncToolSpecification localeTool() {
273+
return McpStatelessServerFeatures.SyncToolSpecification.builder()
274+
.tool(McpSchema.Tool.builder()
275+
.name("locale")
276+
.build())
277+
.callHandler((exchange, req) -> {
278+
if (exchange instanceof MicronautMcpTransportContext context) {
279+
return McpSchema.CallToolResult.builder()
280+
.addTextContent(context.locale().toLanguageTag())
281+
.build();
282+
} else {
283+
return McpSchema.CallToolResult.builder()
284+
.isError(true)
285+
.build();
286+
}
287+
})
288+
.build();
289+
}
290+
291+
@Singleton
292+
McpStatelessServerFeatures.SyncToolSpecification protocolVersionTool() {
293+
return McpStatelessServerFeatures.SyncToolSpecification.builder()
294+
.tool(McpSchema.Tool.builder()
295+
.name("protocolVersion")
296+
.build())
297+
.callHandler((exchange, req) -> {
298+
if (exchange instanceof MicronautMcpTransportContext context) {
299+
return McpSchema.CallToolResult.builder()
300+
.addTextContent(context.protocolVersion())
301+
.build();
302+
} else {
303+
return McpSchema.CallToolResult.builder()
304+
.isError(true)
305+
.build();
306+
}
307+
})
308+
.build();
309+
}
310+
311+
@Singleton
312+
McpStatelessServerFeatures.SyncToolSpecification lastEventIdTool() {
313+
return McpStatelessServerFeatures.SyncToolSpecification.builder()
314+
.tool(McpSchema.Tool.builder()
315+
.name("lastEventId")
316+
.build())
317+
.callHandler((exchange, req) -> {
318+
if (exchange instanceof MicronautMcpTransportContext context) {
319+
return McpSchema.CallToolResult.builder()
320+
.addTextContent(context.lastEventId())
321+
.build();
322+
} else {
323+
return McpSchema.CallToolResult.builder()
324+
.isError(true)
325+
.build();
326+
}
327+
})
328+
.build();
329+
}
330+
331+
@Singleton
332+
McpStatelessServerFeatures.SyncToolSpecification sessionIdTool() {
333+
return McpStatelessServerFeatures.SyncToolSpecification.builder()
334+
.tool(McpSchema.Tool.builder()
335+
.name("sessionId")
336+
.build())
337+
.callHandler((exchange, req) -> {
338+
if (exchange instanceof MicronautMcpTransportContext context) {
339+
return McpSchema.CallToolResult.builder()
340+
.addTextContent(context.sessionId())
341+
.build();
342+
} else {
343+
return McpSchema.CallToolResult.builder()
344+
.isError(true)
345+
.build();
346+
}
347+
})
348+
.build();
349+
}
350+
351+
@Singleton
352+
McpStatelessServerFeatures.SyncToolSpecification userTool() {
353+
return McpStatelessServerFeatures.SyncToolSpecification.builder()
354+
.tool(McpSchema.Tool.builder()
355+
.name("user")
356+
.build())
357+
.callHandler((exchange, req) -> {
358+
if (exchange instanceof MicronautMcpTransportContext context) {
359+
if (context.principal() instanceof Authentication authentication) {
360+
return McpSchema.CallToolResult.builder()
361+
.addTextContent("user: " + authentication.getName() + " role: " + authentication.getRoles())
362+
.build();
363+
} else {
364+
return McpSchema.CallToolResult.builder()
365+
.addTextContent("user: " + context.principal().getName())
366+
.build();
367+
}
368+
} else {
369+
return McpSchema.CallToolResult.builder()
370+
.isError(true)
371+
.build();
372+
}
373+
})
374+
.build();
375+
}
376+
}
377+
20378
@Requires(property = "spec.name", value = "MicronautMcpTransportContextTest")
21379
@Singleton
22380
static class TestAuthenticationFetcher implements AuthenticationFetcher<HttpRequest<?>> {
23381
@Override
24382
public Publisher<Authentication> fetchAuthentication(HttpRequest<?> request) {
25-
return Publishers.just(Authentication.build("sdelamo"));
383+
return Publishers.just(Authentication.build("sdelamo", List.of("ROLE_USER")));
26384
}
27385
}
28386
}

0 commit comments

Comments
 (0)