Skip to content

Commit 09e91f2

Browse files
authored
Merge branch 'main' into issue_612_refactor
2 parents a17175d + e6045f7 commit 09e91f2

23 files changed

+484
-282
lines changed

mcp-core/src/main/java/io/modelcontextprotocol/client/LifecycleInitializer.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,9 @@ public <T> Mono<T> withInitialization(String actionName, Function<Initialization
288288
this.initializationRef.compareAndSet(newInit, null);
289289
return Mono.error(new RuntimeException("Client failed to initialize " + actionName, ex));
290290
})
291-
.flatMap(operation);
291+
.flatMap(res -> operation.apply(res)
292+
.contextWrite(c -> c.put(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
293+
res.initializeResult().protocolVersion())));
292294
});
293295
}
294296

@@ -320,6 +322,8 @@ private Mono<McpSchema.InitializeResult> doInitialize(DefaultInitialization init
320322
}
321323

322324
return mcpClientSession.sendNotification(McpSchema.METHOD_NOTIFICATION_INITIALIZED, null)
325+
.contextWrite(
326+
c -> c.put(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION, initializeResult.protocolVersion()))
323327
.thenReturn(initializeResult);
324328
}).flatMap(initializeResult -> {
325329
initialization.cacheResult(initializeResult);

mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ public class McpAsyncClient {
107107
public static final TypeRef<McpSchema.ProgressNotification> PROGRESS_NOTIFICATION_TYPE_REF = new TypeRef<>() {
108108
};
109109

110+
public static final String NEGOTIATED_PROTOCOL_VERSION = "io.modelcontextprotocol.client.negotiated-protocol-version";
111+
110112
/**
111113
* Client capabilities.
112114
*/

mcp-core/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@
2020
import java.util.function.Consumer;
2121
import java.util.function.Function;
2222

23-
import org.reactivestreams.Publisher;
24-
import org.slf4j.Logger;
25-
import org.slf4j.LoggerFactory;
26-
23+
import io.modelcontextprotocol.client.McpAsyncClient;
2724
import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent;
2825
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
2926
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
@@ -195,7 +192,9 @@ private Publisher<Void> createDelete(String sessionId) {
195192
.uri(uri)
196193
.header("Cache-Control", "no-cache")
197194
.header(HttpHeaders.MCP_SESSION_ID, sessionId)
198-
.header(HttpHeaders.PROTOCOL_VERSION, this.latestSupportedProtocolVersion)
195+
.header(HttpHeaders.PROTOCOL_VERSION,
196+
ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
197+
this.latestSupportedProtocolVersion))
199198
.DELETE();
200199
var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
201200
return Mono.from(this.httpRequestCustomizer.customize(builder, "DELETE", uri, null, transportContext));
@@ -266,7 +265,9 @@ private Mono<Disposable> reconnect(McpTransportStream<Disposable> stream) {
266265
var builder = requestBuilder.uri(uri)
267266
.header(HttpHeaders.ACCEPT, TEXT_EVENT_STREAM)
268267
.header("Cache-Control", "no-cache")
269-
.header(HttpHeaders.PROTOCOL_VERSION, this.latestSupportedProtocolVersion)
268+
.header(HttpHeaders.PROTOCOL_VERSION,
269+
connectionCtx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
270+
this.latestSupportedProtocolVersion))
270271
.GET();
271272
var transportContext = connectionCtx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
272273
return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, null, transportContext));
@@ -441,7 +442,9 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sentMessage) {
441442
.header(HttpHeaders.ACCEPT, APPLICATION_JSON + ", " + TEXT_EVENT_STREAM)
442443
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
443444
.header(HttpHeaders.CACHE_CONTROL, "no-cache")
444-
.header(HttpHeaders.PROTOCOL_VERSION, this.latestSupportedProtocolVersion)
445+
.header(HttpHeaders.PROTOCOL_VERSION,
446+
ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
447+
this.latestSupportedProtocolVersion))
445448
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
446449
var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
447450
return Mono

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -398,19 +398,23 @@ public Mono<CallToolResult> apply(McpAsyncServerExchange exchange, McpSchema.Cal
398398
// results that conform to this schema.
399399
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema
400400
if (result.structuredContent() == null) {
401-
logger.warn(
402-
"Response missing structured content which is expected when calling tool with non-empty outputSchema");
403-
return new CallToolResult(
404-
"Response missing structured content which is expected when calling tool with non-empty outputSchema",
405-
true);
401+
String content = "Response missing structured content which is expected when calling tool with non-empty outputSchema";
402+
logger.warn(content);
403+
return CallToolResult.builder()
404+
.content(List.of(new McpSchema.TextContent(content)))
405+
.isError(true)
406+
.build();
406407
}
407408

408409
// Validate the result against the output schema
409410
var validation = this.jsonSchemaValidator.validate(outputSchema, result.structuredContent());
410411

411412
if (!validation.valid()) {
412413
logger.warn("Tool call result validation failed: {}", validation.errorMessage());
413-
return new CallToolResult(validation.errorMessage(), true);
414+
return CallToolResult.builder()
415+
.content(List.of(new McpSchema.TextContent(validation.errorMessage())))
416+
.isError(true)
417+
.build();
414418
}
415419

416420
if (Utils.isEmpty(result.content())) {

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

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@
6666
* McpServer.sync(transportProvider)
6767
* .serverInfo("my-server", "1.0.0")
6868
* .tool(Tool.builder().name("calculator").title("Performs calculations").inputSchema(schema).build(),
69-
* (exchange, args) -> new CallToolResult("Result: " + calculate(args)))
69+
* (exchange, args) -> CallToolResult.builder()
70+
* .content(List.of(new McpSchema.TextContent("Result: " + calculate(args))))
71+
* .isError(false)
72+
* .build())
7073
* .build();
7174
* }</pre>
7275
*
@@ -75,7 +78,10 @@
7578
* .serverInfo("my-server", "1.0.0")
7679
* .tool(Tool.builder().name("calculator").title("Performs calculations").inputSchema(schema).build(),
7780
* (exchange, args) -> Mono.fromSupplier(() -> calculate(args))
78-
* .map(result -> new CallToolResult("Result: " + result)))
81+
* .map(result -> CallToolResult.builder()
82+
* .content(List.of(new McpSchema.TextContent("Result: " + result)))
83+
* .isError(false)
84+
* .build()))
7985
* .build();
8086
* }</pre>
8187
*
@@ -89,12 +95,18 @@
8995
* McpServerFeatures.AsyncToolSpecification.builder()
9096
* .tool(calculatorTool)
9197
* .callTool((exchange, args) -> Mono.fromSupplier(() -> calculate(args.arguments()))
92-
* .map(result -> new CallToolResult("Result: " + result))))
98+
* .map(result -> CallToolResult.builder()
99+
* .content(List.of(new McpSchema.TextContent("Result: " + result)))
100+
* .isError(false)
101+
* .build()))
93102
*. .build(),
94103
* McpServerFeatures.AsyncToolSpecification.builder()
95104
* .tool((weatherTool)
96105
* .callTool((exchange, args) -> Mono.fromSupplier(() -> getWeather(args.arguments()))
97-
* .map(result -> new CallToolResult("Weather: " + result))))
106+
* .map(result -> CallToolResult.builder()
107+
* .content(List.of(new McpSchema.TextContent("Weather: " + result)))
108+
* .isError(false)
109+
* .build()))
98110
* .build()
99111
* )
100112
* // Register resources
@@ -426,7 +438,10 @@ public AsyncSpecification<S> capabilities(McpSchema.ServerCapabilities serverCap
426438
* .tool(
427439
* Tool.builder().name("calculator").title("Performs calculations").inputSchema(schema).build(),
428440
* (exchange, args) -> Mono.fromSupplier(() -> calculate(args))
429-
* .map(result -> new CallToolResult("Result: " + result))
441+
* .map(result -> CallToolResult.builder()
442+
* .content(List.of(new McpSchema.TextContent("Result: " + result)))
443+
* .isError(false)
444+
* .build()))
430445
* )
431446
* }</pre>
432447
* @param tool The tool definition including name, description, and schema. Must
@@ -1023,7 +1038,10 @@ public SyncSpecification<S> capabilities(McpSchema.ServerCapabilities serverCapa
10231038
* Example usage: <pre>{@code
10241039
* .tool(
10251040
* Tool.builder().name("calculator").title("Performs calculations".inputSchema(schema).build(),
1026-
* (exchange, args) -> new CallToolResult("Result: " + calculate(args))
1041+
* (exchange, args) -> CallToolResult.builder()
1042+
* .content(List.of(new McpSchema.TextContent("Result: " + calculate(args))))
1043+
* .isError(false)
1044+
* .build())
10271045
* )
10281046
* }</pre>
10291047
* @param tool The tool definition including name, description, and schema. Must

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,13 @@ public static Builder builder() {
334334
*
335335
* <pre>{@code
336336
* new McpServerFeatures.AsyncResourceSpecification(
337-
* new Resource("docs", "Documentation files", "text/markdown"),
337+
* Resource.builder()
338+
* .uri("docs")
339+
* .name("Documentation files")
340+
* .title("Documentation files")
341+
* .mimeType("text/markdown")
342+
* .description("Markdown documentation files")
343+
* .build(),
338344
* (exchange, request) -> Mono.fromSupplier(() -> readFile(request.getPath()))
339345
* .map(ReadResourceResult::new))
340346
* }</pre>
@@ -508,7 +514,10 @@ static AsyncCompletionSpecification fromSync(SyncCompletionSpecification complet
508514
* .build()
509515
* .toolHandler((exchange, req) -> {
510516
* String expr = (String) req.arguments().get("expression");
511-
* return new CallToolResult("Result: " + evaluate(expr));
517+
* return CallToolResult.builder()
518+
* .content(List.of(new McpSchema.TextContent("Result: " + evaluate(expr))))
519+
* .isError(false)
520+
* .build();
512521
* }))
513522
* .build();
514523
* }</pre>
@@ -604,7 +613,13 @@ public static Builder builder() {
604613
*
605614
* <pre>{@code
606615
* new McpServerFeatures.SyncResourceSpecification(
607-
* new Resource("docs", "Documentation files", "text/markdown"),
616+
* Resource.builder()
617+
* .uri("docs")
618+
* .name("Documentation files")
619+
* .title("Documentation files")
620+
* .mimeType("text/markdown")
621+
* .description("Markdown documentation files")
622+
* .build(),
608623
* (exchange, request) -> {
609624
* String content = readFile(request.getPath());
610625
* return new ReadResourceResult(content);

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -277,19 +277,23 @@ public Mono<CallToolResult> apply(McpTransportContext transportContext, McpSchem
277277
// results that conform to this schema.
278278
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema
279279
if (result.structuredContent() == null) {
280-
logger.warn(
281-
"Response missing structured content which is expected when calling tool with non-empty outputSchema");
282-
return new CallToolResult(
283-
"Response missing structured content which is expected when calling tool with non-empty outputSchema",
284-
true);
280+
String content = "Response missing structured content which is expected when calling tool with non-empty outputSchema";
281+
logger.warn(content);
282+
return CallToolResult.builder()
283+
.content(List.of(new McpSchema.TextContent(content)))
284+
.isError(true)
285+
.build();
285286
}
286287

287288
// Validate the result against the output schema
288289
var validation = this.jsonSchemaValidator.validate(outputSchema, result.structuredContent());
289290

290291
if (!validation.valid()) {
291292
logger.warn("Tool call result validation failed: {}", validation.errorMessage());
292-
return new CallToolResult(validation.errorMessage(), true);
293+
return CallToolResult.builder()
294+
.content(List.of(new McpSchema.TextContent(validation.errorMessage())))
295+
.isError(true)
296+
.build();
293297
}
294298

295299
if (Utils.isEmpty(result.content())) {

mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1564,6 +1564,7 @@ public CallToolResult(List<Content> content, Boolean isError, Map<String, Object
15641564
* content contains error information. If false or absent, indicates successful
15651565
* execution.
15661566
*/
1567+
@Deprecated
15671568
public CallToolResult(String content, Boolean isError) {
15681569
this(List.of(new TextContent(content)), isError, null);
15691570
}
@@ -1678,7 +1679,7 @@ public Builder meta(Map<String, Object> meta) {
16781679
* @return a new CallToolResult instance
16791680
*/
16801681
public CallToolResult build() {
1681-
return new CallToolResult(content, isError, (Object) structuredContent, meta);
1682+
return new CallToolResult(content, isError, structuredContent, meta);
16821683
}
16831684

16841685
}

mcp-core/src/test/java/io/modelcontextprotocol/common/HttpClientStreamableHttpVersionNegotiationIntegrationTests.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ void usesLatestVersion() {
9090
}
9191

9292
@Test
93-
void usesCustomLatestVersion() {
93+
void usesServerSupportedVersion() {
9494
startTomcat();
9595

9696
var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT)
@@ -102,19 +102,21 @@ void usesCustomLatestVersion() {
102102
McpSchema.CallToolResult response = client.callTool(new McpSchema.CallToolRequest("test-tool", Map.of()));
103103

104104
var calls = requestRecordingFilter.getCalls();
105-
106-
assertThat(calls).filteredOn(c -> !c.body().contains("\"method\":\"initialize\""))
107-
// GET /mcp ; POST notification/initialized ; POST tools/call
108-
.hasSize(3)
105+
// Initialize tells the server the Client's latest supported version
106+
// FIXME: Set the correct protocol version on GET /mcp
107+
assertThat(calls).filteredOn(c -> c.method().equals("POST") && !c.body().contains("\"method\":\"initialize\""))
108+
// POST notification/initialized ; POST tools/call
109+
.hasSize(2)
109110
.map(McpTestRequestRecordingServletFilter.Call::headers)
110-
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version", "2263-03-18"));
111+
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
112+
ProtocolVersions.MCP_2025_06_18));
111113

112114
assertThat(response).isNotNull();
113115
assertThat(response.content()).hasSize(1)
114116
.first()
115117
.extracting(McpSchema.TextContent.class::cast)
116118
.extracting(McpSchema.TextContent::text)
117-
.isEqualTo("2263-03-18");
119+
.isEqualTo(ProtocolVersions.MCP_2025_06_18);
118120
mcpServer.close();
119121
}
120122

0 commit comments

Comments
 (0)