Skip to content

Commit dcc65d3

Browse files
committed
mcp: bind response to mcp responses
1 parent 6c9e9e5 commit dcc65d3

3 files changed

Lines changed: 207 additions & 33 deletions

File tree

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

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -169,24 +169,7 @@ public List<String> generateMcpDefinitionMethod(boolean kt) {
169169
}
170170

171171
String returnTypeStr = getReturnType().getRawType().toString();
172-
boolean isPrimitive =
173-
returnTypeStr.equals("int")
174-
|| returnTypeStr.equals("long")
175-
|| returnTypeStr.equals("double")
176-
|| returnTypeStr.equals("float")
177-
|| returnTypeStr.equals("boolean")
178-
|| returnTypeStr.equals("byte")
179-
|| returnTypeStr.equals("short")
180-
|| returnTypeStr.equals("char");
181-
boolean isLangClass = returnTypeStr.startsWith("java.lang.");
182-
boolean isMcpClass = returnTypeStr.startsWith("io.modelcontextprotocol.spec.McpSchema");
183-
184-
boolean generateOutputSchema =
185-
!returnType.isVoid()
186-
&& !getReturnType().is("io.jooby.StatusCode")
187-
&& !isPrimitive
188-
&& !isLangClass
189-
&& !isMcpClass;
172+
boolean generateOutputSchema = hasOutputSchema();
190173
String outputSchemaArg = "null";
191174

192175
if (generateOutputSchema) {
@@ -644,39 +627,89 @@ public List<String> generateMcpHandlerMethod(boolean kt) {
644627

645628
var methodCall = "c." + getMethodName() + "(" + String.join(", ", javaParamNames) + ")";
646629

630+
// Prefix for Resources: "req.uri(), "
631+
String toMethodPrefix = (isMcpResource() || isMcpResourceTemplate()) ? "req.uri(), " : "";
632+
633+
// Suffix for Tools: ", true" or ", false"
634+
String toMethodSuffix = isMcpTool() ? ", " + hasOutputSchema() : "";
635+
647636
if (getReturnType().isVoid()) {
648637
buffer.add(statement(indent(6), methodCall, semicolon(kt)));
649638
if (kt) {
650639
buffer.add(
651-
statement(indent(6), "return io.jooby.mcp.McpResult(this.json).", toMethod, "(null)"));
640+
statement(
641+
indent(6),
642+
"return io.jooby.mcp.McpResult(this.json).",
643+
toMethod,
644+
"(",
645+
toMethodPrefix,
646+
"null",
647+
toMethodSuffix,
648+
")"));
652649
} else {
653650
buffer.add(
654651
statement(
655652
indent(6),
656653
"return new io.jooby.mcp.McpResult(this.json).",
657654
toMethod,
658-
"(null)",
655+
"(",
656+
toMethodPrefix,
657+
"null",
658+
toMethodSuffix,
659+
")",
659660
semicolon(kt)));
660661
}
661662
} else {
662663
if (kt) {
663664
buffer.add(statement(indent(6), "val result = ", methodCall));
664665
buffer.add(
665666
statement(
666-
indent(6), "return io.jooby.mcp.McpResult(this.json).", toMethod, "(result)"));
667+
indent(6),
668+
"return io.jooby.mcp.McpResult(this.json).",
669+
toMethod,
670+
"(",
671+
toMethodPrefix,
672+
"result",
673+
toMethodSuffix,
674+
")"));
667675
} else {
668676
buffer.add(statement(indent(6), "var result = ", methodCall, semicolon(kt)));
669677
buffer.add(
670678
statement(
671679
indent(6),
672680
"return new io.jooby.mcp.McpResult(this.json).",
673681
toMethod,
674-
"(result)",
682+
"(",
683+
toMethodPrefix,
684+
"result",
685+
toMethodSuffix,
686+
")",
675687
semicolon(kt)));
676688
}
677689
}
678690
buffer.add(statement(indent(4), "}\n"));
679691

680692
return buffer;
681693
}
694+
695+
private boolean hasOutputSchema() {
696+
var returnTypeStr = getReturnType().getRawType().toString();
697+
var isPrimitive =
698+
returnTypeStr.equals("int")
699+
|| returnTypeStr.equals("long")
700+
|| returnTypeStr.equals("double")
701+
|| returnTypeStr.equals("float")
702+
|| returnTypeStr.equals("boolean")
703+
|| returnTypeStr.equals("byte")
704+
|| returnTypeStr.equals("short")
705+
|| returnTypeStr.equals("char");
706+
var isLangClass = returnTypeStr.startsWith("java.lang.");
707+
var isMcpClass = returnTypeStr.startsWith("io.modelcontextprotocol.spec.McpSchema");
708+
709+
return !getReturnType().isVoid()
710+
&& !getReturnType().is("io.jooby.StatusCode")
711+
&& !isPrimitive
712+
&& !isLangClass
713+
&& !isMcpClass;
714+
}
682715
}

modules/jooby-apt/src/test/java/tests/i3830/Issue3830.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ public void shouldGenerateMcpServer() throws Exception {
1717
new ProcessorRunner(new ExampleServer())
1818
.withMcpCode(
1919
source -> {
20-
System.out.println(source);
2120
assertThat(source)
2221
.isEqualToNormalizingWhitespace(
2322
"""
@@ -108,7 +107,7 @@ private io.modelcontextprotocol.spec.McpSchema.CallToolResult add(io.modelcontex
108107
if (raw_b == null) throw new IllegalArgumentException("Missing req param: b");
109108
var b = raw_b instanceof Number ? ((Number) raw_b).intValue() : Integer.parseInt(raw_b.toString());
110109
var result = c.add(a, b);
111-
return new io.jooby.mcp.McpResult(this.json).toCallToolResult(result);
110+
return new io.jooby.mcp.McpResult(this.json).toCallToolResult(result, false);
112111
}
113112
114113
private io.modelcontextprotocol.spec.McpSchema.Prompt reviewCodePromptSpec() {
@@ -139,7 +138,7 @@ private io.modelcontextprotocol.spec.McpSchema.ReadResourceResult getLogs(io.mod
139138
var args = java.util.Collections.<String, Object>emptyMap();
140139
var c = this.factory.apply(ctx);
141140
var result = c.getLogs();
142-
return new io.jooby.mcp.McpResult(this.json).toResourceResult(result);
141+
return new io.jooby.mcp.McpResult(this.json).toResourceResult(req.uri(), result);
143142
}
144143
145144
private io.modelcontextprotocol.spec.McpSchema.ResourceTemplate getUserProfileResourceTemplateSpec() {
@@ -156,7 +155,7 @@ private io.modelcontextprotocol.spec.McpSchema.ReadResourceResult getUserProfile
156155
var raw_id = args.get("id");
157156
var id = raw_id != null ? raw_id.toString() : null;
158157
var result = c.getUserProfile(id);
159-
return new io.jooby.mcp.McpResult(this.json).toResourceResult(result);
158+
return new io.jooby.mcp.McpResult(this.json).toResourceResult(req.uri(), result);
160159
}
161160
162161
private io.modelcontextprotocol.spec.McpSchema.CompleteResult getUserProfileCompletionHandler(io.modelcontextprotocol.server.McpSyncServerExchange exchange, io.modelcontextprotocol.spec.McpSchema.CompleteRequest req) {

modules/jooby-mcp/src/main/java/io/jooby/mcp/McpResult.java

Lines changed: 149 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,168 @@
55
*/
66
package io.jooby.mcp;
77

8+
import static io.modelcontextprotocol.spec.McpSchema.ErrorCodes.INTERNAL_ERROR;
9+
import static io.modelcontextprotocol.spec.McpSchema.Role.USER;
10+
11+
import java.io.IOException;
12+
import java.util.List;
13+
import java.util.Objects;
14+
15+
import io.jooby.SneakyThrows;
816
import io.modelcontextprotocol.json.McpJsonMapper;
17+
import io.modelcontextprotocol.spec.McpError;
918
import io.modelcontextprotocol.spec.McpSchema;
1019

1120
public class McpResult {
1221

13-
public McpResult(McpJsonMapper json) {}
22+
private final McpJsonMapper json;
23+
24+
public McpResult(McpJsonMapper json) {
25+
this.json = json;
26+
}
1427

15-
public McpSchema.CallToolResult toCallToolResult(Object result) {
16-
return null;
28+
public McpSchema.CallToolResult toCallToolResult(Object result, boolean structuredContent) {
29+
try {
30+
if (result == null) {
31+
return buildTextResult("null", false);
32+
} else if (result instanceof McpSchema.CallToolResult callToolResult) {
33+
return callToolResult;
34+
} else if (result instanceof String str) {
35+
return buildTextResult(str, false);
36+
} else if (result instanceof McpSchema.Content content) {
37+
return McpSchema.CallToolResult.builder().content(List.of(content)).isError(false).build();
38+
} else {
39+
if (structuredContent) {
40+
return McpSchema.CallToolResult.builder()
41+
.structuredContent(result)
42+
.isError(false)
43+
.build();
44+
} else {
45+
return buildTextResult(json.writeValueAsString(result), false);
46+
}
47+
}
48+
} catch (Exception x) {
49+
throw SneakyThrows.propagate(x);
50+
}
1751
}
1852

1953
public McpSchema.GetPromptResult toPromptResult(Object result) {
20-
return null;
54+
if (result == null) {
55+
return new McpSchema.GetPromptResult(null, List.of());
56+
} else if (result instanceof McpSchema.GetPromptResult promptResult) {
57+
return promptResult;
58+
} else if (result instanceof McpSchema.PromptMessage promptMessage) {
59+
return new McpSchema.GetPromptResult(null, List.of(promptMessage));
60+
} else if (result instanceof McpSchema.Content content) {
61+
var promptMessage = new McpSchema.PromptMessage(USER, content);
62+
return new McpSchema.GetPromptResult(null, List.of(promptMessage));
63+
} else if (result instanceof String str) {
64+
var promptMessage = new McpSchema.PromptMessage(USER, new McpSchema.TextContent(str));
65+
return new McpSchema.GetPromptResult(null, List.of(promptMessage));
66+
} else if (result instanceof List<?> items) {
67+
//noinspection unchecked
68+
return handleListReturnType((List<McpSchema.PromptMessage>) result, items);
69+
} else {
70+
var promptMessage =
71+
new McpSchema.PromptMessage(USER, new McpSchema.TextContent(result.toString()));
72+
return new McpSchema.GetPromptResult(null, List.of(promptMessage));
73+
}
2174
}
2275

23-
public McpSchema.ReadResourceResult toResourceResult(Object result) {
24-
return null;
76+
public McpSchema.ReadResourceResult toResourceResult(String uri, Object result) {
77+
try {
78+
if (result == null) {
79+
return new McpSchema.ReadResourceResult(List.of());
80+
} else if (result instanceof McpSchema.ReadResourceResult resourceResult) {
81+
return resourceResult;
82+
} else if (result instanceof McpSchema.ResourceContents resourceContents) {
83+
return new McpSchema.ReadResourceResult(List.of(resourceContents));
84+
} else if (result instanceof List<?> contents) {
85+
return handleListReturnType(result, uri, json, contents);
86+
} else {
87+
return toJsonResult(result, uri, json);
88+
}
89+
} catch (Exception x) {
90+
throw SneakyThrows.propagate(x);
91+
}
2592
}
2693

2794
public McpSchema.CompleteResult toCompleteResult(Object result) {
28-
return null;
95+
try {
96+
Objects.requireNonNull(result, "Completion result cannot be null");
97+
98+
if (result instanceof McpSchema.CompleteResult completeResult) {
99+
return completeResult;
100+
} else if (result instanceof McpSchema.CompleteResult.CompleteCompletion completion) {
101+
return new McpSchema.CompleteResult(completion);
102+
} else if (result instanceof List<?> values) {
103+
if (values.isEmpty()) {
104+
return new McpSchema.CompleteResult(
105+
new McpSchema.CompleteResult.CompleteCompletion(List.of(), 0, false));
106+
} else {
107+
var item = values.getFirst();
108+
if (item instanceof String) {
109+
//noinspection unchecked
110+
return new McpSchema.CompleteResult(
111+
new McpSchema.CompleteResult.CompleteCompletion(
112+
(List<String>) values, values.size(), false));
113+
}
114+
}
115+
} else if (result instanceof String singleValue) {
116+
var completion =
117+
new McpSchema.CompleteResult.CompleteCompletion(List.of(singleValue), 1, false);
118+
return new McpSchema.CompleteResult(completion);
119+
}
120+
121+
throw new IllegalStateException("Unexpected error occurred while handling completion result");
122+
} catch (Exception ex) {
123+
throw new McpError(
124+
new McpSchema.JSONRPCResponse.JSONRPCError(INTERNAL_ERROR, ex.getMessage(), null));
125+
}
126+
}
127+
128+
private static McpSchema.ReadResourceResult handleListReturnType(
129+
Object result, String uri, McpJsonMapper mcpJsonMapper, List<?> contents) throws IOException {
130+
if (contents.isEmpty()) {
131+
return new McpSchema.ReadResourceResult(List.of());
132+
} else {
133+
var item = contents.getFirst();
134+
if (item instanceof McpSchema.ResourceContents) {
135+
//noinspection unchecked
136+
return new McpSchema.ReadResourceResult((List<McpSchema.ResourceContents>) contents);
137+
} else {
138+
return toJsonResult(result, uri, mcpJsonMapper);
139+
}
140+
}
141+
}
142+
143+
static McpSchema.ReadResourceResult toJsonResult(
144+
Object result, String uri, McpJsonMapper mcpJsonMapper) throws IOException {
145+
var resultStr = mcpJsonMapper.writeValueAsString(result);
146+
var content = new McpSchema.TextResourceContents(uri, "application/json", resultStr);
147+
return new McpSchema.ReadResourceResult(List.of(content));
148+
}
149+
150+
private McpSchema.CallToolResult buildTextResult(String text, boolean isError) {
151+
return McpSchema.CallToolResult.builder().addTextContent(text).isError(isError).build();
152+
}
153+
154+
private static McpSchema.GetPromptResult handleListReturnType(
155+
List<McpSchema.PromptMessage> result, List<?> items) {
156+
if (items.isEmpty()) {
157+
return new McpSchema.GetPromptResult(null, List.of());
158+
} else {
159+
var item = items.getFirst();
160+
if (item instanceof McpSchema.PromptMessage) {
161+
return new McpSchema.GetPromptResult(null, result);
162+
} else {
163+
var msgs =
164+
items.stream()
165+
.map(
166+
i -> new McpSchema.PromptMessage(USER, new McpSchema.TextContent(i.toString())))
167+
.toList();
168+
return new McpSchema.GetPromptResult(null, msgs);
169+
}
170+
}
29171
}
30172
}

0 commit comments

Comments
 (0)