Skip to content

Commit c0aaed1

Browse files
committed
refactor(mcp): simplify generated code via runtime invoker adapters
- Introduced `McpOperation` to cleanly encapsulate static routing metadata and runtime arguments. - Refactored `McpInvoker` to act as a factory, providing strongly-typed adapters (`asToolHandler`, `asCompletionHandler`, etc.) for both stateful and stateless servers. - Updated APT generator (`McpRouter`, `McpRoute`) to output clean method references instead of complex, boilerplate-heavy lambda chains.
1 parent 6b602e2 commit c0aaed1

14 files changed

Lines changed: 659 additions & 131 deletions

File tree

docs/asciidoc/modules/mcp.adoc

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,16 +204,21 @@ Invokers are chained. You can register multiple invokers and they will wrap the
204204
----
205205
import io.jooby.mcp.McpInvoker;
206206
import io.jooby.mcp.McpOperation;
207+
import io.jooby.mcp.McpChain;
208+
import io.modelcontextprotocol.common.McpTransportContext;
209+
import io.modelcontextprotocol.server.McpSyncServerExchange;
210+
import org.jspecify.annotations.Nullable;
207211
import org.slf4j.MDC;
208212
209213
public class MdcMcpInvoker implements McpInvoker {
210214
@Override
211-
public <R> R invoke(McpOperation operation, SneakyThrows.Supplier<R> action) {
215+
public <R> R invoke(@Nullable McpSyncServerExchange exchange, McpTransportContext transportContext, McpOperation operation, McpChain chain) throws Exception {
212216
try {
213-
MDC.put("mcp.id", operation.id()); <1>
217+
MDC.put("mcp.id", operation.id()); // <1>
214218
MDC.put("mcp.class", operation.className());
215219
MDC.put("mcp.method", operation.methodName());
216-
return action.get(); <2>
220+
221+
return chain.proceed(exchange, transportContext, operation); // <2>
217222
} finally {
218223
MDC.remove("mcp.id");
219224
MDC.remove("mcp.class");
@@ -224,13 +229,13 @@ public class MdcMcpInvoker implements McpInvoker {
224229
225230
{
226231
install(new McpModule(new CalculatorServiceMcp_())
227-
.invoker(new MdcMcpInvoker())); <3>
232+
.invoker(new MdcMcpInvoker())); // <3>
228233
}
229234
----
230235

231236
<1> Extract rich contextual data from the `McpOperation` record.
232-
<2> Proceed with the execution chain.
233-
<3> Register the invoker. Jooby will safely map any business exceptions thrown by your action into valid MCP JSON-RPC errors.
237+
<2> Proceed to the next interceptor in the chain or execute the final target handler.
238+
<3> Register the invoker. Jooby will safely map any business exceptions thrown by your chain into valid MCP JSON-RPC errors.
234239

235240
=== Multiple Servers
236241

modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import javax.tools.*;
2525

2626
import io.jooby.internal.apt.*;
27-
27+
import io.jooby.internal.apt.mcp.McpRouter;
2828
import io.jooby.internal.apt.ws.WsRouter;
2929

3030
/** Process jooby/jakarta annotation and generate source code from MVC controllers. */

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
44
* Copyright 2014 Edgar Espina
55
*/
6-
package io.jooby.internal.apt;
6+
package io.jooby.internal.apt.mcp;
77

88
import static io.jooby.internal.apt.CodeBlock.*;
99
import static io.jooby.internal.apt.CodeBlock.string;
@@ -15,6 +15,8 @@
1515

1616
import javax.lang.model.element.ExecutableElement;
1717

18+
import io.jooby.internal.apt.AnnotationSupport;
19+
import io.jooby.internal.apt.WebRoute;
1820
import io.jooby.javadoc.JavaDocNode;
1921
import io.jooby.javadoc.MethodDoc;
2022

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

Lines changed: 55 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
44
* Copyright 2014 Edgar Espina
55
*/
6-
package io.jooby.internal.apt;
6+
package io.jooby.internal.apt.mcp;
77

88
import static io.jooby.internal.apt.AnnotationSupport.VALUE;
99
import static io.jooby.internal.apt.CodeBlock.*;
@@ -23,6 +23,9 @@
2323
import javax.lang.model.element.ExecutableElement;
2424
import javax.lang.model.element.TypeElement;
2525

26+
import io.jooby.internal.apt.AnnotationSupport;
27+
import io.jooby.internal.apt.MvcContext;
28+
import io.jooby.internal.apt.WebRouter;
2629
import io.jooby.javadoc.JavaDocParser;
2730
import io.jooby.javadoc.MethodDoc;
2831

@@ -347,38 +350,31 @@ private void appendCompletions(
347350
? "io.modelcontextprotocol.spec.McpSchema.ResourceReference"
348351
: "io.modelcontextprotocol.spec.McpSchema.PromptReference";
349352

350-
String lambda;
353+
String invokerCall;
351354
if (groups.containsKey(ref)) {
352355
var targetMethod = findTargetMethodName(ref);
353356
var handlerName = targetMethod + "CompletionHandler";
354-
var operationArg = generateOperationArg(kt, "completions/" + ref, targetMethod);
355-
356-
String invokeArgs =
357-
isStateless ? "null, ctx, req" : "exchange, exchange.transportContext(), req";
358-
String lambdaArgs = isStateless ? "ctx, req" : "exchange, req";
359-
360-
lambda =
361-
kt
362-
? "{ "
363-
+ lambdaArgs
364-
+ " -> invoker.invoke("
365-
+ operationArg
366-
+ ") { this."
367-
+ handlerName
368-
+ "("
369-
+ invokeArgs
370-
+ ") } }"
371-
: "("
372-
+ lambdaArgs
373-
+ ") -> invoker.invoke("
374-
+ operationArg
375-
+ ", () -> this."
376-
+ handlerName
377-
+ "("
378-
+ invokeArgs
379-
+ "))";
357+
var targetClass = getTargetType().toString();
358+
359+
String adapterMethod = isStateless ? "asStatelessCompletionHandler" : "asCompletionHandler";
360+
String handlerRef = "this::" + handlerName;
361+
String operationId = "completions/" + ref; // <--- ADD THIS
362+
363+
// Update the string builder to include operationId:
364+
invokerCall =
365+
"invoker."
366+
+ adapterMethod
367+
+ "("
368+
+ string(operationId)
369+
+ ", "
370+
+ string(targetClass)
371+
+ ", "
372+
+ string(targetMethod)
373+
+ ", "
374+
+ handlerRef
375+
+ ")";
380376
} else {
381-
lambda =
377+
invokerCall =
382378
kt
383379
? "{ _, _ -> io.jooby.mcp.McpResult(this.json).toCompleteResult(emptyList<Any>()) }"
384380
: "(exchange, req) -> new"
@@ -398,7 +394,7 @@ private void appendCompletions(
398394
"(",
399395
string(ref),
400396
"), ",
401-
lambda,
397+
invokerCall,
402398
"))"));
403399
} else {
404400
buffer.append(
@@ -411,7 +407,7 @@ private void appendCompletions(
411407
"(",
412408
string(ref),
413409
"), ",
414-
lambda,
410+
invokerCall,
415411
"))",
416412
semicolon(kt)));
417413
}
@@ -482,32 +478,30 @@ private void appendInstall(
482478
var mcpType = getMcpRouteType(route);
483479
if (mcpType.isEmpty()) continue;
484480

485-
var operationArg = generateOperationArg(kt, mcpType + "/" + mcpName, methodName);
486-
487-
String invokeArgs =
488-
isStateless ? "null, ctx, req" : "exchange, exchange.transportContext(), req";
489-
String lambdaArgs = isStateless ? "ctx, req" : "exchange, req";
490-
491-
var lambda =
492-
kt
493-
? "{ "
494-
+ lambdaArgs
495-
+ " -> invoker.invoke("
496-
+ operationArg
497-
+ ") { this."
498-
+ methodName
499-
+ "("
500-
+ invokeArgs
501-
+ ") } }"
502-
: "("
503-
+ lambdaArgs
504-
+ ") -> invoker.invoke("
505-
+ operationArg
506-
+ ", () -> this."
507-
+ methodName
508-
+ "("
509-
+ invokeArgs
510-
+ "))";
481+
String adapterMethod = "";
482+
if (route.isMcpTool())
483+
adapterMethod = isStateless ? "asStatelessToolHandler" : "asToolHandler";
484+
else if (route.isMcpPrompt())
485+
adapterMethod = isStateless ? "asStatelessPromptHandler" : "asPromptHandler";
486+
else if (route.isMcpResource() || route.isMcpResourceTemplate())
487+
adapterMethod = isStateless ? "asStatelessResourceHandler" : "asResourceHandler";
488+
489+
String handlerRef = "this::" + methodName;
490+
String targetClass = getTargetType().toString();
491+
String operationId = mcpType + "/" + mcpName;
492+
String invokerCall =
493+
of(
494+
"invoker.",
495+
adapterMethod,
496+
"(",
497+
string(operationId),
498+
",",
499+
string(targetClass),
500+
", ",
501+
string(methodName),
502+
", ",
503+
handlerRef,
504+
")");
511505

512506
String prefix = kt ? "" : "new ";
513507
String serverMethod = "io.modelcontextprotocol.server." + featuresClass + ".";
@@ -522,7 +516,7 @@ private void appendInstall(
522516
"SyncToolSpecification(",
523517
methodName,
524518
"ToolSpec(schemaGenerator), ",
525-
lambda,
519+
invokerCall,
526520
"))",
527521
semicolon(kt)));
528522
} else if (route.isMcpPrompt()) {
@@ -535,7 +529,7 @@ private void appendInstall(
535529
"SyncPromptSpecification(",
536530
methodName,
537531
"PromptSpec(), ",
538-
lambda,
532+
invokerCall,
539533
"))",
540534
semicolon(kt)));
541535
} else if (route.isMcpResource() || route.isMcpResourceTemplate()) {
@@ -556,7 +550,7 @@ private void appendInstall(
556550
methodName,
557551
defMethod,
558552
", ",
559-
lambda,
553+
invokerCall,
560554
"))",
561555
semicolon(kt)));
562556
}
@@ -581,7 +575,7 @@ private void appendCompletionHandlers(
581575
+ " io.modelcontextprotocol.spec.McpSchema.CompleteRequest):"
582576
+ " io.modelcontextprotocol.spec.McpSchema.CompleteResult {"));
583577
buffer.append(
584-
statement(indent(6), "val ctx = transportContext.get(\"CTX\") as io.jooby.Context"));
578+
statement(indent(6), "val ctx = transportContext.get(\"CTX\") as? io.jooby.Context"));
585579
buffer.append(statement(indent(6), "val c = this.factory.apply(ctx)"));
586580
buffer.append(statement(indent(6), "val targetArg = req.argument()?.name() ?: \"\""));
587581
buffer.append(statement(indent(6), "val typedValue = req.argument()?.value() ?: \"\""));
@@ -686,18 +680,6 @@ else if (type.equals("io.modelcontextprotocol.common.McpTransportContext"))
686680
}
687681
}
688682

689-
private String generateOperationArg(boolean kt, String operationId, String targetMethod) {
690-
String prefix = kt ? "" : "new ";
691-
return prefix
692-
+ "io.jooby.mcp.McpOperation("
693-
+ string(operationId)
694-
+ ", "
695-
+ string(getTargetType().toString())
696-
+ ", "
697-
+ string(targetMethod)
698-
+ ")";
699-
}
700-
701683
public Optional<MethodDoc> getMethodDoc(String methodName, List<String> types) {
702684
return javadoc.parse(getTargetType().toString()).flatMap(it -> it.getMethod(methodName, types));
703685
}

0 commit comments

Comments
 (0)