Skip to content

Conversation

@DaleSeo
Copy link
Contributor

@DaleSeo DaleSeo commented Nov 28, 2025

Fixes #532

Implements validation to enforce MCP specification requirement that tool outputSchema must have a root type of "object". Tools using structured output (Json<T>) where T is a primitive type will fail at compile time with clear error messages.

Motivation and Context

As reported in issue #532, the MCP specification requires tool outputSchema to have a root type of "object" (see MCP Tool Schema). The Rust SDK did not validate this, allowing spec-violating schemas to be generated.

2025-11-28 at 17 28 42

The latest spec published on Nov 25 states this more clearly:

2025-11-28 at 17 29 45

How Has This Been Tested?

Noticed the sub tool in the calculator example in the repo doesn't comply with the MCP spec, which causes an issue with MCP Insepctor.

#[tool(description = "Calculate the difference of two numbers")]
fn sub(&self, Parameters(SubRequest { a, b }): Parameters<SubRequest>) -> Json<i32> {
Json(a - b)
}

2025-11-29 at 13 56 37

Now that the output schema validation is in place, the server panics during startup:

➜  servers git:(outputschema-validation) ✗ cargo run -p mcp-server-examples --example servers_calculator_stdio
   Compiling mcp-server-examples v0.1.5 (/Users/dale/work/rust-sdk/examples/servers)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.31s
     Running `/Users/dale/work/rust-sdk/target/debug/examples/servers_calculator_stdio`
2025-11-29T18:50:44.901451Z  INFO servers_calculator_stdio: Starting Calculator MCP server

thread 'main' panicked at examples/servers/src/common/calculator.rs:46:5:
Invalid output schema for Json<i32>: MCP specification requires tool outputSchema to have root type 'object', but found 'integer'.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

After fixing the calculator example code so that the sub tool returns unstructured output just like the sum tool, the server starts up without any issues and works well with the MCP Inspector.

    #[tool(description = "Calculate the difference of two numbers")]
    fn sub(&self, Parameters(SubRequest { a, b }): Parameters<SubRequest>) -> String {
        (a - b).to_string()
    }
➜  servers git:(outputschema-validation) ✗ cargo run -p mcp-server-examples --example servers_calculator_stdio
   Compiling mcp-server-examples v0.1.5 (/Users/dale/work/rust-sdk/examples/servers)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.41s
     Running `/Users/dale/work/rust-sdk/target/debug/examples/servers_calculator_stdio`
2025-11-29T19:05:00.312995Z  INFO servers_calculator_stdio: Starting Calculator MCP server
2025-11-29 at 14 05 44

As suggested in issue #532, wrapping the primitive type also works as expected.

    #[tool(description = "Calculate the difference of two numbers")]
    fn sub(&self, Parameters(SubRequest { a, b }): Parameters<SubRequest>) -> Json<Wrapper<i32>> {
        Json(Wrapper::new(a - b))
    }
2025-11-29 at 14 14 16

Breaking Changes

Existing valid code works unchanged:

  • Code returning object types continues to work
  • Unstructured output (eg. i32, String) with no output stream continues to work
  • Only spec-violating code (e.g. <Json<i32>, <Json<String>) fails to compile

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

The TypeScript SDK also enforces this requirement in the Tool schema definition here:

2025-11-28 at 17 40 36

@github-actions github-actions bot added T-core Core library changes T-handler Handler implementation changes T-macros Macro changes T-model Model/data structure changes labels Nov 28, 2025
@DaleSeo DaleSeo changed the title Implements outputSchema validation to enforce MCP specification requirement Implements outputSchema validation Nov 28, 2025
@github-actions github-actions bot added T-dependencies Dependencies related changes T-config Configuration file changes T-examples Example code changes labels Nov 29, 2025
@DaleSeo DaleSeo force-pushed the outputschema-validation branch 2 times, most recently from 95984d1 to f30567b Compare November 29, 2025 19:18
@DaleSeo DaleSeo marked this pull request as ready for review November 29, 2025 19:22
@DaleSeo
Copy link
Contributor Author

DaleSeo commented Dec 4, 2025

Hi @jokemanfire, @4t145, @alexhancock, could one of you please review this PR? Thanks! 🙏

@DaleSeo DaleSeo closed this Dec 4, 2025
@DaleSeo DaleSeo force-pushed the outputschema-validation branch from f30567b to 3c62ee8 Compare December 4, 2025 02:58
@DaleSeo DaleSeo reopened this Dec 4, 2025
@DaleSeo DaleSeo force-pushed the outputschema-validation branch from 3399b25 to 3283a0b Compare December 4, 2025 03:03
alexhancock
alexhancock previously approved these changes Dec 8, 2025
Copy link
Collaborator

@alexhancock alexhancock left a comment

Choose a reason for hiding this comment

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

Good catch. LGTM other than the one recommendation!

}

/// Call [`schema_for_output`] with a cache.
pub fn cached_schema_for_output<T: JsonSchema + std::any::Any>() -> Result<Arc<JsonObject>, String>
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think I would just fold this caching logic into schema_for_output and only have the one public method

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestion, @alexhancock! I've consolidated the caching logic into schema_for_output.

@github-actions github-actions bot added the T-test Testing related changes label Dec 8, 2025
@DaleSeo DaleSeo force-pushed the outputschema-validation branch from 77177f2 to 17e48aa Compare December 8, 2025 21:10
@github-actions github-actions bot removed the T-test Testing related changes label Dec 8, 2025
@alexhancock alexhancock merged commit df84555 into modelcontextprotocol:main Dec 8, 2025
11 checks passed
@github-actions github-actions bot mentioned this pull request Dec 4, 2025
@oriyadid
Copy link

oriyadid commented Dec 8, 2025

Thank you for fixing this, much appreciated!

@github-actions github-actions bot mentioned this pull request Dec 9, 2025
daixijun pushed a commit to daixijun/mcp-rust-sdk that referenced this pull request Dec 9, 2025
* feat: implement output schema validation

* fix: calculator example comply MCP spec

* refactor: merge cached_schema_for_output into schema_for_output
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-config Configuration file changes T-core Core library changes T-dependencies Dependencies related changes T-examples Example code changes T-handler Handler implementation changes T-macros Macro changes T-model Model/data structure changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Structured tool output schema does not follow MCP 2025-06-18.

3 participants