-
Notifications
You must be signed in to change notification settings - Fork 522
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
Open
pantanurag555
wants to merge
4
commits into
modelcontextprotocol:main
Choose a base branch
from
pantanurag555:output-schema
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+327
−42
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
7011142
feat: Add Tool.outputSchema and CallToolResult.structuredContent
pantanurag555 31f453e
Minor fixes/changes
pantanurag555 aaf7add
Store optional json schemas in cache
pantanurag555 f088e75
Merge remote-tracking branch 'upstream/main' into output-schema
pantanurag555 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,15 +5,29 @@ | |
package io.modelcontextprotocol.client; | ||
|
||
import java.time.Duration; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
|
||
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 | ||
|
@@ -62,14 +76,28 @@ public class McpSyncClient implements AutoCloseable { | |
|
||
private final McpAsyncClient delegate; | ||
|
||
/** JSON object mapper for message serialization/deserialization */ | ||
protected ObjectMapper objectMapper; | ||
|
||
/** | ||
* Create a new McpSyncClient with the given delegate. | ||
* @param delegate the asynchronous kernel on top of which this synchronous client | ||
* provides a blocking API. | ||
*/ | ||
McpSyncClient(McpAsyncClient delegate) { | ||
this(delegate, new ObjectMapper()); | ||
} | ||
|
||
/** | ||
* Create a new McpSyncClient with the given delegate. | ||
* @param delegate the asynchronous kernel on top of which this synchronous client | ||
* provides a blocking API. | ||
* @param objectMapper the object mapper for JSON serialization/deserialization | ||
*/ | ||
McpSyncClient(McpAsyncClient delegate, ObjectMapper objectMapper) { | ||
Assert.notNull(delegate, "The delegate can not be null"); | ||
this.delegate = delegate; | ||
this.objectMapper = objectMapper; | ||
} | ||
|
||
/** | ||
|
@@ -206,7 +234,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 +244,54 @@ 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. |
||
ConcurrentHashMap<String, Optional<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"); | ||
} | ||
Optional<McpSchema.JsonSchema> optOutputSchema = toolsOutputSchemaCache.get(callToolRequest.name()); | ||
if (result != null && optOutputSchema != null && optOutputSchema.isPresent()) { | ||
if (result.structuredContent() == null) { | ||
throw new McpError("CallToolResult validation failed: structuredContent is null and " | ||
+ "does not match tool outputSchema."); | ||
} | ||
McpSchema.JsonSchema outputSchema = optOutputSchema.get(); | ||
|
||
try { | ||
// Convert outputSchema to string | ||
String outputSchemaString = this.objectMapper.writeValueAsString(outputSchema); | ||
|
||
// Create JsonSchema validator | ||
ObjectNode schemaNode = (ObjectNode) this.objectMapper.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 = this.objectMapper.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 warning if output schema can't be parsed to prevent erroring out | ||
// for successful call tool request | ||
logger.warn("Failed to validate CallToolResult: Error parsing tool outputSchema: {}", e); | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
/** | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 comment
The 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.