fix: support valid JSON value in tool structured content output (#551)
- Change CallToolResult.structuredContent type from Map<String,Object> to Object to support both objects and arrays - Update JsonSchemaValidator to validate any Object type, not just Maps - Add test cases for array-type structured output validation - Fix type casting in existing tests to handle the more generic Object type - Deprecate CallToolResult constructors in favor of builder pattern - Update server implementations to use CallToolResult builder This allows tools to return arrays as structured content, not just objects, which is required by the MCP specification for tools with array-type output schemas. It is realated to MPC spec fix: https://github.com/modelcontextprotocol/modelcontextprotocol/issues/834 Fixes #550 BREAKING CHANGE! Migration Guide: ---------------- 1. If you're accessing CallToolResult.structuredContent(): - Add explicit casting when you know it's a Map: Before: result.structuredContent().get("key") After: ((Map<String,Object>) result.structuredContent()).get("key") - Or check the type first: if (result.structuredContent() instanceof Map) { Map<String,Object> map = (Map<String,Object>) result.structuredContent(); // use map } else if (result.structuredContent() instanceof List) { List<?> list = (List<?>) result.structuredContent(); // use list } 2. If you're creating CallToolResult instances: - Switch to using the builder pattern: Before: new CallToolResult(content, isError, structuredContent) After: CallToolResult.builder() .content(content) .isError(isError) .structuredContent(structuredContent) .build() 3. If you're implementing JsonSchemaValidator: - Update your validate() method signature: Before: validate(Map<String,Object> schema, Map<String,Object> structuredContent) After: validate(Map<String,Object> schema, Object structuredContent) Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
parent
7e3ea73815
commit
d8959ef854
10 changed files with 283 additions and 39 deletions
|
|
@ -1411,7 +1411,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
|
|
||||||
// In WebMVC, structured content is returned properly
|
// In WebMVC, structured content is returned properly
|
||||||
if (response.structuredContent() != null) {
|
if (response.structuredContent() != null) {
|
||||||
assertThat(response.structuredContent()).containsEntry("result", 5.0)
|
assertThat((Map<String, Object>) response.structuredContent()).containsEntry("result", 5.0)
|
||||||
.containsEntry("operation", "2 + 3")
|
.containsEntry("operation", "2 + 3")
|
||||||
.containsEntry("timestamp", "2024-01-01T10:00:00Z");
|
.containsEntry("timestamp", "2024-01-01T10:00:00Z");
|
||||||
}
|
}
|
||||||
|
|
@ -1433,7 +1433,66 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest(name = "{0} : {displayName} ")
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
@ValueSource(strings = { "httpclient", "webflux" })
|
@ValueSource(strings = { "httpclient" })
|
||||||
|
void testStructuredOutputOfObjectArrayValidationSuccess(String clientType) {
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
// Create a tool with output schema that returns an array of objects
|
||||||
|
Map<String, Object> outputSchema = Map
|
||||||
|
.of( // @formatter:off
|
||||||
|
"type", "array",
|
||||||
|
"items", Map.of(
|
||||||
|
"type", "object",
|
||||||
|
"properties", Map.of(
|
||||||
|
"name", Map.of("type", "string"),
|
||||||
|
"age", Map.of("type", "number")),
|
||||||
|
"required", List.of("name", "age"))); // @formatter:on
|
||||||
|
|
||||||
|
Tool calculatorTool = Tool.builder()
|
||||||
|
.name("getMembers")
|
||||||
|
.description("Returns a list of members")
|
||||||
|
.outputSchema(outputSchema)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder()
|
||||||
|
.tool(calculatorTool)
|
||||||
|
.callHandler((exchange, request) -> {
|
||||||
|
return CallToolResult.builder()
|
||||||
|
.structuredContent(List.of(Map.of("name", "John", "age", 30), Map.of("name", "Peter", "age", 25)))
|
||||||
|
.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()) {
|
||||||
|
assertThat(mcpClient.initialize()).isNotNull();
|
||||||
|
|
||||||
|
// Call tool with valid structured output of type array
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("getMembers", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response.isError()).isFalse();
|
||||||
|
|
||||||
|
assertThat(response.structuredContent()).isNotNull();
|
||||||
|
assertThatJson(response.structuredContent()).when(Option.IGNORING_ARRAY_ORDER)
|
||||||
|
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
||||||
|
.isArray()
|
||||||
|
.hasSize(2)
|
||||||
|
.containsExactlyInAnyOrder(json("""
|
||||||
|
{"name":"John","age":30}"""), json("""
|
||||||
|
{"name":"Peter","age":25}"""));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
mcpServer.closeGracefully();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@ValueSource(strings = { "httpclient" })
|
||||||
void testStructuredOutputWithInHandlerError(String clientType) {
|
void testStructuredOutputWithInHandlerError(String clientType) {
|
||||||
var clientBuilder = clientBuilders.get(clientType);
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
|
@ -1449,16 +1508,13 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
.outputSchema(outputSchema)
|
.outputSchema(outputSchema)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Handler that throws an exception to simulate an error
|
// Handler that returns an error result
|
||||||
McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder()
|
McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder()
|
||||||
.tool(calculatorTool)
|
.tool(calculatorTool)
|
||||||
.callHandler((exchange, request) -> {
|
.callHandler((exchange, request) -> CallToolResult.builder()
|
||||||
|
.isError(true)
|
||||||
return CallToolResult.builder()
|
.content(List.of(new TextContent("Error calling tool: Simulated in-handler error")))
|
||||||
.isError(true)
|
.build())
|
||||||
.content(List.of(new TextContent("Error calling tool: Simulated in-handler error")))
|
|
||||||
.build();
|
|
||||||
})
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
|
var mcpServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,6 @@ public abstract class AbstractStatelessIntegrationTests {
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Tool Structured Output Schema Tests
|
// Tool Structured Output Schema Tests
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
||||||
@ParameterizedTest(name = "{0} : {displayName} ")
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
@ValueSource(strings = { "httpclient", "webflux" })
|
@ValueSource(strings = { "httpclient", "webflux" })
|
||||||
void testStructuredOutputValidationSuccess(String clientType) {
|
void testStructuredOutputValidationSuccess(String clientType) {
|
||||||
|
|
@ -329,7 +328,7 @@ public abstract class AbstractStatelessIntegrationTests {
|
||||||
|
|
||||||
// In WebMVC, structured content is returned properly
|
// In WebMVC, structured content is returned properly
|
||||||
if (response.structuredContent() != null) {
|
if (response.structuredContent() != null) {
|
||||||
assertThat(response.structuredContent()).containsEntry("result", 5.0)
|
assertThat((Map<String, Object>) response.structuredContent()).containsEntry("result", 5.0)
|
||||||
.containsEntry("operation", "2 + 3")
|
.containsEntry("operation", "2 + 3")
|
||||||
.containsEntry("timestamp", "2024-01-01T10:00:00Z");
|
.containsEntry("timestamp", "2024-01-01T10:00:00Z");
|
||||||
}
|
}
|
||||||
|
|
@ -350,6 +349,66 @@ public abstract class AbstractStatelessIntegrationTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@ValueSource(strings = { "httpclient", "webflux" })
|
||||||
|
void testStructuredOutputOfObjectArrayValidationSuccess(String clientType) {
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
// Create a tool with output schema that returns an array of objects
|
||||||
|
Map<String, Object> outputSchema = Map
|
||||||
|
.of( // @formatter:off
|
||||||
|
"type", "array",
|
||||||
|
"items", Map.of(
|
||||||
|
"type", "object",
|
||||||
|
"properties", Map.of(
|
||||||
|
"name", Map.of("type", "string"),
|
||||||
|
"age", Map.of("type", "number")),
|
||||||
|
"required", List.of("name", "age"))); // @formatter:on
|
||||||
|
|
||||||
|
Tool calculatorTool = Tool.builder()
|
||||||
|
.name("getMembers")
|
||||||
|
.description("Returns a list of members")
|
||||||
|
.outputSchema(outputSchema)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
McpStatelessServerFeatures.SyncToolSpecification tool = McpStatelessServerFeatures.SyncToolSpecification
|
||||||
|
.builder()
|
||||||
|
.tool(calculatorTool)
|
||||||
|
.callHandler((exchange, request) -> {
|
||||||
|
return CallToolResult.builder()
|
||||||
|
.structuredContent(List.of(Map.of("name", "John", "age", 30), Map.of("name", "Peter", "age", 25)))
|
||||||
|
.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()) {
|
||||||
|
assertThat(mcpClient.initialize()).isNotNull();
|
||||||
|
|
||||||
|
// Call tool with valid structured output of type array
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("getMembers", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response.isError()).isFalse();
|
||||||
|
|
||||||
|
assertThat(response.structuredContent()).isNotNull();
|
||||||
|
assertThatJson(response.structuredContent()).when(Option.IGNORING_ARRAY_ORDER)
|
||||||
|
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
||||||
|
.isArray()
|
||||||
|
.hasSize(2)
|
||||||
|
.containsExactlyInAnyOrder(json("""
|
||||||
|
{"name":"John","age":30}"""), json("""
|
||||||
|
{"name":"Peter","age":25}"""));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
mcpServer.closeGracefully();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest(name = "{0} : {displayName} ")
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
@ValueSource(strings = { "httpclient", "webflux" })
|
@ValueSource(strings = { "httpclient", "webflux" })
|
||||||
void testStructuredOutputWithInHandlerError(String clientType) {
|
void testStructuredOutputWithInHandlerError(String clientType) {
|
||||||
|
|
|
||||||
|
|
@ -15,15 +15,9 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.DefaultMcpStreamableServerSessionFactory;
|
|
||||||
import io.modelcontextprotocol.spec.McpServerTransportProviderBase;
|
|
||||||
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.modelcontextprotocol.spec.DefaultMcpStreamableServerSessionFactory;
|
||||||
import io.modelcontextprotocol.spec.JsonSchemaValidator;
|
import io.modelcontextprotocol.spec.JsonSchemaValidator;
|
||||||
import io.modelcontextprotocol.spec.McpClientSession;
|
import io.modelcontextprotocol.spec.McpClientSession;
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
|
|
@ -34,14 +28,17 @@ 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;
|
||||||
|
import io.modelcontextprotocol.spec.McpServerTransportProviderBase;
|
||||||
|
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
|
||||||
import io.modelcontextprotocol.util.Assert;
|
import io.modelcontextprotocol.util.Assert;
|
||||||
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
|
import io.modelcontextprotocol.util.DeafaultMcpUriTemplateManagerFactory;
|
||||||
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
|
import io.modelcontextprotocol.util.McpUriTemplateManagerFactory;
|
||||||
import io.modelcontextprotocol.util.Utils;
|
import io.modelcontextprotocol.util.Utils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
|
@ -420,8 +417,11 @@ public class McpAsyncServer {
|
||||||
// TextContent block.)
|
// TextContent block.)
|
||||||
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
|
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
|
||||||
|
|
||||||
return new CallToolResult(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput())),
|
return CallToolResult.builder()
|
||||||
result.isError(), result.structuredContent());
|
.content(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput())))
|
||||||
|
.isError(result.isError())
|
||||||
|
.structuredContent(result.structuredContent())
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -293,8 +293,11 @@ public class McpStatelessAsyncServer {
|
||||||
// TextContent block.)
|
// TextContent block.)
|
||||||
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
|
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
|
||||||
|
|
||||||
return new CallToolResult(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput())),
|
return CallToolResult.builder()
|
||||||
result.isError(), result.structuredContent());
|
.content(List.of(new McpSchema.TextContent(validation.jsonStructuredOutput())))
|
||||||
|
.isError(result.isError())
|
||||||
|
.structuredContent(result.structuredContent())
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ public class DefaultJsonSchemaValidator implements JsonSchemaValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ValidationResponse validate(Map<String, Object> schema, Map<String, Object> structuredContent) {
|
public ValidationResponse validate(Map<String, Object> schema, Object structuredContent) {
|
||||||
|
|
||||||
Assert.notNull(schema, "Schema must not be null");
|
Assert.notNull(schema, "Schema must not be null");
|
||||||
Assert.notNull(structuredContent, "Structured content must not be null");
|
Assert.notNull(structuredContent, "Structured content must not be null");
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,6 @@ public interface JsonSchemaValidator {
|
||||||
* @return A ValidationResponse indicating whether the validation was successful or
|
* @return A ValidationResponse indicating whether the validation was successful or
|
||||||
* not.
|
* not.
|
||||||
*/
|
*/
|
||||||
ValidationResponse validate(Map<String, Object> schema, Map<String, Object> structuredContent);
|
ValidationResponse validate(Map<String, Object> schema, Object structuredContent);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,6 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
|
@ -23,8 +20,9 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
|
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import io.modelcontextprotocol.util.Assert;
|
import io.modelcontextprotocol.util.Assert;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based on the <a href="http://www.jsonrpc.org/specification">JSON-RPC 2.0
|
* Based on the <a href="http://www.jsonrpc.org/specification">JSON-RPC 2.0
|
||||||
|
|
@ -1508,15 +1506,21 @@ public final class McpSchema {
|
||||||
public record CallToolResult( // @formatter:off
|
public record CallToolResult( // @formatter:off
|
||||||
@JsonProperty("content") List<Content> content,
|
@JsonProperty("content") List<Content> content,
|
||||||
@JsonProperty("isError") Boolean isError,
|
@JsonProperty("isError") Boolean isError,
|
||||||
@JsonProperty("structuredContent") Map<String, Object> structuredContent,
|
@JsonProperty("structuredContent") Object structuredContent,
|
||||||
@JsonProperty("_meta") Map<String, Object> meta) implements Result { // @formatter:on
|
@JsonProperty("_meta") Map<String, Object> meta) implements Result { // @formatter:on
|
||||||
|
|
||||||
// backwards compatibility constructor
|
/**
|
||||||
|
* @deprecated use the builder instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public CallToolResult(List<Content> content, Boolean isError) {
|
public CallToolResult(List<Content> content, Boolean isError) {
|
||||||
this(content, isError, null, null);
|
this(content, isError, (Object) null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// backwards compatibility constructor
|
/**
|
||||||
|
* @deprecated use the builder instead.
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public CallToolResult(List<Content> content, Boolean isError, Map<String, Object> structuredContent) {
|
public CallToolResult(List<Content> content, Boolean isError, Map<String, Object> structuredContent) {
|
||||||
this(content, isError, structuredContent, null);
|
this(content, isError, structuredContent, null);
|
||||||
}
|
}
|
||||||
|
|
@ -1551,7 +1555,7 @@ public final class McpSchema {
|
||||||
|
|
||||||
private Boolean isError = false;
|
private Boolean isError = false;
|
||||||
|
|
||||||
private Map<String, Object> structuredContent;
|
private Object structuredContent;
|
||||||
|
|
||||||
private Map<String, Object> meta;
|
private Map<String, Object> meta;
|
||||||
|
|
||||||
|
|
@ -1566,7 +1570,7 @@ public final class McpSchema {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder structuredContent(Map<String, Object> structuredContent) {
|
public Builder structuredContent(Object structuredContent) {
|
||||||
Assert.notNull(structuredContent, "structuredContent must not be null");
|
Assert.notNull(structuredContent, "structuredContent must not be null");
|
||||||
this.structuredContent = structuredContent;
|
this.structuredContent = structuredContent;
|
||||||
return this;
|
return this;
|
||||||
|
|
@ -1644,7 +1648,7 @@ public final class McpSchema {
|
||||||
* @return a new CallToolResult instance
|
* @return a new CallToolResult instance
|
||||||
*/
|
*/
|
||||||
public CallToolResult build() {
|
public CallToolResult build() {
|
||||||
return new CallToolResult(content, isError, structuredContent, meta);
|
return new CallToolResult(content, isError, (Object) structuredContent, meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1407,7 +1407,7 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
|
|
||||||
// In WebMVC, structured content is returned properly
|
// In WebMVC, structured content is returned properly
|
||||||
if (response.structuredContent() != null) {
|
if (response.structuredContent() != null) {
|
||||||
assertThat(response.structuredContent()).containsEntry("result", 5.0)
|
assertThat((Map<String, Object>) response.structuredContent()).containsEntry("result", 5.0)
|
||||||
.containsEntry("operation", "2 + 3")
|
.containsEntry("operation", "2 + 3")
|
||||||
.containsEntry("timestamp", "2024-01-01T10:00:00Z");
|
.containsEntry("timestamp", "2024-01-01T10:00:00Z");
|
||||||
}
|
}
|
||||||
|
|
@ -1428,6 +1428,65 @@ public abstract class AbstractMcpClientServerIntegrationTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@ValueSource(strings = { "httpclient" })
|
||||||
|
void testStructuredOutputOfObjectArrayValidationSuccess(String clientType) {
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
// Create a tool with output schema that returns an array of objects
|
||||||
|
Map<String, Object> outputSchema = Map
|
||||||
|
.of( // @formatter:off
|
||||||
|
"type", "array",
|
||||||
|
"items", Map.of(
|
||||||
|
"type", "object",
|
||||||
|
"properties", Map.of(
|
||||||
|
"name", Map.of("type", "string"),
|
||||||
|
"age", Map.of("type", "number")),
|
||||||
|
"required", List.of("name", "age"))); // @formatter:on
|
||||||
|
|
||||||
|
Tool calculatorTool = Tool.builder()
|
||||||
|
.name("getMembers")
|
||||||
|
.description("Returns a list of members")
|
||||||
|
.outputSchema(outputSchema)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder()
|
||||||
|
.tool(calculatorTool)
|
||||||
|
.callHandler((exchange, request) -> {
|
||||||
|
return CallToolResult.builder()
|
||||||
|
.structuredContent(List.of(Map.of("name", "John", "age", 30), Map.of("name", "Peter", "age", 25)))
|
||||||
|
.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()) {
|
||||||
|
assertThat(mcpClient.initialize()).isNotNull();
|
||||||
|
|
||||||
|
// Call tool with valid structured output of type array
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("getMembers", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response.isError()).isFalse();
|
||||||
|
|
||||||
|
assertThat(response.structuredContent()).isNotNull();
|
||||||
|
assertThatJson(response.structuredContent()).when(Option.IGNORING_ARRAY_ORDER)
|
||||||
|
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
||||||
|
.isArray()
|
||||||
|
.hasSize(2)
|
||||||
|
.containsExactlyInAnyOrder(json("""
|
||||||
|
{"name":"John","age":30}"""), json("""
|
||||||
|
{"name":"Peter","age":25}"""));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
mcpServer.closeGracefully();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest(name = "{0} : {displayName} ")
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
@ValueSource(strings = { "httpclient" })
|
@ValueSource(strings = { "httpclient" })
|
||||||
void testStructuredOutputWithInHandlerError(String clientType) {
|
void testStructuredOutputWithInHandlerError(String clientType) {
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,67 @@ class HttpServletStatelessIntegrationTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@ValueSource(strings = { "httpclient" })
|
||||||
|
void testStructuredOutputOfObjectArrayValidationSuccess(String clientType) {
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
// Create a tool with output schema that returns an array of objects
|
||||||
|
Map<String, Object> outputSchema = Map
|
||||||
|
.of( // @formatter:off
|
||||||
|
"type", "array",
|
||||||
|
"items", Map.of(
|
||||||
|
"type", "object",
|
||||||
|
"properties", Map.of(
|
||||||
|
"name", Map.of("type", "string"),
|
||||||
|
"age", Map.of("type", "number")),
|
||||||
|
"required", List.of("name", "age"))); // @formatter:on
|
||||||
|
|
||||||
|
Tool calculatorTool = Tool.builder()
|
||||||
|
.name("getMembers")
|
||||||
|
.description("Returns a list of members")
|
||||||
|
.outputSchema(outputSchema)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
McpStatelessServerFeatures.SyncToolSpecification tool = McpStatelessServerFeatures.SyncToolSpecification
|
||||||
|
.builder()
|
||||||
|
.tool(calculatorTool)
|
||||||
|
.callHandler((exchange, request) -> {
|
||||||
|
return CallToolResult.builder()
|
||||||
|
.structuredContent(List.of(Map.of("name", "John", "age", 30), Map.of("name", "Peter", "age", 25)))
|
||||||
|
.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()) {
|
||||||
|
assertThat(mcpClient.initialize()).isNotNull();
|
||||||
|
|
||||||
|
// Call tool with valid structured output of type array
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("getMembers", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response.isError()).isFalse();
|
||||||
|
|
||||||
|
assertThat(response.structuredContent()).isNotNull();
|
||||||
|
assertThatJson(response.structuredContent()).when(Option.IGNORING_ARRAY_ORDER)
|
||||||
|
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
||||||
|
.isArray()
|
||||||
|
.hasSize(2)
|
||||||
|
.containsExactlyInAnyOrder(json("""
|
||||||
|
{"name":"John","age":30}"""), json("""
|
||||||
|
{"name":"Peter","age":25}"""));
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
mcpServer.closeGracefully();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest(name = "{0} : {displayName} ")
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
@ValueSource(strings = { "httpclient" })
|
@ValueSource(strings = { "httpclient" })
|
||||||
void testStructuredOutputWithInHandlerError(String clientType) {
|
void testStructuredOutputWithInHandlerError(String clientType) {
|
||||||
|
|
|
||||||
|
|
@ -1206,7 +1206,9 @@ public class McpSchemaTests {
|
||||||
void testCallToolResult() throws Exception {
|
void testCallToolResult() throws Exception {
|
||||||
McpSchema.TextContent content = new McpSchema.TextContent("Tool execution result");
|
McpSchema.TextContent content = new McpSchema.TextContent("Tool execution result");
|
||||||
|
|
||||||
McpSchema.CallToolResult result = new McpSchema.CallToolResult(Collections.singletonList(content), false);
|
McpSchema.CallToolResult result = McpSchema.CallToolResult.builder()
|
||||||
|
.content(Collections.singletonList(content))
|
||||||
|
.build();
|
||||||
|
|
||||||
String value = mapper.writeValueAsString(result);
|
String value = mapper.writeValueAsString(result);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue