Skip to content

Commit 9279688

Browse files
committed
feat(mcp): implement zero-reflection APT dispatcher for MCP servers
Introduces compile-time annotation processing for the Model Context Protocol (MCP), generating highly optimized, reflection-free dispatchers that bridge untyped JSON-RPC arguments to strongly-typed Jooby controllers. Key implementations: - Add `@McpServer`, `@McpTool`, `@McpPrompt`, `@McpResource`, and `@McpCompletion` annotations for defining MCP capabilities. - Add `@McpParam` to customize schema parameter names and provide LLM descriptions (with planned Javadoc fallback). - Update `MvcRoute` to classify MCP components during construction and isolate them into a dedicated `mcpRoutes` list in `MvcRouter` to prevent REST generator conflicts. - Define `McpService` interface enforcing a strict execution contract with `McpSyncServerExchange` for Context resolution. - Implement `getMcpSourceCode` in `MvcRouter` to generate `*McpServer_` classes featuring: - Zero-reflection `switch/case` routing for tools, prompts, resources, templates, and completions. - Safe extraction and primitive casting from untyped argument maps. - Strict Jackson 3 (`tools.jackson`) registry lookup for complex POJO conversions. - Native Jooby dependency injection support using the existing `constructors()` AST utility. - Interface compliance by throwing `UnsupportedOperationException` for unused MCP capabilities.
1 parent 23dbed6 commit 9279688

21 files changed

Lines changed: 911 additions & 40 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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.annotation;
7+
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/** Marks a method as an MCP Completion provider. */
14+
@Target(ElementType.METHOD)
15+
@Retention(RetentionPolicy.RUNTIME)
16+
public @interface McpCompletion {
17+
/**
18+
* The identifier of the reference. This is either the Prompt name (e.g., "code_review") or the
19+
* Resource Template URI (e.g., "file:///project/{name}").
20+
*/
21+
String ref();
22+
23+
/** The name of the argument or template variable being completed. */
24+
String arg();
25+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.annotation;
7+
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/** Provides metadata for an MCP Tool or Prompt parameter. */
14+
@Target(ElementType.PARAMETER)
15+
@Retention(RetentionPolicy.RUNTIME)
16+
public @interface McpParam {
17+
/**
18+
* The name of the parameter in the MCP schema. If empty, the Java variable name is used.
19+
*
20+
* @return Parameter name.
21+
*/
22+
String name() default "";
23+
24+
/**
25+
* A description of the parameter for the LLM. If empty, it falls back to the @param tag in the
26+
* method's Javadoc.
27+
*
28+
* @return Parameter description.
29+
*/
30+
String description() default "";
31+
32+
/**
33+
* Whether this parameter is required.
34+
*
35+
* @return True if required, false otherwise.
36+
*/
37+
boolean required() default true;
38+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.annotation;
7+
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/** Exposes a method as an MCP Prompt. */
14+
@Target(ElementType.METHOD)
15+
@Retention(RetentionPolicy.RUNTIME)
16+
public @interface McpPrompt {
17+
/**
18+
* The name of the prompt. If empty, the method name is used.
19+
*
20+
* @return Prompt name.
21+
*/
22+
String name() default "";
23+
24+
/**
25+
* A description of what the prompt provides.
26+
*
27+
* @return Prompt description.
28+
*/
29+
String description() default "";
30+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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.annotation;
7+
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/**
14+
* Exposes a method as an MCP Resource or Resource Template.
15+
*
16+
* <p>If the URI contains path variables (e.g., "file:///{dir}/{filename}"), it will be treated as a
17+
* ResourceTemplate.
18+
*/
19+
@Target(ElementType.METHOD)
20+
@Retention(RetentionPolicy.RUNTIME)
21+
public @interface McpResource {
22+
/**
23+
* The exact URI or URI template for the resource.
24+
*
25+
* @return The resource URI.
26+
*/
27+
String value();
28+
29+
/**
30+
* The name of the resource.
31+
*
32+
* @return Resource name.
33+
*/
34+
String name() default "";
35+
36+
/**
37+
* A description of the resource.
38+
*
39+
* @return Resource description.
40+
*/
41+
String description() default "";
42+
43+
/**
44+
* The MIME type of the resource (e.g., "text/plain", "application/json"). * @return The MIME
45+
* type.
46+
*/
47+
String mimeType() default "";
48+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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.annotation;
7+
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/** Marks a class as an MCP (Model Context Protocol) Server. */
14+
@Target(ElementType.TYPE)
15+
@Retention(RetentionPolicy.RUNTIME)
16+
public @interface McpServer {
17+
/**
18+
* The server key used to look up configuration in application.conf. Defaults to "default".
19+
*
20+
* @return The server configuration key.
21+
*/
22+
String value() default "default";
23+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.annotation;
7+
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
/** Exposes a method as an MCP Tool. */
14+
@Target(ElementType.METHOD)
15+
@Retention(RetentionPolicy.RUNTIME)
16+
public @interface McpTool {
17+
/**
18+
* The name of the tool. If empty, the method name is used.
19+
*
20+
* @return Tool name.
21+
*/
22+
String name() default "";
23+
24+
/**
25+
* A description of what the tool does. Highly recommended for LLM usage.
26+
*
27+
* @return Tool description.
28+
*/
29+
String description() default "";
30+
}

modules/jooby-apt/pom.xml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@
2525
<scope>test</scope>
2626
</dependency>
2727

28+
<dependency>
29+
<groupId>io.jooby</groupId>
30+
<artifactId>jooby-mcp</artifactId>
31+
<version>${jooby.version}</version>
32+
<scope>test</scope>
33+
</dependency>
34+
2835
<dependency>
2936
<groupId>io.jooby</groupId>
3037
<artifactId>jooby-jackson3</artifactId>
@@ -44,7 +51,6 @@
4451
<scope>test</scope>
4552
</dependency>
4653

47-
<!-- Test dependencies -->
4854
<dependency>
4955
<groupId>com.google.testing.compile</groupId>
5056
<artifactId>compile-testing</artifactId>
@@ -58,6 +64,12 @@
5864
<scope>test</scope>
5965
</dependency>
6066

67+
<dependency>
68+
<groupId>io.modelcontextprotocol.sdk</groupId>
69+
<artifactId>mcp-core</artifactId>
70+
<scope>test</scope>
71+
</dependency>
72+
6173
<dependency>
6274
<groupId>com.google.truth</groupId>
6375
<artifactId>truth</artifactId>

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
ROUTER_SUFFIX,
3939
SKIP_ATTRIBUTE_ANNOTATIONS
4040
})
41-
@SupportedSourceVersion(SourceVersion.RELEASE_17)
41+
@SupportedSourceVersion(SourceVersion.RELEASE_21)
4242
public class JoobyProcessor extends AbstractProcessor {
4343
/** Available options. */
4444
public interface Options {
@@ -193,6 +193,24 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
193193
router.getTargetType());
194194
}
195195
}
196+
// 3. Generate MCP Server File (e.g., WeatherServerMcp_.java)
197+
if (router.hasMcpRoutes()) {
198+
var mcpSource = router.getMcpSourceCode(null);
199+
if (mcpSource != null) {
200+
var sourceLocation = router.getMcpGeneratedFilename();
201+
var generatedType = router.getMcpGeneratedType();
202+
onGeneratedSource(generatedType, toJavaFileObject(sourceLocation, mcpSource));
203+
204+
context.debug("mcp router %s: %s", router.getTargetType(), generatedType);
205+
206+
writeSource(
207+
router.isKt(),
208+
generatedType,
209+
sourceLocation,
210+
mcpSource,
211+
router.getTargetType());
212+
}
213+
}
196214

197215
} catch (IOException cause) {
198216
throw new RuntimeException("Unable to generate: " + router.getTargetType(), cause);
@@ -401,6 +419,11 @@ public Set<String> getSupportedAnnotationTypes() {
401419
var supportedTypes = new HashSet<String>();
402420
supportedTypes.addAll(HttpPath.PATH.getAnnotations());
403421
supportedTypes.addAll(HttpMethod.annotations());
422+
// Add MCP Annotations
423+
supportedTypes.add("io.jooby.annotation.McpTool");
424+
supportedTypes.add("io.jooby.annotation.McpPrompt");
425+
supportedTypes.add("io.jooby.annotation.McpResource");
426+
supportedTypes.add("io.jooby.annotation.McpServer");
404427
return supportedTypes;
405428
}
406429

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,13 @@ public enum HttpMethod implements AnnotationSupport {
3434
"io.jooby.annotation.Trpc",
3535
"io.jooby.annotation.Trpc.Mutation",
3636
"io.jooby.annotation.Trpc.Query")),
37-
JSON_RPC(List.of("io.jooby.annotation.JsonRpc"));
37+
JSON_RPC(List.of("io.jooby.annotation.JsonRpc")),
38+
MCP(
39+
List.of(
40+
"io.jooby.annotation.McpTool",
41+
"io.jooby.annotation.McpPrompt",
42+
"io.jooby.annotation.McpResource",
43+
"io.jooby.annotation.McpServer"));
3844

3945
private final List<String> annotations;
4046

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@ public String getName() {
4141
return parameter.getSimpleName().toString();
4242
}
4343

44+
public String getMcpName() {
45+
var annotation = annotations.get("io.jooby.annotation.McpParam");
46+
if (annotation != null) {
47+
var customName =
48+
io.jooby.internal.apt.AnnotationSupport.findAnnotationValue(annotation, "name"::equals)
49+
.stream()
50+
.findFirst()
51+
.orElse("");
52+
53+
if (!customName.isEmpty()) {
54+
return customName;
55+
}
56+
}
57+
// Fallback to the actual Java parameter name
58+
return getName();
59+
}
60+
4461
public String generateMapping(boolean kt) {
4562
var strategy =
4663
annotations.entrySet().stream()

0 commit comments

Comments
 (0)