-
Notifications
You must be signed in to change notification settings - Fork 531
feat: Add Tool.outputSchema and CallToolResult.structuredContent #302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
7011142
31f453e
aaf7add
f088e75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/local/home/pantanur/workspace/java-sdk/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -202,6 +202,13 @@ | |
<scope>test</scope> | ||
</dependency> | ||
|
||
<!-- Json validator dependency --> | ||
<dependency> | ||
<groupId>com.networknt</groupId> | ||
<artifactId>json-schema-validator</artifactId> | ||
<version>1.5.7</version> | ||
</dependency> | ||
Comment on lines
+205
to
+210
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth getting extra clarification from maintainers on if we want schema validation as part of this or a separate effort, along the lines of the discussion in #271. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discussed some of the issues offline with @LucaButBoring. I wanted to get clarity around the scope of the work on the issue that I had created. There are a few questions that we might want to answer regarding the output validation:
Would like inputs from the maintainers to decide on the best way to proceed. |
||
|
||
</dependencies> | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,15 +5,28 @@ | |
package io.modelcontextprotocol.client; | ||
|
||
import java.time.Duration; | ||
import java.util.HashMap; | ||
import java.util.Set; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import com.fasterxml.jackson.databind.node.ObjectNode; | ||
import com.networknt.schema.JsonSchema; | ||
import com.networknt.schema.JsonSchemaFactory; | ||
import com.networknt.schema.SpecVersion; | ||
import com.networknt.schema.ValidationMessage; | ||
|
||
import io.modelcontextprotocol.spec.McpError; | ||
import io.modelcontextprotocol.spec.McpSchema; | ||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; | ||
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest; | ||
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; | ||
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult; | ||
import io.modelcontextprotocol.util.Assert; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
/** | ||
* A synchronous client implementation for the Model Context Protocol (MCP) that wraps an | ||
|
@@ -55,6 +68,8 @@ public class McpSyncClient implements AutoCloseable { | |
|
||
private static final Logger logger = LoggerFactory.getLogger(McpSyncClient.class); | ||
|
||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); | ||
pantanurag555 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// TODO: Consider providing a client config to set this properly | ||
// this is currently a concern only because AutoCloseable is used - perhaps it | ||
// is not a requirement? | ||
|
@@ -206,7 +221,8 @@ public Object ping() { | |
/** | ||
* Calls a tool provided by the server. Tools enable servers to expose executable | ||
* functionality that can interact with external systems, perform computations, and | ||
* take actions in the real world. | ||
* take actions in the real world. If tool contains an output schema, validates the | ||
* tool result structured content against the output schema. | ||
* @param callToolRequest The request containing: - name: The name of the tool to call | ||
* (must match a tool name from tools/list) - arguments: Arguments that conform to the | ||
* tool's input schema | ||
|
@@ -215,7 +231,53 @@ public Object ping() { | |
* Boolean indicating if the execution failed (true) or succeeded (false/absent) | ||
*/ | ||
public McpSchema.CallToolResult callTool(McpSchema.CallToolRequest callToolRequest) { | ||
return this.delegate.callTool(callToolRequest).block(); | ||
McpSchema.CallToolResult result = this.delegate.callTool(callToolRequest).block(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: Shift to sync instead. |
||
HashMap<String, McpSchema.JsonSchema> toolsOutputSchemaCache = this.delegate.getToolsOutputSchemaCache(); | ||
// Should not be triggered but added for completeness | ||
if (!toolsOutputSchemaCache.containsKey(callToolRequest.name())) { | ||
throw new McpError("Tool with name '" + callToolRequest.name() + "' not found"); | ||
} | ||
if (result != null && toolsOutputSchemaCache.get(callToolRequest.name()) != null) { | ||
if (result.structuredContent() == null) { | ||
throw new McpError("CallToolResult validation failed: structuredContent is null and " | ||
+ "does not match tool outputSchema."); | ||
} | ||
|
||
McpSchema.JsonSchema outputSchema = toolsOutputSchemaCache.get(callToolRequest.name()); | ||
|
||
try { | ||
// Convert outputSchema to string | ||
String outputSchemaString = OBJECT_MAPPER.writeValueAsString(outputSchema); | ||
|
||
// Create JsonSchema validator | ||
ObjectNode schemaNode = (ObjectNode) OBJECT_MAPPER.readTree(outputSchemaString); | ||
// Set additional properties to false if not specified in output schema | ||
if (!schemaNode.has("additionalProperties")) { | ||
schemaNode.put("additionalProperties", false); | ||
} | ||
JsonSchema schema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012) | ||
.getSchema(schemaNode); | ||
|
||
// Convert structured content in reult to JsonNode | ||
JsonNode jsonNode = OBJECT_MAPPER.valueToTree(result.structuredContent()); | ||
|
||
// Validate outputSchema against structuredContent | ||
Set<ValidationMessage> validationResult = schema.validate(jsonNode); | ||
|
||
// Check if validation passed | ||
if (!validationResult.isEmpty()) { | ||
// Handle validation errors | ||
throw new McpError( | ||
"CallToolResult validation failed: structuredContent does not match tool outputSchema."); | ||
} | ||
} | ||
catch (JsonProcessingException e) { | ||
// Log error if output schema can't be parsed to prevent erroring out for | ||
// successful call tool request | ||
logger.error("Encountered exception when parsing outputSchema: {}", e); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth getting other opinions on this, I think this should be rethrown (potentially wrapped in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. For now, changing this to log a warning. This leaves the option open for the client side implementation to proceed as it deems fit. I would like feedback from others about what would be the ideal behavior in this scenario. |
||
} | ||
return result; | ||
} | ||
|
||
/** | ||
|
@@ -353,4 +415,8 @@ public McpSchema.CompleteResult completeCompletion(McpSchema.CompleteRequest com | |
return this.delegate.completeCompletion(completeRequest).block(); | ||
} | ||
|
||
private void isStrict(boolean b) { | ||
pantanurag555 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
throw new UnsupportedOperationException("Not supported yet."); | ||
} | ||
|
||
} |
Uh oh!
There was an error while loading. Please reload this page.