Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4c1931afa | ||
|
|
e94a93a91b | ||
|
|
210a81399a | ||
|
|
f4380e7fb3 | ||
|
|
a184a621ce |
20 changed files with 414 additions and 113 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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())));
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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())));
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,7 +147,8 @@ 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);
|
||||||
|
if (response.id() != null) {
|
||||||
var sink = pendingResponses.remove(response.id());
|
var sink = pendingResponses.remove(response.id());
|
||||||
if (sink == null) {
|
if (sink == null) {
|
||||||
logger.warn("Unexpected response for unknown id {}", response.id());
|
logger.warn("Unexpected response for unknown id {}", response.id());
|
||||||
|
|
@ -155,6 +157,12 @@ public class McpClientSession implements McpSession {
|
||||||
sink.success(response);
|
sink.success(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
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) {
|
||||||
logger.debug("Received request: {}", request);
|
logger.debug("Received request: {}", request);
|
||||||
handleIncomingRequest(request).onErrorResume(error -> {
|
handleIncomingRequest(request).onErrorResume(error -> {
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,8 @@ 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);
|
||||||
|
if (response.id() != null) {
|
||||||
var sink = pendingResponses.remove(response.id());
|
var sink = pendingResponses.remove(response.id());
|
||||||
if (sink == null) {
|
if (sink == null) {
|
||||||
logger.warn("Unexpected response for unknown id {}", response.id());
|
logger.warn("Unexpected response for unknown id {}", response.id());
|
||||||
|
|
@ -212,6 +213,12 @@ public class McpServerSession implements McpLoggableSession {
|
||||||
else {
|
else {
|
||||||
sink.success(response);
|
sink.success(response);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
else if (message instanceof McpSchema.JSONRPCRequest request) {
|
else if (message instanceof McpSchema.JSONRPCRequest request) {
|
||||||
|
|
|
||||||
|
|
@ -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,20 +216,31 @@ 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(() -> {
|
||||||
|
logger.debug("Received response: {}", response);
|
||||||
|
|
||||||
|
if (response.id() != null) {
|
||||||
var stream = this.requestIdToStream.get(response.id());
|
var stream = this.requestIdToStream.get(response.id());
|
||||||
if (stream == null) {
|
if (stream == null) {
|
||||||
return Mono.error(new McpError("Unexpected response for unknown id " + response.id())); // TODO
|
return Mono.error(McpError.builder(ErrorCodes.INTERNAL_ERROR)
|
||||||
// JSONize
|
.message("Unexpected response for unknown id " + response.id())
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
// TODO: encapsulate this inside the stream itself
|
// TODO: encapsulate this inside the stream itself
|
||||||
var sink = stream.pendingResponses.remove(response.id());
|
var sink = stream.pendingResponses.remove(response.id());
|
||||||
if (sink == null) {
|
if (sink == null) {
|
||||||
return Mono.error(new McpError("Unexpected response for unknown id " + response.id())); // TODO
|
return Mono.error(McpError.builder(ErrorCodes.INTERNAL_ERROR)
|
||||||
// JSONize
|
.message("Unexpected response for unknown id " + response.id())
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
sink.success(response);
|
sink.success(response);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
2
pom.xml
2
pom.xml
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue