Skip to content

Commit a4a115b

Browse files
committed
feat(mcp): modularize Jackson and victools support for v2 and v3
This commit completely decouples the MCP core and APT code generator from Jackson and victools, resolving classpath conflicts between Jackson 2 and Jackson 3. It introduces dedicated integration modules to cleanly handle JSON serialization and JSON Schema generation based on the user's runtime environment. Details: * Refactored the APT generator (`McpRoute`, `McpRouter`) to build schemas using standard `java.util.LinkedHashMap` and `java.util.ArrayList` instead of Jackson's `ObjectNode` and `ArrayNode`. * Removed hardcoded `victools` configuration from the generated `install` methods. The generated router now dynamically requires the `SchemaGenerator` from the Jooby application registry. * Delegated the final schema map conversion strictly to the abstracted `McpJsonMapper` interface. * Introduced the `jooby-mcp-jackson2` module to provide bindings for Jackson 2 and `victools` 4.x. * Introduced the `jooby-mcp-jackson3` module to provide bindings for Jackson 3 and `victools` 5.x.
1 parent bc9714c commit a4a115b

16 files changed

Lines changed: 332 additions & 199 deletions

File tree

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

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -413,23 +413,28 @@ private List<String> generateToolDefinition(boolean kt) {
413413
indent(4),
414414
"private fun ",
415415
getMethodName(),
416-
"ToolSpec(mapper: tools.jackson.databind.ObjectMapper, schemaGenerator:"
416+
"ToolSpec(schemaGenerator:"
417417
+ " com.github.victools.jsonschema.generator.SchemaGenerator):"
418418
+ " io.modelcontextprotocol.spec.McpSchema.Tool {"));
419-
buffer.add(statement(indent(6), "val schema = mapper.createObjectNode()"));
419+
buffer.add(statement(indent(6), "val schema = java.util.LinkedHashMap<String, Any>()"));
420420
buffer.add(statement(indent(6), "schema.put(", string("type"), ", ", string("object"), ")"));
421-
buffer.add(statement(indent(6), "val props = schema.putObject(", string("properties"), ")"));
422-
buffer.add(statement(indent(6), "val req = schema.putArray(", string("required"), ")"));
421+
buffer.add(statement(indent(6), "val props = java.util.LinkedHashMap<String, Any>()"));
422+
buffer.add(statement(indent(6), "schema.put(", string("properties"), ", props)"));
423+
buffer.add(statement(indent(6), "val req = java.util.ArrayList<String>()"));
424+
buffer.add(statement(indent(6), "schema.put(", string("required"), ", req)"));
423425
} else {
424426
buffer.add(
425427
statement(
426428
indent(4),
427429
"private io.modelcontextprotocol.spec.McpSchema.Tool ",
428430
getMethodName(),
429-
"ToolSpec(tools.jackson.databind.ObjectMapper mapper,"
430-
+ " com.github.victools.jsonschema.generator.SchemaGenerator"
431+
"ToolSpec(com.github.victools.jsonschema.generator.SchemaGenerator"
431432
+ " schemaGenerator) {"));
432-
buffer.add(statement(indent(6), "var schema = mapper.createObjectNode()", semicolon(kt)));
433+
buffer.add(
434+
statement(
435+
indent(6),
436+
"var schema = new java.util.LinkedHashMap<String, Object>()",
437+
semicolon(kt)));
433438
buffer.add(
434439
statement(
435440
indent(6),
@@ -442,13 +447,13 @@ private List<String> generateToolDefinition(boolean kt) {
442447
buffer.add(
443448
statement(
444449
indent(6),
445-
"var props = schema.putObject(",
446-
string("properties"),
447-
")",
450+
"var props = new java.util.LinkedHashMap<String, Object>()",
448451
semicolon(kt)));
449452
buffer.add(
450-
statement(
451-
indent(6), "var req = schema.putArray(", string("required"), ")", semicolon(kt)));
453+
statement(indent(6), "schema.put(", string("properties"), ", props)", semicolon(kt)));
454+
buffer.add(
455+
statement(indent(6), "var req = new java.util.ArrayList<String>()", semicolon(kt)));
456+
buffer.add(statement(indent(6), "schema.put(", string("required"), ", req)", semicolon(kt)));
452457
}
453458

454459
// --- PARAMETER SCHEMA GENERATION ---
@@ -487,14 +492,8 @@ private List<String> generateToolDefinition(boolean kt) {
487492
")"));
488493
}
489494

490-
buffer.add(
491-
statement(
492-
indent(6),
493-
"props.set<tools.jackson.databind.JsonNode>(",
494-
string(mcpName),
495-
", schema_",
496-
mcpName,
497-
")"));
495+
// Switched from .set() to .put() for standard Map
496+
buffer.add(statement(indent(6), "props.put(", string(mcpName), ", schema_", mcpName, ")"));
498497

499498
if (!param.isNullable(kt)) {
500499
buffer.add(statement(indent(6), "req.add(", string(mcpName), ")"));
@@ -524,10 +523,11 @@ private List<String> generateToolDefinition(boolean kt) {
524523
semicolon(kt)));
525524
}
526525

526+
// Switched from .set() to .put() for standard Map
527527
buffer.add(
528528
statement(
529529
indent(6),
530-
"props.set(",
530+
"props.put(",
531531
string(mcpName),
532532
", schema_",
533533
mcpName,
@@ -556,14 +556,15 @@ private List<String> generateToolDefinition(boolean kt) {
556556
"Node = schemaGenerator.generateSchema(",
557557
returnTypeStr,
558558
"::class.java)"));
559+
// Use this.json to convert the output schema
559560
buffer.add(
560561
statement(
561562
indent(6),
562563
"val ",
563564
outputSchemaArg,
564-
" = mapper.convertValue(",
565+
" = this.json.convertValue(",
565566
outputSchemaArg,
566-
"Node, Map::class.java) as Map<String, Any>"));
567+
"Node, java.util.Map::class.java) as java.util.Map<String, Any>"));
567568
} else {
568569
buffer.add(
569570
statement(
@@ -574,12 +575,13 @@ private List<String> generateToolDefinition(boolean kt) {
574575
returnTypeStr,
575576
".class)",
576577
semicolon(kt)));
578+
// Use this.json to convert the output schema
577579
buffer.add(
578580
statement(
579581
indent(6),
580582
"var ",
581583
outputSchemaArg,
582-
" = mapper.convertValue(",
584+
" = this.json.convertValue(",
583585
outputSchemaArg,
584586
"Node, java.util.Map.class)",
585587
semicolon(kt)));
@@ -620,7 +622,8 @@ private List<String> generateToolDefinition(boolean kt) {
620622
titleArg,
621623
", ",
622624
string(description),
623-
", mapper.treeToValue(schema,"
625+
// Use this.json to convert the main schema map into JsonSchema
626+
", this.json.convertValue(schema,"
624627
+ " io.modelcontextprotocol.spec.McpSchema.JsonSchema::class.java), ",
625628
outputSchemaArg,
626629
", ",
@@ -657,7 +660,8 @@ private List<String> generateToolDefinition(boolean kt) {
657660
titleArg,
658661
", ",
659662
string(description),
660-
", mapper.treeToValue(schema,"
663+
// Use this.json to convert the main schema map into JsonSchema
664+
", this.json.convertValue(schema,"
661665
+ " io.modelcontextprotocol.spec.McpSchema.JsonSchema.class), ",
662666
outputSchemaArg,
663667
", ",

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

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -365,23 +365,14 @@ public String toSourceCode(boolean kt) throws IOException {
365365
statement(
366366
indent(6),
367367
"this.json ="
368-
+ " app.services.require(io.modelcontextprotocol.json.McpJsonMapper::class.java)"));
369-
buffer.append(
370-
statement(
371-
indent(6),
372-
"val mapper = app.require(tools.jackson.databind.ObjectMapper::class.java)"));
368+
+ " app.require(io.modelcontextprotocol.json.McpJsonMapper::class.java)"));
369+
373370
if (!tools.isEmpty()) {
374-
buffer.append(
375-
statement(
376-
indent(6),
377-
"val configBuilder ="
378-
+ " com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder(com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12,"
379-
+ " com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON)"));
380371
buffer.append(
381372
statement(
382373
indent(6),
383374
"val schemaGenerator ="
384-
+ " com.github.victools.jsonschema.generator.SchemaGenerator(configBuilder.build())"));
375+
+ " app.require(com.github.victools.jsonschema.generator.SchemaGenerator::class.java)"));
385376
}
386377
} else {
387378
buffer.append(statement(indent(4), "@Override"));
@@ -394,27 +385,15 @@ public String toSourceCode(boolean kt) throws IOException {
394385
buffer.append(
395386
statement(
396387
indent(6),
397-
"this.json ="
398-
+ " app.getServices().require(io.modelcontextprotocol.json.McpJsonMapper.class)",
399-
semicolon(kt)));
400-
buffer.append(
401-
statement(
402-
indent(6),
403-
"var mapper = app.require(tools.jackson.databind.ObjectMapper.class)",
388+
"this.json =" + " app.require(io.modelcontextprotocol.json.McpJsonMapper.class)",
404389
semicolon(kt)));
390+
405391
if (!tools.isEmpty()) {
406392
buffer.append(
407393
statement(
408394
indent(6),
409-
"var configBuilder = new"
410-
+ " com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder(com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12,"
411-
+ " com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON)",
412-
semicolon(kt)));
413-
buffer.append(
414-
statement(
415-
indent(6),
416-
"var schemaGenerator = new"
417-
+ " com.github.victools.jsonschema.generator.SchemaGenerator(configBuilder.build())",
395+
"var schemaGenerator ="
396+
+ " app.require(com.github.victools.jsonschema.generator.SchemaGenerator.class)",
418397
semicolon(kt)));
419398
}
420399
}
@@ -438,7 +417,8 @@ public String toSourceCode(boolean kt) throws IOException {
438417
: "(exchange, req) -> this." + methodName + "(exchange, null, req)");
439418

440419
if (route.isMcpTool()) {
441-
var defArgs = "mapper, schemaGenerator";
420+
// Removed "mapper" from defArgs
421+
var defArgs = "schemaGenerator";
442422
if (kt) {
443423
buffer.append(
444424
statement(

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

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public ExampleServerMcp_(ExampleServer instance) {
1818
}
1919

2020
public ExampleServerMcp_(io.jooby.SneakyThrows.Supplier<ExampleServer> provider) {
21-
setup(ctx -> (ExampleServer) provider.get());
21+
setup(ctx -> provider.get());
2222
}
2323

2424
public ExampleServerMcp_(
@@ -89,18 +89,12 @@ public String serverKey() {
8989
public void install(io.jooby.Jooby app, io.modelcontextprotocol.server.McpSyncServer server)
9090
throws Exception {
9191
this.json = app.getServices().require(io.modelcontextprotocol.json.McpJsonMapper.class);
92-
var mapper = app.require(tools.jackson.databind.ObjectMapper.class);
93-
var configBuilder =
94-
new com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder(
95-
com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12,
96-
com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON);
9792
var schemaGenerator =
98-
new com.github.victools.jsonschema.generator.SchemaGenerator(configBuilder.build());
93+
app.require(com.github.victools.jsonschema.generator.SchemaGenerator.class);
9994

10095
server.addTool(
10196
new io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification(
102-
addToolSpec(mapper, schemaGenerator),
103-
(exchange, req) -> this.add(exchange, null, req)));
97+
addToolSpec(schemaGenerator), (exchange, req) -> this.add(exchange, null, req)));
10498
server.addPrompt(
10599
new io.modelcontextprotocol.server.McpServerFeatures.SyncPromptSpecification(
106100
reviewCodePromptSpec(), (exchange, req) -> this.reviewCode(exchange, null, req)));
@@ -118,17 +112,12 @@ public void install(
118112
io.jooby.Jooby app, io.modelcontextprotocol.server.McpStatelessSyncServer server)
119113
throws Exception {
120114
this.json = app.getServices().require(io.modelcontextprotocol.json.McpJsonMapper.class);
121-
var mapper = app.require(tools.jackson.databind.ObjectMapper.class);
122-
var configBuilder =
123-
new com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder(
124-
com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12,
125-
com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON);
126115
var schemaGenerator =
127-
new com.github.victools.jsonschema.generator.SchemaGenerator(configBuilder.build());
116+
app.require(com.github.victools.jsonschema.generator.SchemaGenerator.class);
128117

129118
server.addTool(
130119
new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncToolSpecification(
131-
addToolSpec(mapper, schemaGenerator), (ctx, req) -> this.add(null, ctx, req)));
120+
addToolSpec(schemaGenerator), (ctx, req) -> this.add(null, ctx, req)));
132121
server.addPrompt(
133122
new io.modelcontextprotocol.server.McpStatelessServerFeatures.SyncPromptSpecification(
134123
reviewCodePromptSpec(), (ctx, req) -> this.reviewCode(null, ctx, req)));
@@ -143,19 +132,20 @@ public void install(
143132
}
144133

145134
private io.modelcontextprotocol.spec.McpSchema.Tool addToolSpec(
146-
tools.jackson.databind.ObjectMapper mapper,
147135
com.github.victools.jsonschema.generator.SchemaGenerator schemaGenerator) {
148-
var schema = mapper.createObjectNode();
136+
var schema = new java.util.LinkedHashMap<String, Object>();
149137
schema.put("type", "object");
150-
var props = schema.putObject("properties");
151-
var req = schema.putArray("required");
138+
var props = new java.util.LinkedHashMap<String, Object>();
139+
schema.put("properties", props);
140+
var req = new java.util.ArrayList<String>();
141+
schema.put("required", req);
152142
var schema_a = schemaGenerator.generateSchema(int.class);
153143
schema_a.put("description", "1st number");
154-
props.set("a", schema_a);
144+
props.put("a", schema_a);
155145
req.add("a");
156146
var schema_b = schemaGenerator.generateSchema(int.class);
157147
schema_b.put("description", "2nd number");
158-
props.set("b", schema_b);
148+
props.put("b", schema_b);
159149
req.add("b");
160150
var annotations =
161151
new io.modelcontextprotocol.spec.McpSchema.ToolAnnotations(
@@ -164,7 +154,7 @@ private io.modelcontextprotocol.spec.McpSchema.Tool addToolSpec(
164154
"calculator",
165155
"Add two numbers.",
166156
"A simple calculator.",
167-
mapper.treeToValue(schema, io.modelcontextprotocol.spec.McpSchema.JsonSchema.class),
157+
this.json.convertValue(schema, io.modelcontextprotocol.spec.McpSchema.JsonSchema.class),
168158
null,
169159
annotations,
170160
null);

0 commit comments

Comments
 (0)