Skip to content

Commit f722ce8

Browse files
committed
- add default loging to fallback invoker
1 parent 8d53bbf commit f722ce8

7 files changed

Lines changed: 158 additions & 40 deletions

File tree

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

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ private String findTargetMethodName(String ref) {
114114
@Override
115115
public String toSourceCode(boolean kt) throws IOException {
116116
var generateTypeName = getTargetType().getSimpleName().toString();
117+
var targetClassName = getTargetType().toString();
117118
var mcpClassName = getGeneratedType().substring(getGeneratedType().lastIndexOf('.') + 1);
118119
var packageName = getPackageName();
119120

@@ -280,17 +281,36 @@ public String toSourceCode(boolean kt) throws IOException {
280281

281282
String lambda;
282283
if (completionGroups.containsKey(ref)) {
283-
var handlerName = findTargetMethodName(ref) + "CompletionHandler";
284+
var targetMethod = findTargetMethodName(ref);
285+
var handlerName = targetMethod + "CompletionHandler";
284286
var operationId = "completions/" + ref;
287+
288+
String operationArg =
289+
kt
290+
? "io.jooby.mcp.McpOperation("
291+
+ string(operationId)
292+
+ ", "
293+
+ string(targetClassName)
294+
+ ", "
295+
+ string(targetMethod)
296+
+ ")"
297+
: "new io.jooby.mcp.McpOperation("
298+
+ string(operationId)
299+
+ ", "
300+
+ string(targetClassName)
301+
+ ", "
302+
+ string(targetMethod)
303+
+ ")";
304+
285305
lambda =
286306
kt
287307
? "{ exchange, req -> invoker.invoke("
288-
+ string(operationId)
308+
+ operationArg
289309
+ ") { this."
290310
+ handlerName
291311
+ "(exchange, exchange.transportContext(), req) } }"
292312
: "(exchange, req) -> invoker.invoke("
293-
+ string(operationId)
313+
+ operationArg
294314
+ ", () -> this."
295315
+ handlerName
296316
+ "(exchange, exchange.transportContext(), req))";
@@ -377,17 +397,36 @@ public String toSourceCode(boolean kt) throws IOException {
377397

378398
String lambda;
379399
if (completionGroups.containsKey(ref)) {
380-
var handlerName = findTargetMethodName(ref) + "CompletionHandler";
400+
var targetMethod = findTargetMethodName(ref);
401+
var handlerName = targetMethod + "CompletionHandler";
381402
var operationId = "completions/" + ref;
403+
404+
var operationArg =
405+
kt
406+
? "io.jooby.mcp.McpOperation("
407+
+ string(operationId)
408+
+ ", "
409+
+ string(targetClassName)
410+
+ ", "
411+
+ string(targetMethod)
412+
+ ")"
413+
: "new io.jooby.mcp.McpOperation("
414+
+ string(operationId)
415+
+ ", "
416+
+ string(targetClassName)
417+
+ ", "
418+
+ string(targetMethod)
419+
+ ")";
420+
382421
lambda =
383422
kt
384423
? "{ ctx, req -> invoker.invoke("
385-
+ string(operationId)
424+
+ operationArg
386425
+ ") { this."
387426
+ handlerName
388427
+ "(null, ctx, req) } }"
389428
: "(ctx, req) -> invoker.invoke("
390-
+ string(operationId)
429+
+ operationArg
391430
+ ", () -> this."
392431
+ handlerName
393432
+ "(null, ctx, req))";
@@ -530,31 +569,47 @@ public String toSourceCode(boolean kt) throws IOException {
530569
.orElse("")
531570
: "";
532571
}
533-
if (mcpName == null || mcpName.isEmpty()) mcpName = methodName;
534-
String operationId = mcpType + "/" + mcpName;
572+
if (mcpName.isEmpty()) mcpName = methodName;
573+
var operationId = mcpType + "/" + mcpName;
574+
575+
var operationArg =
576+
kt
577+
? "io.jooby.mcp.McpOperation("
578+
+ string(operationId)
579+
+ ", "
580+
+ string(targetClassName)
581+
+ ", "
582+
+ string(methodName)
583+
+ ")"
584+
: "new io.jooby.mcp.McpOperation("
585+
+ string(operationId)
586+
+ ", "
587+
+ string(targetClassName)
588+
+ ", "
589+
+ string(methodName)
590+
+ ")";
535591

536-
// --- Lambda Router Definition ---
537-
String lambda =
592+
var lambda =
538593
kt
539594
? (isStateless
540595
? "{ ctx, req -> invoker.invoke("
541-
+ string(operationId)
596+
+ operationArg
542597
+ ") { this."
543598
+ methodName
544599
+ "(null, ctx, req) } }"
545600
: "{ exchange, req -> invoker.invoke("
546-
+ string(operationId)
601+
+ operationArg
547602
+ ") { this."
548603
+ methodName
549604
+ "(exchange, exchange.transportContext(), req) } }")
550605
: (isStateless
551606
? "(ctx, req) -> invoker.invoke("
552-
+ string(operationId)
607+
+ operationArg
553608
+ ", () -> this."
554609
+ methodName
555610
+ "(null, ctx, req))"
556611
: "(exchange, req) -> invoker.invoke("
557-
+ string(operationId)
612+
+ operationArg
558613
+ ", () -> this."
559614
+ methodName
560615
+ "(exchange, exchange.transportContext(), req))");

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,17 @@ public String serverKey() {
6464
public java.util.List<io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification> completions(io.jooby.Jooby app) {
6565
var invoker = app.require(io.jooby.mcp.McpInvoker.class);
6666
var completions = new java.util.ArrayList<io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification>();
67-
completions.add(new io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.PromptReference("review_code"), (exchange, req) -> invoker.invoke("completions/review_code", () -> this.reviewCodeCompletionHandler(exchange, exchange.transportContext(), req))));
68-
completions.add(new io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.ResourceReference("file:///users/{id}/{name}/profile"), (exchange, req) -> invoker.invoke("completions/file:///users/{id}/{name}/profile", () -> this.getUserProfileCompletionHandler(exchange, exchange.transportContext(), req))));
67+
completions.add(new io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.PromptReference("review_code"), (exchange, req) -> invoker.invoke(new io.jooby.mcp.McpOperation("completions/review_code", "tests.i3830.ExampleServer", "reviewCode"), () -> this.reviewCodeCompletionHandler(exchange, exchange.transportContext(), req))));
68+
completions.add(new io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.ResourceReference("file:///users/{id}/{name}/profile"), (exchange, req) -> invoker.invoke(new io.jooby.mcp.McpOperation("completions/file:///users/{id}/{name}/profile", "tests.i3830.ExampleServer", "getUserProfile"), () -> this.getUserProfileCompletionHandler(exchange, exchange.transportContext(), req))));
6969
return completions;
7070
}
7171
7272
@Override
7373
public java.util.List<io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification> statelessCompletions(io.jooby.Jooby app) {
7474
var invoker = app.require(io.jooby.mcp.McpInvoker.class);
7575
var completions = new java.util.ArrayList<io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification>();
76-
completions.add(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.PromptReference("review_code"), (ctx, req) -> invoker.invoke("completions/review_code", () -> this.reviewCodeCompletionHandler(null, ctx, req))));
77-
completions.add(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.ResourceReference("file:///users/{id}/{name}/profile"), (ctx, req) -> invoker.invoke("completions/file:///users/{id}/{name}/profile", () -> this.getUserProfileCompletionHandler(null, ctx, req))));
76+
completions.add(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.PromptReference("review_code"), (ctx, req) -> invoker.invoke(new io.jooby.mcp.McpOperation("completions/review_code", "tests.i3830.ExampleServer", "reviewCode"), () -> this.reviewCodeCompletionHandler(null, ctx, req))));
77+
completions.add(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.ResourceReference("file:///users/{id}/{name}/profile"), (ctx, req) -> invoker.invoke(new io.jooby.mcp.McpOperation("completions/file:///users/{id}/{name}/profile", "tests.i3830.ExampleServer", "getUserProfile"), () -> this.getUserProfileCompletionHandler(null, ctx, req))));
7878
return completions;
7979
}
8080
@@ -84,10 +84,10 @@ public void install(io.jooby.Jooby app, io.modelcontextprotocol.server.McpSyncSe
8484
var invoker = app.require(io.jooby.mcp.McpInvoker.class);
8585
var schemaGenerator = app.require(com.github.victools.jsonschema.generator.SchemaGenerator.class);
8686
87-
server.addTool(new io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification(addToolSpec(schemaGenerator), (exchange, req) -> invoker.invoke("tools/calculator", () -> this.add(exchange, exchange.transportContext(), req))));
88-
server.addPrompt(new io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification(reviewCodePromptSpec(), (exchange, req) -> invoker.invoke("prompts/review_code", () -> this.reviewCode(exchange, exchange.transportContext(), req))));
89-
server.addResource(new io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification(getLogsResourceSpec(), (exchange, req) -> invoker.invoke("resources/file:///logs/app.log", () -> this.getLogs(exchange, exchange.transportContext(), req))));
90-
server.addResourceTemplate(new io.modelcontextprotocol.server.McpServerFeatures.SyncResourceTemplateSpecification(getUserProfileResourceTemplateSpec(), (exchange, req) -> invoker.invoke("resources/file:///users/{id}/{name}/profile", () -> this.getUserProfile(exchange, exchange.transportContext(), req))));
87+
server.addTool(new io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification(addToolSpec(schemaGenerator), (exchange, req) -> invoker.invoke(new io.jooby.mcp.McpOperation("tools/calculator", "tests.i3830.ExampleServer", "add"), () -> this.add(exchange, exchange.transportContext(), req))));
88+
server.addPrompt(new io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification(reviewCodePromptSpec(), (exchange, req) -> invoker.invoke(new io.jooby.mcp.McpOperation("prompts/review_code", "tests.i3830.ExampleServer", "reviewCode"), () -> this.reviewCode(exchange, exchange.transportContext(), req))));
89+
server.addResource(new io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification(getLogsResourceSpec(), (exchange, req) -> invoker.invoke(new io.jooby.mcp.McpOperation("resources/file:///logs/app.log", "tests.i3830.ExampleServer", "getLogs"), () -> this.getLogs(exchange, exchange.transportContext(), req))));
90+
server.addResourceTemplate(new io.modelcontextprotocol.server.McpServerFeatures.SyncResourceTemplateSpecification(getUserProfileResourceTemplateSpec(), (exchange, req) -> invoker.invoke(new io.jooby.mcp.McpOperation("resources/file:///users/{id}/{name}/profile", "tests.i3830.ExampleServer", "getUserProfile"), () -> this.getUserProfile(exchange, exchange.transportContext(), req))));
9191
}
9292
9393
@Override
@@ -96,10 +96,10 @@ public void install(io.jooby.Jooby app, io.modelcontextprotocol.server.McpStatel
9696
var invoker = app.require(io.jooby.mcp.McpInvoker.class);
9797
var schemaGenerator = app.require(com.github.victools.jsonschema.generator.SchemaGenerator.class);
9898
99-
server.addTool(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification(addToolSpec(schemaGenerator), (ctx, req) -> invoker.invoke("tools/calculator", () -> this.add(null, ctx, req))));
100-
server.addPrompt(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification(reviewCodePromptSpec(), (ctx, req) -> invoker.invoke("prompts/review_code", () -> this.reviewCode(null, ctx, req))));
101-
server.addResource(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpecification(getLogsResourceSpec(), (ctx, req) -> invoker.invoke("resources/file:///logs/app.log", () -> this.getLogs(null, ctx, req))));
102-
server.addResourceTemplate(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceTemplateSpecification(getUserProfileResourceTemplateSpec(), (ctx, req) -> invoker.invoke("resources/file:///users/{id}/{name}/profile", () -> this.getUserProfile(null, ctx, req))));
99+
server.addTool(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification(addToolSpec(schemaGenerator), (ctx, req) -> invoker.invoke(new io.jooby.mcp.McpOperation("tools/calculator", "tests.i3830.ExampleServer", "add"), () -> this.add(null, ctx, req))));
100+
server.addPrompt(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification(reviewCodePromptSpec(), (ctx, req) -> invoker.invoke(new io.jooby.mcp.McpOperation("prompts/review_code", "tests.i3830.ExampleServer", "reviewCode"), () -> this.reviewCode(null, ctx, req))));
101+
server.addResource(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpecification(getLogsResourceSpec(), (ctx, req) -> invoker.invoke(new io.jooby.mcp.McpOperation("resources/file:///logs/app.log", "tests.i3830.ExampleServer", "getLogs"), () -> this.getLogs(null, ctx, req))));
102+
server.addResourceTemplate(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceTemplateSpecification(getUserProfileResourceTemplateSpec(), (ctx, req) -> invoker.invoke(new io.jooby.mcp.McpOperation("resources/file:///users/{id}/{name}/profile", "tests.i3830.ExampleServer", "getUserProfile"), () -> this.getUserProfile(null, ctx, req))));
103103
}
104104
105105
private io.modelcontextprotocol.spec.McpSchema.Tool addToolSpec(com.github.victools.jsonschema.generator.SchemaGenerator schemaGenerator) {

modules/jooby-mcp/src/main/java/io/jooby/internal/mcp/DefaultMcpInvoker.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
*/
66
package io.jooby.internal.mcp;
77

8+
import org.slf4j.LoggerFactory;
9+
10+
import edu.umd.cs.findbugs.annotations.NonNull;
811
import io.jooby.Jooby;
912
import io.jooby.SneakyThrows;
1013
import io.jooby.StatusCode;
1114
import io.jooby.mcp.McpInvoker;
15+
import io.jooby.mcp.McpOperation;
1216
import io.modelcontextprotocol.spec.McpError;
1317
import io.modelcontextprotocol.spec.McpSchema;
1418

@@ -21,19 +25,25 @@ public DefaultMcpInvoker(Jooby application) {
2125

2226
@SuppressWarnings("unchecked")
2327
@Override
24-
public <R> R invoke(String operationId, SneakyThrows.Supplier<R> action) {
28+
public @NonNull <R> R invoke(McpOperation operation, SneakyThrows.Supplier<R> action) {
2529
try {
2630
return action.get();
2731
} catch (McpError mcpError) {
2832
throw mcpError;
2933
} catch (Throwable cause) {
30-
if (operationId.startsWith("tools/")) {
34+
var log = LoggerFactory.getLogger(operation.className());
35+
if (operation.id().startsWith("tools/")) {
3136
// Tool error
3237
var errorMessage = cause.getMessage() != null ? cause.getMessage() : cause.toString();
3338
return (R)
3439
McpSchema.CallToolResult.builder().addTextContent(errorMessage).isError(true).build();
3540
}
3641
var statusCode = application.getRouter().errorCode(cause);
42+
if (statusCode.value() >= 500) {
43+
log.error("execution of {} resulted in exception", operation.id(), cause);
44+
} else {
45+
log.debug("execution of {} resulted in exception", operation.id(), cause);
46+
}
3747
var mcpErrorCode = toMcpErrorCode(statusCode);
3848
throw new McpError(
3949
new McpSchema.JSONRPCResponse.JSONRPCError(mcpErrorCode, cause.getMessage(), null));

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,13 @@ private McpServerConfig resolveMcpServerConfig(Jooby app) {
171171
new StartupException("MCP server named '%s' not found".formatted(defaultServer)));
172172
}
173173

174-
return srvConfigs.get(0);
174+
return srvConfigs.getFirst();
175175
}
176176

177177
private String buildConfigJson(McpServerConfig config, String location) {
178178
var endpoint = resolveEndpoint(config);
179-
var transport = config.getTransport();
179+
var transport =
180+
config.isSseTransport() ? McpModule.Transport.SSE : McpModule.Transport.STREAMABLE_HTTP;
180181
return """
181182
{
182183
"defaultEnvironment": {

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,13 @@ public interface McpInvoker {
5050
/**
5151
* Executes the given MCP operation.
5252
*
53-
* @param operationId The identifier of the operation being executed. Typically formatted as
54-
* {@code [type]/[name]} (e.g., {@code "tools/add_numbers"}, {@code "prompts/greeting"}, or
55-
* {@code "resources/file://config"}).
53+
* @param operation The operation being executed.
5654
* @param action The actual execution of the operation, or the next invoker in the chain. Must be
5755
* invoked via {@link SneakyThrows.Supplier#get()} to proceed.
5856
* @param <R> The return type of the operation.
5957
* @return The result of the operation.
6058
*/
61-
<R> R invoke(String operationId, SneakyThrows.Supplier<R> action);
59+
<R> R invoke(McpOperation operation, SneakyThrows.Supplier<R> action);
6260

6361
/**
6462
* Chains this invoker with another one. This invoker runs first, and its "action" becomes calling
@@ -76,8 +74,8 @@ default McpInvoker then(McpInvoker next) {
7674
}
7775
return new McpInvoker() {
7876
@Override
79-
public <R> R invoke(String operationId, SneakyThrows.Supplier<R> action) {
80-
return McpInvoker.this.invoke(operationId, () -> next.invoke(operationId, action));
77+
public <R> R invoke(McpOperation operation, SneakyThrows.Supplier<R> action) {
78+
return McpInvoker.this.invoke(operation, () -> next.invoke(operation, action));
8179
}
8280
};
8381
}

0 commit comments

Comments
 (0)