Skip to content

Prototype support for outputSchema/structuredContent #454

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

Merged
merged 18 commits into from
May 15, 2025

Conversation

bhosmer-ant
Copy link
Contributor

@bhosmer-ant bhosmer-ant commented May 6, 2025

Prototype support for schema validation additions proposed in modelcontextprotocol/modelcontextprotocol#371

Design choices (RFC)

  • new McpServer.registerTool() API: uses a config object for all tool properties except name and the tool callback. Old tool() API (optional arguments for all properties + overloads for every combination + argument parsing via type sniffing in implementation) was already stretched to the breaking point, and outputSchema would've been unavoidably ambiguous with this approach due to type overlap.

    • tool() is still supported, but registerTool() is preferred.
  • tool outputSchema caching in Client: a Client needs a tool's outputSchema to do validation of tool call results. This is the first instance of the Client needing to remember something about a tool in order to complete a tool call successfully: previously, the client retained no information from a "tools/list" request, and tool calls would be validated entirely on the server side. This PR does this by caching output schemas from incoming tool listings, which adds state to the Client that must be kept up to date.

    • outputSchemas are cached as a side effect of Client.listTools(), which means that bare requests to "tools/list" will not retain them. This confines support of outputSchema and structuredContent to the high level API.
    • We will need to add "notifications/tools/list_changed" tracking (Added auto-refreshing tool list notification handler to client #239) to ensure validators remain current across tool updates.

Reviewer notes

  • PR is less huge than it looks! Line count is inflated by test changes: exercising the registerTool() API on all existing tests, and addition of comment headers for readability.
  • Change summary:
    • types.ts: added outputSchema to ToolSchema, split CallToolResult into structured and unstructured variants
    • client/: added structuredContent handling (incl. schema validation) and tests
    • server/': added structuredContentand compatibility output, **newtool()api**, tests (new api and outputSchema`)
    • package.json, package-lock.json: added json-schema-to-zod for output validation against outputSchema
    • examples/client: minor changes needed to handle optionality of result.content
    • examples/server: new examples exercising structured output capability

Motivation and Context

See above

How Has This Been Tested?

Added tests for new functionality

Breaking Changes

No

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@bhosmer-ant bhosmer-ant force-pushed the basil/output_schema branch 2 times, most recently from 2ab965e to 3a83e40 Compare May 8, 2025 03:11
@bhosmer-ant bhosmer-ant marked this pull request as ready for review May 8, 2025 03:13
Copy link
Contributor

@ihrpr ihrpr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some concerns around protocol version bump and questions

{ method: "tools/list", params },
ListToolsResultSchema,
options,
);

// Cache the tools and their output schemas for future validation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit worried about this part related to caching tools

there are some efforts to refresh tool list on client like this one. Which will work fine as listTools is used there

I think my question here is, do we need to have cached output types - how expensive it is to convert it on the fly?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think _cachedTools is not used, right?

Copy link
Contributor Author

@bhosmer-ant bhosmer-ant May 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[edited] the problem is that the client isn't actually storing tool definitions anywhere - so unless we grab and stash on an organic listTools call, we'd need to do one prior to the tool call itself (with no filter, and maybe multiple pages of results).

So yeah, this would be the first real connective state between tool definitions and tool calls in the client, I think. (And thus the first state that could go out of sync.)

But I'm not sure it's different in kind from, say, calling a tool based on a stale param schema... though ofc in that case the error would be on the server rather than the client.

@bhosmer-ant bhosmer-ant marked this pull request as draft May 13, 2025 01:31
@bhosmer-ant
Copy link
Contributor Author

drafting until new api is up

@bhosmer-ant bhosmer-ant force-pushed the basil/output_schema branch from 3a83e40 to d3ed226 Compare May 13, 2025 19:12
@bhosmer-ant bhosmer-ant marked this pull request as ready for review May 13, 2025 22:56
// Check if the tool has an outputSchema
const outputSchema = this.getToolOutputSchema(params.name);
if (outputSchema) {
// If tool has outputSchema, it MUST return structuredContent (unless it's an error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we want to relax this. What if an SDK implemented some server changes by mistake - would the client then be unable to use it at all? Is this something we want? I think enforcing those requirements on the server would definitely be a good thing, but I'm not sure it should apply to the client.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it depends on how strictly we want to enforce the "trusted output" aspect of the structured result contract, so I don't think we should unconditionally allow potentially malicious stuff through. But we could make it discretionary if this seems too inflexible - maybe add a strictness/trust flag to RequestOptions?

More radically we could not do validation here at all and have a separate "validate" method that the user of the client can call at will - not sure I feel about making validation opt-in rather than opt-out but it's an option.

}
} else {
// If tool doesn't have outputSchema, it MUST NOT return structuredContent
if (result.structuredContent) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, do we need to throw?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nah you're right this one seems overly pedantic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

followup: this restriction is actually referenced in the spec types. I think we should relax the language there, but keep the type distinction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should relax the language there, but keep the type distinction.

modelcontextprotocol/modelcontextprotocol#532

@@ -0,0 +1,93 @@
#!/usr/bin/env node
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this one, I think mcpServerOutputSchema.ts can be used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, mcpServerOutputSchema.ts and lowLevelOutputSchema.ts are servers (high- and low-level respectively), and this is just a test script that runs a client against either of them.

/**
 * Client to test outputSchema servers
 * This client connects to either the high-level or low-level outputSchema server
 * and calls the get_weather tool to demonstrate structured output
 */

It was useful for me to test the examples, but your call whether it's worth including in the SDK itself!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, should it be move do client folder?

Copy link
Contributor

@ihrpr ihrpr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

  • consider moving testOutputSchemaServers to client folder
  • looks like there is a merge conflict

bhosmer-ant and others added 15 commits May 15, 2025 09:58
- Add outputSchema field to Tool type and RegisteredTool interface
- Make content field optional in CallToolResult
- Update ListToolsRequestSchema handler to include outputSchema in tool list responses
- Add support for structuredContent in tool results
- Update examples to handle optional content field
- Add tests for new outputSchema and structuredContent functionality
- Update ToolCallback documentation to clarify when to use structuredContent vs content

This change enables tools to define structured output schemas and return
structured JSON content, providing better type safety and validation for
tool outputs.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- Cache tool output schemas when listing tools
- Validate structuredContent against outputSchema during callTool
- Enforce that tools with outputSchema must return structuredContent
- Add json-schema-to-zod dependency for schema conversion
- Add comprehensive tests for validation scenarios

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
…tured tool output

- Add outputSchema support to Tool interface with proper documentation
- Split CallToolResult into structured and unstructured variants
- Change structuredContent from string to object type
- Add validation that tools without outputSchema cannot return structuredContent
- Add validation that tools with outputSchema must return structuredContent
- Update client to validate structured content as object (no JSON parsing)
- Update tests to use object format for structuredContent
- Add tests for new validation constraints
- Update LATEST_PROTOCOL_VERSION to DRAFT-2025-v2

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
…patibility

- Update McpServer to support registering tools with outputSchema
- Add automatic content generation from structuredContent for backward compatibility
- Add validation to ensure proper usage of structuredContent vs content
- Add comprehensive tests for outputSchema functionality
- Add example servers demonstrating structured output usage
- Update existing test to match new backward compatibility behavior

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
- fix schema validation
- comment headers on tests
- Remove tool() overload that takes a config object with callback
- Add new registerTool() method that separates config from callback
- Extract common tool registration logic into _createRegisteredTool() helper
- Update all tests to use registerTool() for tools with output schemas
- Update examples to use registerTool() and high-level Client API
- Add comment noting that output schema validation only works with high-level API

This change improves API clarity by using a distinct method for tools with
output schemas while maintaining backward compatibility.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
This file is a client implementation that tests outputSchema servers,
so it belongs in the client examples directory rather than server examples.
Updated file paths in error messages to reflect the new location.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
@bhosmer-ant bhosmer-ant force-pushed the basil/output_schema branch from 1363c86 to f470f09 Compare May 15, 2025 14:16
@bhosmer-ant bhosmer-ant enabled auto-merge May 15, 2025 14:21
@bhosmer-ant bhosmer-ant merged commit 1fd6265 into main May 15, 2025
5 checks passed
@bhosmer-ant bhosmer-ant deleted the basil/output_schema branch May 15, 2025 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants