Skip to content

Commit 85bb2e1

Browse files
committed
feat: add duplicateStructuredContent option to server builders
Add a configurable flag to control whether structured content is automatically duplicated into text content for backwards compatibility. Defaults to true (preserving existing behavior). When set to false, the server skips the duplication, reducing response payload size for clients that fully support structuredContent. The flag is added to all four server builder specifications (async, sync, stateless async, stateless sync) and passed through to StructuredOutputCallToolHandler in both McpAsyncServer and McpStatelessAsyncServer. Closes #688
1 parent accba74 commit 85bb2e1

3 files changed

Lines changed: 141 additions & 27 deletions

File tree

mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ public class McpAsyncServer {
101101

102102
private final boolean validateToolInputs;
103103

104+
private final boolean duplicateStructuredContent;
105+
104106
private final McpSchema.ServerCapabilities serverCapabilities;
105107

106108
private final McpSchema.Implementation serverInfo;
@@ -133,20 +135,22 @@ public class McpAsyncServer {
133135
McpAsyncServer(McpServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper,
134136
McpServerFeatures.Async features, Duration requestTimeout,
135137
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator,
136-
boolean validateToolInputs) {
138+
boolean validateToolInputs, boolean duplicateStructuredContent) {
137139
this.mcpTransportProvider = mcpTransportProvider;
138140
this.jsonMapper = jsonMapper;
139141
this.serverInfo = features.serverInfo();
140142
this.serverCapabilities = features.serverCapabilities().mutate().logging().build();
141143
this.instructions = features.instructions();
142-
this.tools.addAll(withStructuredOutputHandling(jsonSchemaValidator, features.tools()));
144+
this.tools
145+
.addAll(withStructuredOutputHandling(jsonSchemaValidator, duplicateStructuredContent, features.tools()));
143146
this.resources.putAll(features.resources());
144147
this.resourceTemplates.putAll(features.resourceTemplates());
145148
this.prompts.putAll(features.prompts());
146149
this.completions.putAll(features.completions());
147150
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
148151
this.jsonSchemaValidator = jsonSchemaValidator;
149152
this.validateToolInputs = validateToolInputs;
153+
this.duplicateStructuredContent = duplicateStructuredContent;
150154

151155
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
152156
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
@@ -164,20 +168,22 @@ public class McpAsyncServer {
164168
McpAsyncServer(McpStreamableServerTransportProvider mcpTransportProvider, McpJsonMapper jsonMapper,
165169
McpServerFeatures.Async features, Duration requestTimeout,
166170
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator,
167-
boolean validateToolInputs) {
171+
boolean validateToolInputs, boolean duplicateStructuredContent) {
168172
this.mcpTransportProvider = mcpTransportProvider;
169173
this.jsonMapper = jsonMapper;
170174
this.serverInfo = features.serverInfo();
171175
this.serverCapabilities = features.serverCapabilities().mutate().logging().build();
172176
this.instructions = features.instructions();
173-
this.tools.addAll(withStructuredOutputHandling(jsonSchemaValidator, features.tools()));
177+
this.tools
178+
.addAll(withStructuredOutputHandling(jsonSchemaValidator, duplicateStructuredContent, features.tools()));
174179
this.resources.putAll(features.resources());
175180
this.resourceTemplates.putAll(features.resourceTemplates());
176181
this.prompts.putAll(features.prompts());
177182
this.completions.putAll(features.completions());
178183
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
179184
this.jsonSchemaValidator = jsonSchemaValidator;
180185
this.validateToolInputs = validateToolInputs;
186+
this.duplicateStructuredContent = duplicateStructuredContent;
181187

182188
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
183189
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
@@ -357,7 +363,8 @@ public Mono<Void> addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica
357363
return Mono.error(e);
358364
}
359365

360-
var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, toolSpecification);
366+
var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator,
367+
this.duplicateStructuredContent, toolSpecification);
361368

362369
return Mono.defer(() -> {
363370
// Remove tools with duplicate tool names first
@@ -384,15 +391,18 @@ private static class StructuredOutputCallToolHandler
384391

385392
private final Map<String, Object> outputSchema;
386393

394+
private final boolean duplicateStructuredContent;
395+
387396
public StructuredOutputCallToolHandler(JsonSchemaValidator jsonSchemaValidator,
388-
Map<String, Object> outputSchema,
397+
Map<String, Object> outputSchema, boolean duplicateStructuredContent,
389398
BiFunction<McpAsyncServerExchange, McpSchema.CallToolRequest, Mono<McpSchema.CallToolResult>> delegateHandler) {
390399

391400
Assert.notNull(jsonSchemaValidator, "JsonSchemaValidator must not be null");
392401
Assert.notNull(delegateHandler, "Delegate call tool result handler must not be null");
393402

394403
this.delegateCallToolResult = delegateHandler;
395404
this.outputSchema = outputSchema;
405+
this.duplicateStructuredContent = duplicateStructuredContent;
396406
this.jsonSchemaValidator = jsonSchemaValidator;
397407
}
398408

@@ -440,7 +450,7 @@ public Mono<CallToolResult> apply(McpAsyncServerExchange exchange, McpSchema.Cal
440450
.build();
441451
}
442452

443-
if (Utils.isEmpty(result.content())) {
453+
if (this.duplicateStructuredContent && Utils.isEmpty(result.content())) {
444454
// For backwards compatibility, a tool that returns structured
445455
// content SHOULD also return functionally equivalent unstructured
446456
// content. (For example, serialized JSON can be returned in a
@@ -461,17 +471,21 @@ public Mono<CallToolResult> apply(McpAsyncServerExchange exchange, McpSchema.Cal
461471
}
462472

463473
private static List<McpServerFeatures.AsyncToolSpecification> withStructuredOutputHandling(
464-
JsonSchemaValidator jsonSchemaValidator, List<McpServerFeatures.AsyncToolSpecification> tools) {
474+
JsonSchemaValidator jsonSchemaValidator, boolean duplicateStructuredContent,
475+
List<McpServerFeatures.AsyncToolSpecification> tools) {
465476

466477
if (Utils.isEmpty(tools)) {
467478
return tools;
468479
}
469480

470-
return tools.stream().map(tool -> withStructuredOutputHandling(jsonSchemaValidator, tool)).toList();
481+
return tools.stream()
482+
.map(tool -> withStructuredOutputHandling(jsonSchemaValidator, duplicateStructuredContent, tool))
483+
.toList();
471484
}
472485

473486
private static McpServerFeatures.AsyncToolSpecification withStructuredOutputHandling(
474-
JsonSchemaValidator jsonSchemaValidator, McpServerFeatures.AsyncToolSpecification toolSpecification) {
487+
JsonSchemaValidator jsonSchemaValidator, boolean duplicateStructuredContent,
488+
McpServerFeatures.AsyncToolSpecification toolSpecification) {
475489

476490
if (toolSpecification.callHandler() instanceof StructuredOutputCallToolHandler) {
477491
// If the tool is already wrapped, return it as is
@@ -485,8 +499,9 @@ private static McpServerFeatures.AsyncToolSpecification withStructuredOutputHand
485499

486500
return McpServerFeatures.AsyncToolSpecification.builder()
487501
.tool(toolSpecification.tool())
488-
.callHandler(new StructuredOutputCallToolHandler(jsonSchemaValidator,
489-
toolSpecification.tool().outputSchema(), toolSpecification.callHandler()))
502+
.callHandler(
503+
new StructuredOutputCallToolHandler(jsonSchemaValidator, toolSpecification.tool().outputSchema(),
504+
duplicateStructuredContent, toolSpecification.callHandler()))
490505
.build();
491506
}
492507

mcp-core/src/main/java/io/modelcontextprotocol/server/McpServer.java

Lines changed: 94 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,8 @@ public McpAsyncServer build() {
246246
validateAsyncToolSchemas(jsonSchemaValidator, this.tools);
247247

248248
return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
249-
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
249+
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs,
250+
duplicateStructuredContent);
250251
}
251252

252253
}
@@ -275,7 +276,8 @@ public McpAsyncServer build() {
275276
validateAsyncToolSchemas(jsonSchemaValidator, this.tools);
276277

277278
return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
278-
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
279+
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs,
280+
duplicateStructuredContent);
279281
}
280282

281283
}
@@ -301,6 +303,8 @@ abstract class AsyncSpecification<S extends AsyncSpecification<S>> {
301303

302304
boolean validateToolInputs = true;
303305

306+
boolean duplicateStructuredContent = true;
307+
304308
/**
305309
* The Model Context Protocol (MCP) allows servers to expose tools that can be
306310
* invoked by language models. Tools enable models to interact with external
@@ -440,6 +444,25 @@ public AsyncSpecification<S> validateToolInputs(boolean validate) {
440444
return this;
441445
}
442446

447+
/**
448+
* Sets whether to automatically duplicate structured content into text content
449+
* for backwards compatibility. When enabled (the default), tools that return
450+
* structured content will also have the serialized JSON added as a
451+
* {@link McpSchema.TextContent} block in the {@code content} field, as
452+
* recommended by the MCP specification. Disabling this can reduce response
453+
* payload size when clients fully support {@code structuredContent}.
454+
* @param duplicate true to duplicate structured content into text content
455+
* (default), false to skip duplication
456+
* @return This builder instance for method chaining
457+
* @see <a href=
458+
* "https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content">MCP
459+
* Structured Content</a>
460+
*/
461+
public AsyncSpecification<S> duplicateStructuredContent(boolean duplicate) {
462+
this.duplicateStructuredContent = duplicate;
463+
return this;
464+
}
465+
443466
/**
444467
* Sets the server capabilities that will be advertised to clients during
445468
* connection initialization. Capabilities define what features the server
@@ -841,7 +864,7 @@ public McpSyncServer build() {
841864

842865
var asyncServer = new McpAsyncServer(transportProvider,
843866
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, requestTimeout,
844-
uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
867+
uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs, duplicateStructuredContent);
845868
return new McpSyncServer(asyncServer, this.immediateExecution);
846869
}
847870

@@ -875,7 +898,8 @@ public McpSyncServer build() {
875898

876899
var asyncServer = new McpAsyncServer(transportProvider,
877900
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, this.requestTimeout,
878-
this.uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
901+
this.uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs,
902+
duplicateStructuredContent);
879903
return new McpSyncServer(asyncServer, this.immediateExecution);
880904
}
881905

@@ -900,6 +924,8 @@ abstract class SyncSpecification<S extends SyncSpecification<S>> {
900924

901925
boolean validateToolInputs = true;
902926

927+
boolean duplicateStructuredContent = true;
928+
903929
/**
904930
* The Model Context Protocol (MCP) allows servers to expose tools that can be
905931
* invoked by language models. Tools enable models to interact with external
@@ -1043,6 +1069,25 @@ public SyncSpecification<S> validateToolInputs(boolean validate) {
10431069
return this;
10441070
}
10451071

1072+
/**
1073+
* Sets whether to automatically duplicate structured content into text content
1074+
* for backwards compatibility. When enabled (the default), tools that return
1075+
* structured content will also have the serialized JSON added as a
1076+
* {@link McpSchema.TextContent} block in the {@code content} field, as
1077+
* recommended by the MCP specification. Disabling this can reduce response
1078+
* payload size when clients fully support {@code structuredContent}.
1079+
* @param duplicate true to duplicate structured content into text content
1080+
* (default), false to skip duplication
1081+
* @return This builder instance for method chaining
1082+
* @see <a href=
1083+
* "https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content">MCP
1084+
* Structured Content</a>
1085+
*/
1086+
public SyncSpecification<S> duplicateStructuredContent(boolean duplicate) {
1087+
this.duplicateStructuredContent = duplicate;
1088+
return this;
1089+
}
1090+
10461091
/**
10471092
* Sets the server capabilities that will be advertised to clients during
10481093
* connection initialization. Capabilities define what features the server
@@ -1442,6 +1487,8 @@ class StatelessAsyncSpecification {
14421487

14431488
boolean validateToolInputs = true;
14441489

1490+
boolean duplicateStructuredContent = true;
1491+
14451492
/**
14461493
* The Model Context Protocol (MCP) allows servers to expose tools that can be
14471494
* invoked by language models. Tools enable models to interact with external
@@ -1582,6 +1629,25 @@ public StatelessAsyncSpecification validateToolInputs(boolean validate) {
15821629
return this;
15831630
}
15841631

1632+
/**
1633+
* Sets whether to automatically duplicate structured content into text content
1634+
* for backwards compatibility. When enabled (the default), tools that return
1635+
* structured content will also have the serialized JSON added as a
1636+
* {@link McpSchema.TextContent} block in the {@code content} field, as
1637+
* recommended by the MCP specification. Disabling this can reduce response
1638+
* payload size when clients fully support {@code structuredContent}.
1639+
* @param duplicate true to duplicate structured content into text content
1640+
* (default), false to skip duplication
1641+
* @return This builder instance for method chaining
1642+
* @see <a href=
1643+
* "https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content">MCP
1644+
* Structured Content</a>
1645+
*/
1646+
public StatelessAsyncSpecification duplicateStructuredContent(boolean duplicate) {
1647+
this.duplicateStructuredContent = duplicate;
1648+
return this;
1649+
}
1650+
15851651
/**
15861652
* Sets the server capabilities that will be advertised to clients during
15871653
* connection initialization. Capabilities define what features the server
@@ -1915,7 +1981,8 @@ public McpStatelessAsyncServer build() {
19151981
validateStatelessAsyncToolSchemas(jsonSchemaValidator, this.tools);
19161982

19171983
return new McpStatelessAsyncServer(transport, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
1918-
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
1984+
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs,
1985+
duplicateStructuredContent);
19191986
}
19201987

19211988
}
@@ -1942,6 +2009,8 @@ class StatelessSyncSpecification {
19422009

19432010
boolean validateToolInputs = true;
19442011

2012+
boolean duplicateStructuredContent = true;
2013+
19452014
/**
19462015
* The Model Context Protocol (MCP) allows servers to expose tools that can be
19472016
* invoked by language models. Tools enable models to interact with external
@@ -2082,6 +2151,25 @@ public StatelessSyncSpecification validateToolInputs(boolean validate) {
20822151
return this;
20832152
}
20842153

2154+
/**
2155+
* Sets whether to automatically duplicate structured content into text content
2156+
* for backwards compatibility. When enabled (the default), tools that return
2157+
* structured content will also have the serialized JSON added as a
2158+
* {@link McpSchema.TextContent} block in the {@code content} field, as
2159+
* recommended by the MCP specification. Disabling this can reduce response
2160+
* payload size when clients fully support {@code structuredContent}.
2161+
* @param duplicate true to duplicate structured content into text content
2162+
* (default), false to skip duplication
2163+
* @return This builder instance for method chaining
2164+
* @see <a href=
2165+
* "https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content">MCP
2166+
* Structured Content</a>
2167+
*/
2168+
public StatelessSyncSpecification duplicateStructuredContent(boolean duplicate) {
2169+
this.duplicateStructuredContent = duplicate;
2170+
return this;
2171+
}
2172+
20852173
/**
20862174
* Sets the server capabilities that will be advertised to clients during
20872175
* connection initialization. Capabilities define what features the server
@@ -2433,7 +2521,7 @@ public McpStatelessSyncServer build() {
24332521

24342522
var asyncServer = new McpStatelessAsyncServer(transport,
24352523
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, requestTimeout,
2436-
uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
2524+
uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs, duplicateStructuredContent);
24372525
return new McpStatelessSyncServer(asyncServer, this.immediateExecution);
24382526
}
24392527

0 commit comments

Comments
 (0)