Compare commits

...
Author SHA1 Message Date
Christian Tzolov
e4c1931afa Release version 0.12.1
Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
2025-09-08 14:09:56 +02:00
Yanming Zhou
e94a93a91b fix: Add null check for response ID to prevent memory leaks
- Check if response.id() is not null before processing in MCP session classes
- Log error when MCP response lacks session ID to warn about potential memory leaks
- Improve error handling in McpStreamableServerSession with proper error codes

The missing null check could lead to memory leaks as pending requests
would never be completed when responses lack session IDs. This fix ensures
proper handling of such cases with appropriate error logging.

Resolves #506

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2025-09-08 14:01:57 +02:00
Christian Tzolov
210a81399a fix: Skip structured output validation for error tool results
This ensures that when a tool handler returns an error result, the structured
output schema validation is skipped, preventing validation failures on error
responses that don't conform to the expected output schema.

- Add validation bypass when CallToolResult.isError() is true in async/stateless servers
- Fix async tool handler chaining to properly use then() instead of block()
- Add comprehensive tests for structured output with in-handler errors
- Improve error handling to use proper JSON-RPC error codes for unknown tools
- Add findRootCause utility method for better error diagnostics
- Increase test timeouts for stability in StdioMcp client tests. These tests use npx
  to download and run the MCP "everything" server locally. The first
  test execution will download the everything server scripts and cache them locally,
  which can take more than 15 seconds. Subsequent test runs will use the cached version
  and execute faster.

Resolves #538
Related to #422

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
2025-09-08 13:56:14 +02:00
Christian Tzolov
f4380e7fb3 Next development version
Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
2025-09-04 21:11:37 +02:00
Christian Tzolov
a184a621ce Release version 0.12.0
Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
2025-09-04 21:11:10 +02:00
20 changed files with 414 additions and 113 deletions

View file

@ -7,7 +7,7 @@
<parent> <parent>
<groupId>io.modelcontextprotocol.sdk</groupId> <groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-parent</artifactId> <artifactId>mcp-parent</artifactId>
<version>0.12.0-SNAPSHOT</version> <version>0.12.1</version>
</parent> </parent>
<artifactId>mcp-bom</artifactId> <artifactId>mcp-bom</artifactId>

View file

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>io.modelcontextprotocol.sdk</groupId> <groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-parent</artifactId> <artifactId>mcp-parent</artifactId>
<version>0.12.0-SNAPSHOT</version> <version>0.12.1</version>
<relativePath>../../pom.xml</relativePath> <relativePath>../../pom.xml</relativePath>
</parent> </parent>
<artifactId>mcp-spring-webflux</artifactId> <artifactId>mcp-spring-webflux</artifactId>
@ -25,13 +25,13 @@
<dependency> <dependency>
<groupId>io.modelcontextprotocol.sdk</groupId> <groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId> <artifactId>mcp</artifactId>
<version>0.12.0-SNAPSHOT</version> <version>0.12.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.modelcontextprotocol.sdk</groupId> <groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-test</artifactId> <artifactId>mcp-test</artifactId>
<version>0.12.0-SNAPSHOT</version> <version>0.12.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>

View file

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>io.modelcontextprotocol.sdk</groupId> <groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-parent</artifactId> <artifactId>mcp-parent</artifactId>
<version>0.12.0-SNAPSHOT</version> <version>0.12.1</version>
<relativePath>../../pom.xml</relativePath> <relativePath>../../pom.xml</relativePath>
</parent> </parent>
<artifactId>mcp-spring-webmvc</artifactId> <artifactId>mcp-spring-webmvc</artifactId>
@ -25,7 +25,7 @@
<dependency> <dependency>
<groupId>io.modelcontextprotocol.sdk</groupId> <groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId> <artifactId>mcp</artifactId>
<version>0.12.0-SNAPSHOT</version> <version>0.12.1</version>
</dependency> </dependency>
<dependency> <dependency>
@ -37,14 +37,14 @@
<dependency> <dependency>
<groupId>io.modelcontextprotocol.sdk</groupId> <groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-test</artifactId> <artifactId>mcp-test</artifactId>
<version>0.12.0-SNAPSHOT</version> <version>0.12.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>io.modelcontextprotocol.sdk</groupId> <groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-spring-webflux</artifactId> <artifactId>mcp-spring-webflux</artifactId>
<version>0.12.0-SNAPSHOT</version> <version>0.12.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>

View file

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>io.modelcontextprotocol.sdk</groupId> <groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-parent</artifactId> <artifactId>mcp-parent</artifactId>
<version>0.12.0-SNAPSHOT</version> <version>0.12.1</version>
</parent> </parent>
<artifactId>mcp-test</artifactId> <artifactId>mcp-test</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>
@ -24,7 +24,7 @@
<dependency> <dependency>
<groupId>io.modelcontextprotocol.sdk</groupId> <groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId> <artifactId>mcp</artifactId>
<version>0.12.0-SNAPSHOT</version> <version>0.12.1</version>
</dependency> </dependency>
<dependency> <dependency>

View file

@ -4,14 +4,6 @@
package io.modelcontextprotocol; package io.modelcontextprotocol;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertWith;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
@ -29,9 +21,6 @@ import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpServer;
@ -56,12 +45,23 @@ import io.modelcontextprotocol.spec.McpSchema.PromptReference;
import io.modelcontextprotocol.spec.McpSchema.Role; import io.modelcontextprotocol.spec.McpSchema.Role;
import io.modelcontextprotocol.spec.McpSchema.Root; import io.modelcontextprotocol.spec.McpSchema.Root;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.util.Utils; import io.modelcontextprotocol.util.Utils;
import net.javacrumbs.jsonunit.core.Option; import net.javacrumbs.jsonunit.core.Option;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertWith;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
public abstract class AbstractMcpClientServerIntegrationTests { public abstract class AbstractMcpClientServerIntegrationTests {
protected ConcurrentHashMap<String, McpClient.SyncSpec> clientBuilders = new ConcurrentHashMap<>(); protected ConcurrentHashMap<String, McpClient.SyncSpec> clientBuilders = new ConcurrentHashMap<>();
@ -108,8 +108,8 @@ public abstract class AbstractMcpClientServerIntegrationTests {
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
exchange.createMessage(mock(McpSchema.CreateMessageRequest.class)).block(); return exchange.createMessage(mock(McpSchema.CreateMessageRequest.class))
return Mono.just(mock(CallToolResult.class)); .then(Mono.just(mock(CallToolResult.class)));
}) })
.build(); .build();
@ -1434,6 +1434,66 @@ public abstract class AbstractMcpClientServerIntegrationTests {
@ParameterizedTest(name = "{0} : {displayName} ") @ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" }) @ValueSource(strings = { "httpclient", "webflux" })
void testStructuredOutputWithInHandlerError(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
// Create a tool with output schema
Map<String, Object> outputSchema = Map.of(
"type", "object", "properties", Map.of("result", Map.of("type", "number"), "operation",
Map.of("type", "string"), "timestamp", Map.of("type", "string")),
"required", List.of("result", "operation"));
Tool calculatorTool = Tool.builder()
.name("calculator")
.description("Performs mathematical calculations")
.outputSchema(outputSchema)
.build();
// Handler that throws an exception to simulate an error
McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder()
.tool(calculatorTool)
.callHandler((exchange, request) -> {
return CallToolResult.builder()
.isError(true)
.content(List.of(new TextContent("Error calling tool: Simulated in-handler error")))
.build();
})
.build();
var mcpServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(tool)
.build();
try (var mcpClient = clientBuilder.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
// Verify tool is listed with output schema
var toolsList = mcpClient.listTools();
assertThat(toolsList.tools()).hasSize(1);
assertThat(toolsList.tools().get(0).name()).isEqualTo("calculator");
// Note: outputSchema might be null in sync server, but validation still works
// Call tool with valid structured output
CallToolResult response = mcpClient
.callTool(new McpSchema.CallToolRequest("calculator", Map.of("expression", "2 + 3")));
assertThat(response).isNotNull();
assertThat(response.isError()).isTrue();
assertThat(response.content()).isNotEmpty();
assertThat(response.content())
.containsExactly(new McpSchema.TextContent("Error calling tool: Simulated in-handler error"));
assertThat(response.structuredContent()).isNull();
}
finally {
mcpServer.closeGracefully();
}
}
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient" })
void testStructuredOutputValidationFailure(String clientType) { void testStructuredOutputValidationFailure(String clientType) {
var clientBuilder = clientBuilders.get(clientType); var clientBuilder = clientBuilders.get(clientType);

View file

@ -4,12 +4,6 @@
package io.modelcontextprotocol; package io.modelcontextprotocol;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.awaitility.Awaitility.await;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
@ -20,9 +14,6 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.server.McpServer.StatelessAsyncSpecification; import io.modelcontextprotocol.server.McpServer.StatelessAsyncSpecification;
import io.modelcontextprotocol.server.McpServer.StatelessSyncSpecification; import io.modelcontextprotocol.server.McpServer.StatelessSyncSpecification;
@ -33,10 +24,19 @@ import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.InitializeResult; import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpSchema.Tool;
import net.javacrumbs.jsonunit.core.Option; import net.javacrumbs.jsonunit.core.Option;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.awaitility.Awaitility.await;
public abstract class AbstractStatelessIntegrationTests { public abstract class AbstractStatelessIntegrationTests {
protected ConcurrentHashMap<String, McpClient.SyncSpec> clientBuilders = new ConcurrentHashMap<>(); protected ConcurrentHashMap<String, McpClient.SyncSpec> clientBuilders = new ConcurrentHashMap<>();
@ -350,6 +350,64 @@ public abstract class AbstractStatelessIntegrationTests {
} }
} }
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
void testStructuredOutputWithInHandlerError(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
// Create a tool with output schema
Map<String, Object> outputSchema = Map.of(
"type", "object", "properties", Map.of("result", Map.of("type", "number"), "operation",
Map.of("type", "string"), "timestamp", Map.of("type", "string")),
"required", List.of("result", "operation"));
Tool calculatorTool = Tool.builder()
.name("calculator")
.description("Performs mathematical calculations")
.outputSchema(outputSchema)
.build();
// Handler that throws an exception to simulate an error
McpStatelessServerFeatures.SyncToolSpecification tool = McpStatelessServerFeatures.SyncToolSpecification
.builder()
.tool(calculatorTool)
.callHandler((exchange, request) -> CallToolResult.builder()
.isError(true)
.content(List.of(new TextContent("Error calling tool: Simulated in-handler error")))
.build())
.build();
var mcpServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(tool)
.build();
try (var mcpClient = clientBuilder.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
// Verify tool is listed with output schema
var toolsList = mcpClient.listTools();
assertThat(toolsList.tools()).hasSize(1);
assertThat(toolsList.tools().get(0).name()).isEqualTo("calculator");
// Note: outputSchema might be null in sync server, but validation still works
// Call tool with valid structured output
CallToolResult response = mcpClient
.callTool(new McpSchema.CallToolRequest("calculator", Map.of("expression", "2 + 3")));
assertThat(response).isNotNull();
assertThat(response.isError()).isTrue();
assertThat(response.content()).isNotEmpty();
assertThat(response.content())
.containsExactly(new McpSchema.TextContent("Error calling tool: Simulated in-handler error"));
assertThat(response.structuredContent()).isNull();
}
finally {
mcpServer.closeGracefully();
}
}
@ParameterizedTest(name = "{0} : {displayName} ") @ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" }) @ValueSource(strings = { "httpclient", "webflux" })
void testStructuredOutputValidationFailure(String clientType) { void testStructuredOutputValidationFailure(String clientType) {

View file

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>io.modelcontextprotocol.sdk</groupId> <groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-parent</artifactId> <artifactId>mcp-parent</artifactId>
<version>0.12.0-SNAPSHOT</version> <version>0.12.1</version>
</parent> </parent>
<artifactId>mcp</artifactId> <artifactId>mcp</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>

View file

@ -29,10 +29,12 @@ import io.modelcontextprotocol.spec.McpClientSession;
import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate; import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest; import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.spec.McpServerSession; import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider;
@ -376,6 +378,11 @@ public class McpAsyncServer {
return this.delegateCallToolResult.apply(exchange, request).map(result -> { return this.delegateCallToolResult.apply(exchange, request).map(result -> {
if (Boolean.TRUE.equals(result.isError())) {
// If the tool call resulted in an error, skip further validation
return result;
}
if (outputSchema == null) { if (outputSchema == null) {
if (result.structuredContent() != null) { if (result.structuredContent() != null) {
logger.warn( logger.warn(
@ -507,11 +514,11 @@ public class McpAsyncServer {
.findAny(); .findAny();
if (toolSpecification.isEmpty()) { if (toolSpecification.isEmpty()) {
return Mono.error(new McpError("Tool not found: " + callToolRequest.name())); return Mono.error(new McpError(new JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INVALID_PARAMS,
"Unknown tool: invalid_tool_name", "Tool not found: " + callToolRequest.name())));
} }
return toolSpecification.map(tool -> Mono.defer(() -> tool.callHandler().apply(exchange, callToolRequest))) return toolSpecification.get().callHandler().apply(exchange, callToolRequest);
.orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
}; };
} }

View file

@ -11,7 +11,9 @@ import io.modelcontextprotocol.spec.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse;
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate; import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.spec.McpStatelessServerTransport; import io.modelcontextprotocol.spec.McpStatelessServerTransport;
import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.Assert;
@ -249,6 +251,11 @@ public class McpStatelessAsyncServer {
return this.delegateHandler.apply(transportContext, request).map(result -> { return this.delegateHandler.apply(transportContext, request).map(result -> {
if (Boolean.TRUE.equals(result.isError())) {
// If the tool call resulted in an error, skip further validation
return result;
}
if (outputSchema == null) { if (outputSchema == null) {
if (result.structuredContent() != null) { if (result.structuredContent() != null) {
logger.warn( logger.warn(
@ -375,11 +382,11 @@ public class McpStatelessAsyncServer {
.findAny(); .findAny();
if (toolSpecification.isEmpty()) { if (toolSpecification.isEmpty()) {
return Mono.error(new McpError("Tool not found: " + callToolRequest.name())); return Mono.error(new McpError(new JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INVALID_PARAMS,
"Unknown tool: invalid_tool_name", "Tool not found: " + callToolRequest.name())));
} }
return toolSpecification.map(tool -> tool.callHandler().apply(ctx, callToolRequest)) return toolSpecification.get().callHandler().apply(ctx, callToolRequest);
.orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
}; };
} }

View file

@ -35,6 +35,7 @@ import java.util.function.Function;
* *
* @author Christian Tzolov * @author Christian Tzolov
* @author Dariusz Jędrzejczyk * @author Dariusz Jędrzejczyk
* @author Yanming Zhou
*/ */
public class McpClientSession implements McpSession { public class McpClientSession implements McpSession {
@ -146,13 +147,20 @@ public class McpClientSession implements McpSession {
private void handle(McpSchema.JSONRPCMessage message) { private void handle(McpSchema.JSONRPCMessage message) {
if (message instanceof McpSchema.JSONRPCResponse response) { if (message instanceof McpSchema.JSONRPCResponse response) {
logger.debug("Received Response: {}", response); logger.debug("Received response: {}", response);
var sink = pendingResponses.remove(response.id()); if (response.id() != null) {
if (sink == null) { var sink = pendingResponses.remove(response.id());
logger.warn("Unexpected response for unknown id {}", response.id()); if (sink == null) {
logger.warn("Unexpected response for unknown id {}", response.id());
}
else {
sink.success(response);
}
} }
else { else {
sink.success(response); logger.error("Discarded MCP request response without session id. "
+ "This is an indication of a bug in the request sender code that can lead to memory "
+ "leaks as pending requests will never be completed.");
} }
} }
else if (message instanceof McpSchema.JSONRPCRequest request) { else if (message instanceof McpSchema.JSONRPCRequest request) {

View file

@ -204,13 +204,20 @@ public class McpServerSession implements McpLoggableSession {
// TODO handle errors for communication to without initialization happening // TODO handle errors for communication to without initialization happening
// first // first
if (message instanceof McpSchema.JSONRPCResponse response) { if (message instanceof McpSchema.JSONRPCResponse response) {
logger.debug("Received Response: {}", response); logger.debug("Received response: {}", response);
var sink = pendingResponses.remove(response.id()); if (response.id() != null) {
if (sink == null) { var sink = pendingResponses.remove(response.id());
logger.warn("Unexpected response for unknown id {}", response.id()); if (sink == null) {
logger.warn("Unexpected response for unknown id {}", response.id());
}
else {
sink.success(response);
}
} }
else { else {
sink.success(response); logger.error("Discarded MCP request response without session id. "
+ "This is an indication of a bug in the request sender code that can lead to memory "
+ "leaks as pending requests will never be completed.");
} }
return Mono.empty(); return Mono.empty();
} }

View file

@ -21,6 +21,7 @@ import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpAsyncServerExchange; import io.modelcontextprotocol.server.McpAsyncServerExchange;
import io.modelcontextprotocol.server.McpNotificationHandler; import io.modelcontextprotocol.server.McpNotificationHandler;
import io.modelcontextprotocol.server.McpRequestHandler; import io.modelcontextprotocol.server.McpRequestHandler;
import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.Assert;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -33,6 +34,7 @@ import reactor.core.publisher.MonoSink;
* capability without the insight into the transport-specific details of HTTP handling. * capability without the insight into the transport-specific details of HTTP handling.
* *
* @author Dariusz Jędrzejczyk * @author Dariusz Jędrzejczyk
* @author Yanming Zhou
*/ */
public class McpStreamableServerSession implements McpLoggableSession { public class McpStreamableServerSession implements McpLoggableSession {
@ -214,19 +216,30 @@ public class McpStreamableServerSession implements McpLoggableSession {
*/ */
public Mono<Void> accept(McpSchema.JSONRPCResponse response) { public Mono<Void> accept(McpSchema.JSONRPCResponse response) {
return Mono.defer(() -> { return Mono.defer(() -> {
var stream = this.requestIdToStream.get(response.id()); logger.debug("Received response: {}", response);
if (stream == null) {
return Mono.error(new McpError("Unexpected response for unknown id " + response.id())); // TODO if (response.id() != null) {
// JSONize var stream = this.requestIdToStream.get(response.id());
} if (stream == null) {
// TODO: encapsulate this inside the stream itself return Mono.error(McpError.builder(ErrorCodes.INTERNAL_ERROR)
var sink = stream.pendingResponses.remove(response.id()); .message("Unexpected response for unknown id " + response.id())
if (sink == null) { .build());
return Mono.error(new McpError("Unexpected response for unknown id " + response.id())); // TODO }
// JSONize // TODO: encapsulate this inside the stream itself
var sink = stream.pendingResponses.remove(response.id());
if (sink == null) {
return Mono.error(McpError.builder(ErrorCodes.INTERNAL_ERROR)
.message("Unexpected response for unknown id " + response.id())
.build());
}
else {
sink.success(response);
}
} }
else { else {
sink.success(response); logger.error("Discarded MCP request response without session id. "
+ "This is an indication of a bug in the request sender code that can lead to memory "
+ "leaks as pending requests will never be completed.");
} }
return Mono.empty(); return Mono.empty();
}); });

View file

@ -4,12 +4,12 @@
package io.modelcontextprotocol.util; package io.modelcontextprotocol.util;
import reactor.util.annotation.Nullable;
import java.net.URI; import java.net.URI;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import reactor.util.annotation.Nullable;
/** /**
* Miscellaneous utility methods. * Miscellaneous utility methods.
* *

View file

@ -14,10 +14,17 @@ import org.junit.jupiter.api.Timeout;
/** /**
* Tests for the {@link McpAsyncClient} with {@link StdioClientTransport}. * Tests for the {@link McpAsyncClient} with {@link StdioClientTransport}.
* *
* <p>
* These tests use npx to download and run the MCP "everything" server locally. The first
* test execution will download the everything server scripts and cache them locally,
* which can take more than 15 seconds. Subsequent test runs will use the cached version
* and execute faster.
*
* @author Christian Tzolov * @author Christian Tzolov
* @author Dariusz Jędrzejczyk * @author Dariusz Jędrzejczyk
*/ */
@Timeout(15) // Giving extra time beyond the client timeout @Timeout(25) // Giving extra time beyond the client timeout to account for initial server
// download
class StdioMcpAsyncClientTests extends AbstractMcpAsyncClientTests { class StdioMcpAsyncClientTests extends AbstractMcpAsyncClientTests {
@Override @Override
@ -40,4 +47,9 @@ class StdioMcpAsyncClientTests extends AbstractMcpAsyncClientTests {
return Duration.ofSeconds(20); return Duration.ofSeconds(20);
} }
@Override
protected Duration getRequestTimeout() {
return Duration.ofSeconds(25);
}
} }

View file

@ -22,10 +22,17 @@ import static org.assertj.core.api.Assertions.assertThat;
/** /**
* Tests for the {@link McpSyncClient} with {@link StdioClientTransport}. * Tests for the {@link McpSyncClient} with {@link StdioClientTransport}.
* *
* <p>
* These tests use npx to download and run the MCP "everything" server locally. The first
* test execution will download the everything server scripts and cache them locally,
* which can take more than 15 seconds. Subsequent test runs will use the cached version
* and execute faster.
*
* @author Christian Tzolov * @author Christian Tzolov
* @author Dariusz Jędrzejczyk * @author Dariusz Jędrzejczyk
*/ */
@Timeout(15) // Giving extra time beyond the client timeout @Timeout(25) // Giving extra time beyond the client timeout to account for initial server
// download
class StdioMcpSyncClientTests extends AbstractMcpSyncClientTests { class StdioMcpSyncClientTests extends AbstractMcpSyncClientTests {
@Override @Override
@ -71,4 +78,9 @@ class StdioMcpSyncClientTests extends AbstractMcpSyncClientTests {
return Duration.ofSeconds(10); return Duration.ofSeconds(10);
} }
@Override
protected Duration getRequestTimeout() {
return Duration.ofSeconds(25);
}
} }

View file

@ -4,15 +4,6 @@
package io.modelcontextprotocol.server; package io.modelcontextprotocol.server;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertWith;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
import io.modelcontextprotocol.common.McpTransportContext;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
@ -30,10 +21,8 @@ import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
@ -52,12 +41,23 @@ import io.modelcontextprotocol.spec.McpSchema.PromptReference;
import io.modelcontextprotocol.spec.McpSchema.Role; import io.modelcontextprotocol.spec.McpSchema.Role;
import io.modelcontextprotocol.spec.McpSchema.Root; import io.modelcontextprotocol.spec.McpSchema.Root;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.util.Utils; import io.modelcontextprotocol.util.Utils;
import net.javacrumbs.jsonunit.core.Option; import net.javacrumbs.jsonunit.core.Option;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; import reactor.test.StepVerifier;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertWith;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
public abstract class AbstractMcpClientServerIntegrationTests { public abstract class AbstractMcpClientServerIntegrationTests {
protected ConcurrentHashMap<String, McpClient.SyncSpec> clientBuilders = new ConcurrentHashMap<>(); protected ConcurrentHashMap<String, McpClient.SyncSpec> clientBuilders = new ConcurrentHashMap<>();
@ -104,8 +104,8 @@ public abstract class AbstractMcpClientServerIntegrationTests {
McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder()
.tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build()) .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(emptyJsonSchema).build())
.callHandler((exchange, request) -> { .callHandler((exchange, request) -> {
exchange.createMessage(mock(McpSchema.CreateMessageRequest.class)).block(); return exchange.createMessage(mock(McpSchema.CreateMessageRequest.class))
return Mono.just(mock(CallToolResult.class)); .then(Mono.just(mock(CallToolResult.class)));
}) })
.build(); .build();
@ -1428,6 +1428,63 @@ public abstract class AbstractMcpClientServerIntegrationTests {
} }
} }
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient" })
void testStructuredOutputWithInHandlerError(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
// Create a tool with output schema
Map<String, Object> outputSchema = Map.of(
"type", "object", "properties", Map.of("result", Map.of("type", "number"), "operation",
Map.of("type", "string"), "timestamp", Map.of("type", "string")),
"required", List.of("result", "operation"));
Tool calculatorTool = Tool.builder()
.name("calculator")
.description("Performs mathematical calculations")
.outputSchema(outputSchema)
.build();
// Handler that returns an error result
McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder()
.tool(calculatorTool)
.callHandler((exchange, request) -> CallToolResult.builder()
.isError(true)
.content(List.of(new TextContent("Error calling tool: Simulated in-handler error")))
.build())
.build();
var mcpServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(tool)
.build();
try (var mcpClient = clientBuilder.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
// Verify tool is listed with output schema
var toolsList = mcpClient.listTools();
assertThat(toolsList.tools()).hasSize(1);
assertThat(toolsList.tools().get(0).name()).isEqualTo("calculator");
// Note: outputSchema might be null in sync server, but validation still works
// Call tool with valid structured output
CallToolResult response = mcpClient
.callTool(new McpSchema.CallToolRequest("calculator", Map.of("expression", "2 + 3")));
assertThat(response).isNotNull();
assertThat(response.isError()).isTrue();
assertThat(response.content()).isNotEmpty();
assertThat(response.content())
.containsExactly(new McpSchema.TextContent("Error calling tool: Simulated in-handler error"));
assertThat(response.structuredContent()).isNull();
}
finally {
mcpServer.closeGracefully();
}
}
@ParameterizedTest(name = "{0} : {displayName} ") @ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient" }) @ValueSource(strings = { "httpclient" })
void testStructuredOutputValidationFailure(String clientType) { void testStructuredOutputValidationFailure(String clientType) {

View file

@ -4,12 +4,18 @@
package io.modelcontextprotocol.server; package io.modelcontextprotocol.server;
import static org.assertj.core.api.Assertions.assertThat;
import io.modelcontextprotocol.common.McpTransportContext;
import java.time.Duration; import java.time.Duration;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpServer.AsyncSpecification;
import io.modelcontextprotocol.server.McpServer.SyncSpecification;
import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider;
import io.modelcontextprotocol.server.transport.TomcatTestUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState; import org.apache.catalina.LifecycleState;
import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.Tomcat;
@ -17,15 +23,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.Timeout;
import com.fasterxml.jackson.databind.ObjectMapper; import static org.assertj.core.api.Assertions.assertThat;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
import io.modelcontextprotocol.server.McpServer.AsyncSpecification;
import io.modelcontextprotocol.server.McpServer.SyncSpecification;
import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider;
import io.modelcontextprotocol.server.transport.TomcatTestUtil;
import jakarta.servlet.http.HttpServletRequest;
@Timeout(15) @Timeout(15)
class HttpServletSseIntegrationTests extends AbstractMcpClientServerIntegrationTests { class HttpServletSseIntegrationTests extends AbstractMcpClientServerIntegrationTests {

View file

@ -4,6 +4,13 @@
package io.modelcontextprotocol.server; package io.modelcontextprotocol.server;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
@ -11,16 +18,17 @@ import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport; import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport;
import io.modelcontextprotocol.server.transport.TomcatTestUtil; import io.modelcontextprotocol.server.transport.TomcatTestUtil;
import io.modelcontextprotocol.spec.HttpHeaders; import io.modelcontextprotocol.spec.HttpHeaders;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.CompleteRequest; import io.modelcontextprotocol.spec.McpSchema.CompleteRequest;
import io.modelcontextprotocol.spec.McpSchema.CompleteResult; import io.modelcontextprotocol.spec.McpSchema.CompleteResult;
import io.modelcontextprotocol.spec.McpSchema.ErrorCodes;
import io.modelcontextprotocol.spec.McpSchema.InitializeResult; import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
import io.modelcontextprotocol.spec.McpSchema.Prompt; import io.modelcontextprotocol.spec.McpSchema.Prompt;
import io.modelcontextprotocol.spec.McpSchema.PromptArgument; import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
import io.modelcontextprotocol.spec.McpSchema.PromptReference; import io.modelcontextprotocol.spec.McpSchema.PromptReference;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.TextContent;
import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.spec.ProtocolVersions; import io.modelcontextprotocol.spec.ProtocolVersions;
import net.javacrumbs.jsonunit.core.Option; import net.javacrumbs.jsonunit.core.Option;
@ -33,17 +41,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.APPLICATION_JSON; import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.APPLICATION_JSON;
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.TEXT_EVENT_STREAM; import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.TEXT_EVENT_STREAM;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
@ -298,6 +300,65 @@ class HttpServletStatelessIntegrationTests {
} }
} }
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient" })
void testStructuredOutputWithInHandlerError(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
// Create a tool with output schema
Map<String, Object> outputSchema = Map.of(
"type", "object", "properties", Map.of("result", Map.of("type", "number"), "operation",
Map.of("type", "string"), "timestamp", Map.of("type", "string")),
"required", List.of("result", "operation"));
Tool calculatorTool = Tool.builder()
.name("calculator")
.description("Performs mathematical calculations")
.outputSchema(outputSchema)
.build();
// Handler that returns an error result
McpStatelessServerFeatures.SyncToolSpecification tool = McpStatelessServerFeatures.SyncToolSpecification
.builder()
.tool(calculatorTool)
.callHandler((exchange, request) -> CallToolResult.builder()
.isError(true)
.content(List.of(new TextContent("Error calling tool: Simulated in-handler error")))
.build())
.build();
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(tool)
.build();
try (var mcpClient = clientBuilder.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
// Verify tool is listed with output schema
var toolsList = mcpClient.listTools();
assertThat(toolsList.tools()).hasSize(1);
assertThat(toolsList.tools().get(0).name()).isEqualTo("calculator");
// Note: outputSchema might be null in sync server, but validation still works
// Call tool with valid structured output
CallToolResult response = mcpClient
.callTool(new McpSchema.CallToolRequest("calculator", Map.of("expression", "2 + 3")));
assertThat(response).isNotNull();
assertThat(response.isError()).isTrue();
assertThat(response.content()).isNotEmpty();
assertThat(response.content())
.containsExactly(new McpSchema.TextContent("Error calling tool: Simulated in-handler error"));
assertThat(response.structuredContent()).isNull();
}
finally {
mcpServer.closeGracefully();
}
}
@ParameterizedTest(name = "{0} : {displayName} ") @ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient" }) @ValueSource(strings = { "httpclient" })
void testStructuredOutputValidationFailure(String clientType) { void testStructuredOutputValidationFailure(String clientType) {
@ -477,7 +538,7 @@ class HttpServletStatelessIntegrationTests {
} }
@Test @Test
void testThrownMcpError() throws Exception { void testThrownMcpErrorAndJsonRpcError() throws Exception {
var mcpServer = McpServer.sync(mcpStatelessServerTransport) var mcpServer = McpServer.sync(mcpStatelessServerTransport)
.serverInfo("test-server", "1.0.0") .serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build()) .capabilities(ServerCapabilities.builder().tools(true).build())
@ -487,7 +548,7 @@ class HttpServletStatelessIntegrationTests {
McpStatelessServerFeatures.SyncToolSpecification toolSpec = new McpStatelessServerFeatures.SyncToolSpecification( McpStatelessServerFeatures.SyncToolSpecification toolSpec = new McpStatelessServerFeatures.SyncToolSpecification(
testTool, (transportContext, request) -> { testTool, (transportContext, request) -> {
throw new McpError(new McpSchema.JSONRPCResponse.JSONRPCError(12345, "testing", Map.of("a", "b"))); throw new RuntimeException("testing");
}); });
mcpServer.addTool(toolSpec); mcpServer.addTool(toolSpec);
@ -508,13 +569,16 @@ class HttpServletStatelessIntegrationTests {
request.addHeader("Content-Type", APPLICATION_JSON); request.addHeader("Content-Type", APPLICATION_JSON);
request.addHeader("Cache-Control", "no-cache"); request.addHeader("Cache-Control", "no-cache");
request.addHeader(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_03_26); request.addHeader(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_03_26);
mcpStatelessServerTransport.service(request, response); mcpStatelessServerTransport.service(request, response);
McpSchema.JSONRPCResponse jsonrpcResponse = new ObjectMapper().readValue(response.getContentAsByteArray(), McpSchema.JSONRPCResponse jsonrpcResponse = new ObjectMapper().readValue(response.getContentAsByteArray(),
McpSchema.JSONRPCResponse.class); McpSchema.JSONRPCResponse.class);
assertThat(jsonrpcResponse.error()) assertThat(jsonrpcResponse).isNotNull();
.isEqualTo(new McpSchema.JSONRPCResponse.JSONRPCError(12345, "testing", Map.of("a", "b"))); assertThat(jsonrpcResponse.error()).isNotNull();
assertThat(jsonrpcResponse.error().code()).isEqualTo(ErrorCodes.INTERNAL_ERROR);
assertThat(jsonrpcResponse.error().message()).isEqualTo("testing");
mcpServer.close(); mcpServer.close();
} }

View file

@ -4,12 +4,18 @@
package io.modelcontextprotocol.server; package io.modelcontextprotocol.server;
import static org.assertj.core.api.Assertions.assertThat;
import io.modelcontextprotocol.common.McpTransportContext;
import java.time.Duration; import java.time.Duration;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpServer.AsyncSpecification;
import io.modelcontextprotocol.server.McpServer.SyncSpecification;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.server.transport.TomcatTestUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState; import org.apache.catalina.LifecycleState;
import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.Tomcat;
@ -17,15 +23,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.Timeout;
import com.fasterxml.jackson.databind.ObjectMapper; import static org.assertj.core.api.Assertions.assertThat;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.server.McpServer.AsyncSpecification;
import io.modelcontextprotocol.server.McpServer.SyncSpecification;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.server.transport.TomcatTestUtil;
import jakarta.servlet.http.HttpServletRequest;
@Timeout(15) @Timeout(15)
class HttpServletStreamableIntegrationTests extends AbstractMcpClientServerIntegrationTests { class HttpServletStreamableIntegrationTests extends AbstractMcpClientServerIntegrationTests {

View file

@ -6,7 +6,7 @@
<groupId>io.modelcontextprotocol.sdk</groupId> <groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-parent</artifactId> <artifactId>mcp-parent</artifactId>
<version>0.12.0-SNAPSHOT</version> <version>0.12.1</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<url>https://github.com/modelcontextprotocol/java-sdk</url> <url>https://github.com/modelcontextprotocol/java-sdk</url>