Skip to content

Commit 161208b

Browse files
committed
add test
1 parent c7f46e2 commit 161208b

File tree

1 file changed

+358
-1
lines changed

1 file changed

+358
-1
lines changed
Lines changed: 358 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,385 @@
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 org.json.JSONException;
9+
import io.micronaut.http.client.BlockingHttpClient;
10+
import io.micronaut.http.client.HttpClient;
11+
import io.micronaut.http.client.annotation.Client;
312
import io.micronaut.context.annotation.Property;
413
import io.micronaut.context.annotation.Requires;
514
import io.micronaut.core.async.publisher.Publishers;
615
import io.micronaut.http.HttpRequest;
16+
import io.micronaut.http.HttpResponse;
17+
import io.micronaut.http.HttpStatus;
718
import io.micronaut.security.authentication.Authentication;
819
import io.micronaut.security.filters.AuthenticationFetcher;
920
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
1021
import jakarta.inject.Singleton;
22+
import org.junit.jupiter.api.Test;
1123
import org.reactivestreams.Publisher;
24+
import org.skyscreamer.jsonassert.JSONAssert;
25+
26+
import java.util.List;
27+
28+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
29+
import static org.junit.jupiter.api.Assertions.assertEquals;
1230

1331
@Property(name = "micronaut.mcp.server.info.name", value = "mcp-server")
1432
@Property(name = "micronaut.mcp.server.info.version", value = "0.0.1")
1533
@Property(name = "micronaut.mcp.server.transport", value = "HTTP")
1634
@Property(name = "spec.name", value = "MicronautMcpTransportContextTest")
35+
@Property(name = "micronaut.server.locale-resolution.fixed", value = "es_ES")
1736
@MicronautTest
1837
class MicronautMcpTransportContextTest {
1938

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

0 commit comments

Comments
 (0)