Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import javax.tools.*;

import io.jooby.internal.apt.*;

import io.jooby.internal.apt.mcp.McpRouter;
import io.jooby.internal.apt.ws.WsRouter;

/** Process jooby/jakarta annotation and generate source code from MVC controllers. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.internal.apt;
package io.jooby.internal.apt.mcp;

import static io.jooby.internal.apt.CodeBlock.*;
import static io.jooby.internal.apt.CodeBlock.string;
Expand All @@ -15,6 +15,8 @@

import javax.lang.model.element.ExecutableElement;

import io.jooby.internal.apt.AnnotationSupport;
import io.jooby.internal.apt.WebRoute;
import io.jooby.javadoc.JavaDocNode;
import io.jooby.javadoc.MethodDoc;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.internal.apt;
package io.jooby.internal.apt.mcp;

import static io.jooby.internal.apt.AnnotationSupport.VALUE;
import static io.jooby.internal.apt.CodeBlock.*;
Expand All @@ -23,6 +23,9 @@
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;

import io.jooby.internal.apt.AnnotationSupport;
import io.jooby.internal.apt.MvcContext;
import io.jooby.internal.apt.WebRouter;
import io.jooby.javadoc.JavaDocParser;
import io.jooby.javadoc.MethodDoc;

Expand Down Expand Up @@ -347,38 +350,31 @@ private void appendCompletions(
? "io.modelcontextprotocol.spec.McpSchema.ResourceReference"
: "io.modelcontextprotocol.spec.McpSchema.PromptReference";

String lambda;
String invokerCall;
if (groups.containsKey(ref)) {
var targetMethod = findTargetMethodName(ref);
var handlerName = targetMethod + "CompletionHandler";
var operationArg = generateOperationArg(kt, "completions/" + ref, targetMethod);

String invokeArgs =
isStateless ? "null, ctx, req" : "exchange, exchange.transportContext(), req";
String lambdaArgs = isStateless ? "ctx, req" : "exchange, req";

lambda =
kt
? "{ "
+ lambdaArgs
+ " -> invoker.invoke("
+ operationArg
+ ") { this."
+ handlerName
+ "("
+ invokeArgs
+ ") } }"
: "("
+ lambdaArgs
+ ") -> invoker.invoke("
+ operationArg
+ ", () -> this."
+ handlerName
+ "("
+ invokeArgs
+ "))";
var targetClass = getTargetType().toString();

String adapterMethod = isStateless ? "asStatelessCompletionHandler" : "asCompletionHandler";
String handlerRef = "this::" + handlerName;
String operationId = "completions/" + ref; // <--- ADD THIS

// Update the string builder to include operationId:
invokerCall =
"invoker."
+ adapterMethod
+ "("
+ string(operationId)
+ ", "
+ string(targetClass)
+ ", "
+ string(targetMethod)
+ ", "
+ handlerRef
+ ")";
} else {
lambda =
invokerCall =
kt
? "{ _, _ -> io.jooby.mcp.McpResult(this.json).toCompleteResult(emptyList<Any>()) }"
: "(exchange, req) -> new"
Expand All @@ -398,7 +394,7 @@ private void appendCompletions(
"(",
string(ref),
"), ",
lambda,
invokerCall,
"))"));
} else {
buffer.append(
Expand All @@ -411,7 +407,7 @@ private void appendCompletions(
"(",
string(ref),
"), ",
lambda,
invokerCall,
"))",
semicolon(kt)));
}
Expand Down Expand Up @@ -482,32 +478,30 @@ private void appendInstall(
var mcpType = getMcpRouteType(route);
if (mcpType.isEmpty()) continue;

var operationArg = generateOperationArg(kt, mcpType + "/" + mcpName, methodName);

String invokeArgs =
isStateless ? "null, ctx, req" : "exchange, exchange.transportContext(), req";
String lambdaArgs = isStateless ? "ctx, req" : "exchange, req";

var lambda =
kt
? "{ "
+ lambdaArgs
+ " -> invoker.invoke("
+ operationArg
+ ") { this."
+ methodName
+ "("
+ invokeArgs
+ ") } }"
: "("
+ lambdaArgs
+ ") -> invoker.invoke("
+ operationArg
+ ", () -> this."
+ methodName
+ "("
+ invokeArgs
+ "))";
String adapterMethod = "";
if (route.isMcpTool())
adapterMethod = isStateless ? "asStatelessToolHandler" : "asToolHandler";
else if (route.isMcpPrompt())
adapterMethod = isStateless ? "asStatelessPromptHandler" : "asPromptHandler";
else if (route.isMcpResource() || route.isMcpResourceTemplate())
adapterMethod = isStateless ? "asStatelessResourceHandler" : "asResourceHandler";

String handlerRef = "this::" + methodName;
String targetClass = getTargetType().toString();
String operationId = mcpType + "/" + mcpName;
String invokerCall =
of(
"invoker.",
adapterMethod,
"(",
string(operationId),
",",
string(targetClass),
", ",
string(methodName),
", ",
handlerRef,
")");

String prefix = kt ? "" : "new ";
String serverMethod = "io.modelcontextprotocol.server." + featuresClass + ".";
Expand All @@ -522,7 +516,7 @@ private void appendInstall(
"SyncToolSpecification(",
methodName,
"ToolSpec(schemaGenerator), ",
lambda,
invokerCall,
"))",
semicolon(kt)));
} else if (route.isMcpPrompt()) {
Expand All @@ -535,7 +529,7 @@ private void appendInstall(
"SyncPromptSpecification(",
methodName,
"PromptSpec(), ",
lambda,
invokerCall,
"))",
semicolon(kt)));
} else if (route.isMcpResource() || route.isMcpResourceTemplate()) {
Expand All @@ -556,7 +550,7 @@ private void appendInstall(
methodName,
defMethod,
", ",
lambda,
invokerCall,
"))",
semicolon(kt)));
}
Expand All @@ -581,7 +575,7 @@ private void appendCompletionHandlers(
+ " io.modelcontextprotocol.spec.McpSchema.CompleteRequest):"
+ " io.modelcontextprotocol.spec.McpSchema.CompleteResult {"));
buffer.append(
statement(indent(6), "val ctx = transportContext.get(\"CTX\") as io.jooby.Context"));
statement(indent(6), "val ctx = transportContext.get(\"CTX\") as? io.jooby.Context"));
buffer.append(statement(indent(6), "val c = this.factory.apply(ctx)"));
buffer.append(statement(indent(6), "val targetArg = req.argument()?.name() ?: \"\""));
buffer.append(statement(indent(6), "val typedValue = req.argument()?.value() ?: \"\""));
Expand Down Expand Up @@ -686,18 +680,6 @@ else if (type.equals("io.modelcontextprotocol.common.McpTransportContext"))
}
}

private String generateOperationArg(boolean kt, String operationId, String targetMethod) {
String prefix = kt ? "" : "new ";
return prefix
+ "io.jooby.mcp.McpOperation("
+ string(operationId)
+ ", "
+ string(getTargetType().toString())
+ ", "
+ string(targetMethod)
+ ")";
}

public Optional<MethodDoc> getMethodDoc(String methodName, List<String> types) {
return javadoc.parse(getTargetType().toString()).flatMap(it -> it.getMethod(methodName, types));
}
Expand Down
24 changes: 12 additions & 12 deletions modules/jooby-apt/src/test/java/tests/i3830/Issue3830.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,17 @@ public String serverKey() {
public java.util.List<io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification> completions(io.jooby.Jooby app) {
var invoker = app.require(io.jooby.mcp.McpInvoker.class);
var completions = new java.util.ArrayList<io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification>();
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))));
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))));
completions.add(new io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.PromptReference("review_code"), invoker.asCompletionHandler("completions/review_code", "tests.i3830.ExampleServer", "reviewCode", this::reviewCodeCompletionHandler)));
completions.add(new io.modelcontextprotocol.server.McpServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.ResourceReference("file:///users/{id}/{name}/profile"), invoker.asCompletionHandler("completions/file:///users/{id}/{name}/profile", "tests.i3830.ExampleServer", "getUserProfile", this::getUserProfileCompletionHandler)));
return completions;
}

@Override
public java.util.List<io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification> statelessCompletions(io.jooby.Jooby app) {
var invoker = app.require(io.jooby.mcp.McpInvoker.class);
var completions = new java.util.ArrayList<io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification>();
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))));
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))));
completions.add(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.PromptReference("review_code"), invoker.asStatelessCompletionHandler("completions/review_code", "tests.i3830.ExampleServer", "reviewCode", this::reviewCodeCompletionHandler)));
completions.add(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncCompletionSpecification(new io.modelcontextprotocol.spec.McpSchema.ResourceReference("file:///users/{id}/{name}/profile"), invoker.asStatelessCompletionHandler("completions/file:///users/{id}/{name}/profile", "tests.i3830.ExampleServer", "getUserProfile", this::getUserProfileCompletionHandler)));
return completions;
}

Expand All @@ -91,10 +91,10 @@ public void install(io.jooby.Jooby app, io.modelcontextprotocol.server.McpSyncSe
var invoker = app.require(io.jooby.mcp.McpInvoker.class);
var schemaGenerator = app.require(com.github.victools.jsonschema.generator.SchemaGenerator.class);

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))));
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))));
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))));
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))));
server.addTool(new io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification(addToolSpec(schemaGenerator), invoker.asToolHandler("tools/calculator","tests.i3830.ExampleServer", "add", this::add)));
server.addPrompt(new io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification(reviewCodePromptSpec(), invoker.asPromptHandler("prompts/review_code","tests.i3830.ExampleServer", "reviewCode", this::reviewCode)));
server.addResource(new io.modelcontextprotocol.server.McpServerFeatures.SyncResourceSpecification(getLogsResourceSpec(), invoker.asResourceHandler("resources/file:///logs/app.log","tests.i3830.ExampleServer", "getLogs", this::getLogs)));
server.addResourceTemplate(new io.modelcontextprotocol.server.McpServerFeatures.SyncResourceTemplateSpecification(getUserProfileResourceTemplateSpec(), invoker.asResourceHandler("resources/file:///users/{id}/{name}/profile","tests.i3830.ExampleServer", "getUserProfile", this::getUserProfile)));
}

@Override
Expand All @@ -103,10 +103,10 @@ public void install(io.jooby.Jooby app, io.modelcontextprotocol.server.McpStatel
var invoker = app.require(io.jooby.mcp.McpInvoker.class);
var schemaGenerator = app.require(com.github.victools.jsonschema.generator.SchemaGenerator.class);

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))));
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))));
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))));
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))));
server.addTool(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification(addToolSpec(schemaGenerator), invoker.asStatelessToolHandler("tools/calculator","tests.i3830.ExampleServer", "add", this::add)));
server.addPrompt(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification(reviewCodePromptSpec(), invoker.asStatelessPromptHandler("prompts/review_code","tests.i3830.ExampleServer", "reviewCode", this::reviewCode)));
server.addResource(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceSpecification(getLogsResourceSpec(), invoker.asStatelessResourceHandler("resources/file:///logs/app.log","tests.i3830.ExampleServer", "getLogs", this::getLogs)));
server.addResourceTemplate(new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncResourceTemplateSpecification(getUserProfileResourceTemplateSpec(), invoker.asStatelessResourceHandler("resources/file:///users/{id}/{name}/profile","tests.i3830.ExampleServer", "getUserProfile", this::getUserProfile)));
}

private io.modelcontextprotocol.spec.McpSchema.Tool addToolSpec(com.github.victools.jsonschema.generator.SchemaGenerator schemaGenerator) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public OtelJsonRcpTracing onEnd(SneakyThrows.Consumer3<Context, JsonRpcRequest,
*/
@Override
public @NonNull Optional<JsonRpcResponse> invoke(
@NonNull Context ctx, @NonNull JsonRpcRequest request, JsonRpcChain chain) {
@NonNull Context ctx, @NonNull JsonRpcRequest request, @NonNull JsonRpcChain chain) {
var method = Optional.ofNullable(request.getMethod()).orElse("unknown_method");
var span =
tracer
Expand Down
Loading
Loading