Skip to content

Commit 8cd4c0a

Browse files
committed
- add more tests over mcp
1 parent a86ee88 commit 8cd4c0a

6 files changed

Lines changed: 210 additions & 15 deletions

File tree

modules/jooby-apt/src/main/java/io/jooby/internal/apt/McpRoute.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -542,14 +542,11 @@ public List<String> generateMcpHandlerMethod(boolean kt) {
542542
var isNullable = param.isNullable(kt);
543543
javaParamNames.add(javaName);
544544

545-
if (type.equals("io.jooby.Context")) {
546-
buffer.add(statement(indent(6), kt ? "val " : "var ", javaName, " = ctx", semicolon(kt)));
545+
if (type.equals("io.jooby.Context")
546+
|| type.equals("io.modelcontextprotocol.server.McpSyncServerExchange")) {
547547
continue;
548-
} else if (type.equals("io.modelcontextprotocol.server.McpSyncServerExchange")) {
549-
buffer.add(
550-
statement(indent(6), kt ? "val " : "var ", javaName, " = exchange", semicolon(kt)));
551-
continue;
552-
} else if (type.equals("io.modelcontextprotocol.common.McpTransportContext")) {
548+
}
549+
if (type.equals("io.modelcontextprotocol.common.McpTransportContext")) {
553550
if (kt) {
554551
buffer.add(
555552
statement(

tests/src/test/java/io/jooby/i3830/CalculatorTools.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
package io.jooby.i3830;
77

88
import java.util.List;
9+
import java.util.Optional;
910

1011
import io.jooby.annotation.McpCompletion;
1112
import io.jooby.annotation.McpPrompt;
1213
import io.jooby.annotation.McpResource;
1314
import io.jooby.annotation.McpTool;
15+
import io.modelcontextprotocol.server.McpSyncServerExchange;
1416

1517
/** A collection of tools, prompts, and resources exposed to the LLM via MCP. */
1618
public class CalculatorTools {
@@ -52,4 +54,11 @@ public List<String> historyCompletions(String user) {
5254
// In a real app, this would query a database for active usernames matching the input
5355
return List.of("alice", "bob", "charlie");
5456
}
57+
58+
@McpTool(
59+
name = "get_session_info",
60+
description = "Returns the current MCP session ID using the injected exchange.")
61+
public String getSessionInfo(McpSyncServerExchange exchange) {
62+
return Optional.ofNullable(exchange.sessionId()).orElse("No active session");
63+
}
5564
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.i3830;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
import static org.junit.jupiter.api.Assertions.assertNotNull;
11+
12+
import java.util.concurrent.atomic.AtomicReference;
13+
14+
import io.jooby.jackson3.Jackson3Module;
15+
import io.jooby.junit.ServerTest;
16+
import io.jooby.junit.ServerTestRunner;
17+
import io.jooby.mcp.McpModule;
18+
19+
public class McpExchangeInjectionTest {
20+
21+
@ServerTest
22+
public void shouldInjectExchangeAndAccessSession(ServerTestRunner runner) throws Exception {
23+
runner
24+
.define(
25+
app -> {
26+
app.install(new Jackson3Module());
27+
// Register the module using the STREAMABLE_HTTP transport
28+
app.install(
29+
new McpModule(new CalculatorToolsMcp_())
30+
.transport(McpModule.Transport.STREAMABLE_HTTP));
31+
})
32+
.ready(
33+
client -> {
34+
AtomicReference<String> sessionId = new AtomicReference<>();
35+
36+
// 1. STREAMABLE_HTTP requires a formal MCP initialize handshake via POST
37+
// to generate the session ID.
38+
String initRequest =
39+
"""
40+
{
41+
"jsonrpc": "2.0",
42+
"id": "init-1",
43+
"method": "initialize",
44+
"params": {
45+
"protocolVersion": "2024-11-05",
46+
"capabilities": {},
47+
"clientInfo": { "name": "test-client", "version": "1.0.0" }
48+
}
49+
}
50+
""";
51+
52+
// The transport provider strictly requires these Accept headers
53+
client.header("Accept", "text/event-stream, application/json");
54+
client.header("Content-Type", "application/json");
55+
56+
client.postJson(
57+
"/mcp",
58+
initRequest,
59+
response -> {
60+
assertEquals(200, response.code());
61+
62+
// 2. Extract the session ID from the headers
63+
String header = response.header("mcp-session-id");
64+
assertNotNull(
65+
header, "mcp-session-id header must be present in initialization response");
66+
sessionId.set(header);
67+
});
68+
69+
// 3. Construct the Tool request
70+
String toolRequest =
71+
"""
72+
{
73+
"jsonrpc": "2.0",
74+
"id": "tool-1",
75+
"method": "tools/call",
76+
"params": {
77+
"name": "get_session_info",
78+
"arguments": {}
79+
}
80+
}
81+
""";
82+
83+
// 4. Send the tool request, appending the session ID we just obtained
84+
client.header("Accept", "text/event-stream, application/json");
85+
client.header("Content-Type", "application/json");
86+
client.header("mcp-session-id", sessionId.get());
87+
88+
client.postJson(
89+
"/mcp",
90+
toolRequest,
91+
response -> {
92+
assertEquals(200, response.code());
93+
94+
var body = response.body().string();
95+
96+
assertThat(body)
97+
.contains("\"id\":\"tool-1\"")
98+
.as("The response should contain the calculated result");
99+
;
100+
});
101+
});
102+
}
103+
}

tests/src/test/java/io/jooby/i3830/McpTest.java

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.i3830;
7+
8+
import io.jooby.annotation.McpTool;
9+
10+
public class UserTools {
11+
12+
// The structured output schema we want the LLM to receive
13+
public record UserProfile(String username, String role, boolean active) {}
14+
15+
@McpTool(
16+
name = "get_user_profile",
17+
description = "Fetches a user profile as a structured JSON object")
18+
public UserProfile getUserProfile(String username) {
19+
// In a real app, this would query a database
20+
return new UserProfile(username, "admin", true);
21+
}
22+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.i3830;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
import io.jooby.jackson3.Jackson3Module;
11+
import io.jooby.junit.ServerTest;
12+
import io.jooby.junit.ServerTestRunner;
13+
import io.jooby.mcp.McpModule;
14+
15+
public class UserToolsTest {
16+
17+
@ServerTest
18+
public void shouldReturnStructuredJsonObject(ServerTestRunner runner) {
19+
runner
20+
.define(
21+
app -> {
22+
app.install(new Jackson3Module());
23+
// Register the tool using the stateless transport
24+
app.install(
25+
new McpModule(new UserToolsMcp_())
26+
.transport(McpModule.Transport.STATELESS_STREAMABLE_HTTP));
27+
})
28+
.ready(
29+
client -> {
30+
// 1. Ask for the structured user profile
31+
String jsonRpcRequest =
32+
"""
33+
{
34+
"jsonrpc": "2.0",
35+
"id": "req-user-1",
36+
"method": "tools/call",
37+
"params": {
38+
"name": "get_user_profile",
39+
"arguments": {
40+
"username": "edgar"
41+
}
42+
}
43+
}
44+
""";
45+
46+
client.header("Accept", "text/event-stream, application/json");
47+
client.header("Content-Type", "application/json");
48+
49+
client.postJson(
50+
"/mcp",
51+
jsonRpcRequest,
52+
response -> {
53+
assertThat(response.code()).isEqualTo(200);
54+
55+
String body = response.body().string();
56+
57+
// 2. Verify the response ID matches
58+
assertThat(body).containsPattern("\"id\":\\s*\"req-user-1\"");
59+
60+
// 3. Verify the Java Record was correctly serialized into the tool's text
61+
// content!
62+
assertThat(body)
63+
.contains("\"username\":\"edgar\"")
64+
.contains("\"role\":\"admin\"")
65+
.contains("\"active\":true")
66+
.as(
67+
"The output should be a fully structured JSON payload representing the"
68+
+ " Record");
69+
});
70+
});
71+
}
72+
}

0 commit comments

Comments
 (0)