Skip to content

Implements outputSchema validation - #566

Merged
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation
Dec 8, 2025
Merged

Implements outputSchema validation#566
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Nov 28, 2025

Copy link
Copy Markdown
Member

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")]
fnsub(&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")]fnsub(&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")]fnsub(&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-actionsgithub-actionsBot 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
@DaleSeoDaleSeo changed the title Implements outputSchema validation to enforce MCP specification requirementImplements outputSchema validationNov 28, 2025
@github-actionsgithub-actionsBot added T-dependencies Dependencies related changes T-config Configuration file changes T-examples Example code changes labels Nov 29, 2025
@DaleSeo
DaleSeo marked this pull request as ready for review November 29, 2025 19:22
@DaleSeo

Copy link
Copy Markdown
MemberAuthor

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

@DaleSeoDaleSeo closed this Dec 4, 2025
@DaleSeoDaleSeo reopened this Dec 4, 2025
alexhancock
alexhancock previously approved these changes Dec 8, 2025

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
MemberAuthor

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-actionsgithub-actionsBot added the T-test Testing related changes label Dec 8, 2025
@github-actionsgithub-actionsBot removed the T-test Testing related changes label Dec 8, 2025
@alexhancock
alexhancock merged commit df84555 into modelcontextprotocol:mainDec 8, 2025
11 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Dec 4, 2025
@oriyadid

Copy link
Copy Markdown

Thank you for fixing this, much appreciated!

@github-actionsgithub-actionsBot 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
takumi-earth pushed a commit to earthlings-dev/rmcp that referenced this pull request Jan 27, 2026
* feat: implement output schema validation
* fix: calculator example comply MCP spec
* refactor: merge cached_schema_for_output into schema_for_output
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-configConfiguration file changesT-coreCore library changesT-dependenciesDependencies related changesT-examplesExample code changesT-handlerHandler implementation changesT-macrosMacro changesT-modelModel/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

@DaleSeo@oriyadid@alexhancock
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Implements outputSchema validation by DaleSeo · Pull Request #566 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Implements outputSchema validation - #566

Merged
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation
Dec 8, 2025
Merged

Implements outputSchema validation#566
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Nov 28, 2025

Copy link
Copy Markdown
Member

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")]
fnsub(&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")]fnsub(&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")]fnsub(&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-actionsgithub-actionsBot 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
@DaleSeoDaleSeo changed the title Implements outputSchema validation to enforce MCP specification requirementImplements outputSchema validationNov 28, 2025
@github-actionsgithub-actionsBot added T-dependencies Dependencies related changes T-config Configuration file changes T-examples Example code changes labels Nov 29, 2025
@DaleSeo
DaleSeo marked this pull request as ready for review November 29, 2025 19:22
@DaleSeo

Copy link
Copy Markdown
MemberAuthor

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

@DaleSeoDaleSeo closed this Dec 4, 2025
@DaleSeoDaleSeo reopened this Dec 4, 2025
alexhancock
alexhancock previously approved these changes Dec 8, 2025

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
MemberAuthor

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-actionsgithub-actionsBot added the T-test Testing related changes label Dec 8, 2025
@github-actionsgithub-actionsBot removed the T-test Testing related changes label Dec 8, 2025
@alexhancock
alexhancock merged commit df84555 into modelcontextprotocol:mainDec 8, 2025
11 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Dec 4, 2025
@oriyadid

Copy link
Copy Markdown

Thank you for fixing this, much appreciated!

@github-actionsgithub-actionsBot 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
takumi-earth pushed a commit to earthlings-dev/rmcp that referenced this pull request Jan 27, 2026
* feat: implement output schema validation
* fix: calculator example comply MCP spec
* refactor: merge cached_schema_for_output into schema_for_output
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-configConfiguration file changesT-coreCore library changesT-dependenciesDependencies related changesT-examplesExample code changesT-handlerHandler implementation changesT-macrosMacro changesT-modelModel/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

@DaleSeo@oriyadid@alexhancock
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Implements outputSchema validation by DaleSeo · Pull Request #566 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Implements outputSchema validation - #566

Merged
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation
Dec 8, 2025
Merged

Implements outputSchema validation#566
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Nov 28, 2025

Copy link
Copy Markdown
Member

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")]
fnsub(&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")]fnsub(&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")]fnsub(&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-actionsgithub-actionsBot 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
@DaleSeoDaleSeo changed the title Implements outputSchema validation to enforce MCP specification requirementImplements outputSchema validationNov 28, 2025
@github-actionsgithub-actionsBot added T-dependencies Dependencies related changes T-config Configuration file changes T-examples Example code changes labels Nov 29, 2025
@DaleSeo
DaleSeo marked this pull request as ready for review November 29, 2025 19:22
@DaleSeo

Copy link
Copy Markdown
MemberAuthor

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

@DaleSeoDaleSeo closed this Dec 4, 2025
@DaleSeoDaleSeo reopened this Dec 4, 2025
alexhancock
alexhancock previously approved these changes Dec 8, 2025

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
MemberAuthor

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-actionsgithub-actionsBot added the T-test Testing related changes label Dec 8, 2025
@github-actionsgithub-actionsBot removed the T-test Testing related changes label Dec 8, 2025
@alexhancock
alexhancock merged commit df84555 into modelcontextprotocol:mainDec 8, 2025
11 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Dec 4, 2025
@oriyadid

Copy link
Copy Markdown

Thank you for fixing this, much appreciated!

@github-actionsgithub-actionsBot 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
takumi-earth pushed a commit to earthlings-dev/rmcp that referenced this pull request Jan 27, 2026
* feat: implement output schema validation
* fix: calculator example comply MCP spec
* refactor: merge cached_schema_for_output into schema_for_output
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-configConfiguration file changesT-coreCore library changesT-dependenciesDependencies related changesT-examplesExample code changesT-handlerHandler implementation changesT-macrosMacro changesT-modelModel/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

@DaleSeo@oriyadid@alexhancock
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Implements outputSchema validation by DaleSeo · Pull Request #566 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Implements outputSchema validation - #566

Merged
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation
Dec 8, 2025
Merged

Implements outputSchema validation#566
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Nov 28, 2025

Copy link
Copy Markdown
Member

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")]
fnsub(&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")]fnsub(&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")]fnsub(&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-actionsgithub-actionsBot 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
@DaleSeoDaleSeo changed the title Implements outputSchema validation to enforce MCP specification requirementImplements outputSchema validationNov 28, 2025
@github-actionsgithub-actionsBot added T-dependencies Dependencies related changes T-config Configuration file changes T-examples Example code changes labels Nov 29, 2025
@DaleSeo
DaleSeo marked this pull request as ready for review November 29, 2025 19:22
@DaleSeo

Copy link
Copy Markdown
MemberAuthor

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

@DaleSeoDaleSeo closed this Dec 4, 2025
@DaleSeoDaleSeo reopened this Dec 4, 2025
alexhancock
alexhancock previously approved these changes Dec 8, 2025

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
MemberAuthor

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-actionsgithub-actionsBot added the T-test Testing related changes label Dec 8, 2025
@github-actionsgithub-actionsBot removed the T-test Testing related changes label Dec 8, 2025
@alexhancock
alexhancock merged commit df84555 into modelcontextprotocol:mainDec 8, 2025
11 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Dec 4, 2025
@oriyadid

Copy link
Copy Markdown

Thank you for fixing this, much appreciated!

@github-actionsgithub-actionsBot 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
takumi-earth pushed a commit to earthlings-dev/rmcp that referenced this pull request Jan 27, 2026
* feat: implement output schema validation
* fix: calculator example comply MCP spec
* refactor: merge cached_schema_for_output into schema_for_output
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-configConfiguration file changesT-coreCore library changesT-dependenciesDependencies related changesT-examplesExample code changesT-handlerHandler implementation changesT-macrosMacro changesT-modelModel/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

@DaleSeo@oriyadid@alexhancock
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Implements outputSchema validation by DaleSeo · Pull Request #566 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Implements outputSchema validation - #566

Merged
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation
Dec 8, 2025
Merged

Implements outputSchema validation#566
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Nov 28, 2025

Copy link
Copy Markdown
Member

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")]
fnsub(&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")]fnsub(&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")]fnsub(&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-actionsgithub-actionsBot 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
@DaleSeoDaleSeo changed the title Implements outputSchema validation to enforce MCP specification requirementImplements outputSchema validationNov 28, 2025
@github-actionsgithub-actionsBot added T-dependencies Dependencies related changes T-config Configuration file changes T-examples Example code changes labels Nov 29, 2025
@DaleSeo
DaleSeo marked this pull request as ready for review November 29, 2025 19:22
@DaleSeo

Copy link
Copy Markdown
MemberAuthor

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

@DaleSeoDaleSeo closed this Dec 4, 2025
@DaleSeoDaleSeo reopened this Dec 4, 2025
alexhancock
alexhancock previously approved these changes Dec 8, 2025

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
MemberAuthor

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-actionsgithub-actionsBot added the T-test Testing related changes label Dec 8, 2025
@github-actionsgithub-actionsBot removed the T-test Testing related changes label Dec 8, 2025
@alexhancock
alexhancock merged commit df84555 into modelcontextprotocol:mainDec 8, 2025
11 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Dec 4, 2025
@oriyadid

Copy link
Copy Markdown

Thank you for fixing this, much appreciated!

@github-actionsgithub-actionsBot 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
takumi-earth pushed a commit to earthlings-dev/rmcp that referenced this pull request Jan 27, 2026
* feat: implement output schema validation
* fix: calculator example comply MCP spec
* refactor: merge cached_schema_for_output into schema_for_output
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-configConfiguration file changesT-coreCore library changesT-dependenciesDependencies related changesT-examplesExample code changesT-handlerHandler implementation changesT-macrosMacro changesT-modelModel/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

@DaleSeo@oriyadid@alexhancock
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Implements outputSchema validation by DaleSeo · Pull Request #566 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Implements outputSchema validation - #566

Merged
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation
Dec 8, 2025
Merged

Implements outputSchema validation#566
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Nov 28, 2025

Copy link
Copy Markdown
Member

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")]
fnsub(&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")]fnsub(&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")]fnsub(&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-actionsgithub-actionsBot 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
@DaleSeoDaleSeo changed the title Implements outputSchema validation to enforce MCP specification requirementImplements outputSchema validationNov 28, 2025
@github-actionsgithub-actionsBot added T-dependencies Dependencies related changes T-config Configuration file changes T-examples Example code changes labels Nov 29, 2025
@DaleSeo
DaleSeo marked this pull request as ready for review November 29, 2025 19:22
@DaleSeo

Copy link
Copy Markdown
MemberAuthor

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

@DaleSeoDaleSeo closed this Dec 4, 2025
@DaleSeoDaleSeo reopened this Dec 4, 2025
alexhancock
alexhancock previously approved these changes Dec 8, 2025

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
MemberAuthor

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-actionsgithub-actionsBot added the T-test Testing related changes label Dec 8, 2025
@github-actionsgithub-actionsBot removed the T-test Testing related changes label Dec 8, 2025
@alexhancock
alexhancock merged commit df84555 into modelcontextprotocol:mainDec 8, 2025
11 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Dec 4, 2025
@oriyadid

Copy link
Copy Markdown

Thank you for fixing this, much appreciated!

@github-actionsgithub-actionsBot 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
takumi-earth pushed a commit to earthlings-dev/rmcp that referenced this pull request Jan 27, 2026
* feat: implement output schema validation
* fix: calculator example comply MCP spec
* refactor: merge cached_schema_for_output into schema_for_output
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-configConfiguration file changesT-coreCore library changesT-dependenciesDependencies related changesT-examplesExample code changesT-handlerHandler implementation changesT-macrosMacro changesT-modelModel/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

@DaleSeo@oriyadid@alexhancock
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Implements outputSchema validation by DaleSeo · Pull Request #566 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Implements outputSchema validation - #566

Merged
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation
Dec 8, 2025
Merged

Implements outputSchema validation#566
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Nov 28, 2025

Copy link
Copy Markdown
Member

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")]
fnsub(&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")]fnsub(&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")]fnsub(&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-actionsgithub-actionsBot 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
@DaleSeoDaleSeo changed the title Implements outputSchema validation to enforce MCP specification requirementImplements outputSchema validationNov 28, 2025
@github-actionsgithub-actionsBot added T-dependencies Dependencies related changes T-config Configuration file changes T-examples Example code changes labels Nov 29, 2025
@DaleSeo
DaleSeo marked this pull request as ready for review November 29, 2025 19:22
@DaleSeo

Copy link
Copy Markdown
MemberAuthor

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

@DaleSeoDaleSeo closed this Dec 4, 2025
@DaleSeoDaleSeo reopened this Dec 4, 2025
alexhancock
alexhancock previously approved these changes Dec 8, 2025

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
MemberAuthor

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-actionsgithub-actionsBot added the T-test Testing related changes label Dec 8, 2025
@github-actionsgithub-actionsBot removed the T-test Testing related changes label Dec 8, 2025
@alexhancock
alexhancock merged commit df84555 into modelcontextprotocol:mainDec 8, 2025
11 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Dec 4, 2025
@oriyadid

Copy link
Copy Markdown

Thank you for fixing this, much appreciated!

@github-actionsgithub-actionsBot 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
takumi-earth pushed a commit to earthlings-dev/rmcp that referenced this pull request Jan 27, 2026
* feat: implement output schema validation
* fix: calculator example comply MCP spec
* refactor: merge cached_schema_for_output into schema_for_output
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-configConfiguration file changesT-coreCore library changesT-dependenciesDependencies related changesT-examplesExample code changesT-handlerHandler implementation changesT-macrosMacro changesT-modelModel/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

@DaleSeo@oriyadid@alexhancock
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Implements outputSchema validation by DaleSeo · Pull Request #566 · modelcontextprotocol/rust-sdk · GitHub
Skip to content

Implements outputSchema validation - #566

Merged
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation
Dec 8, 2025
Merged

Implements outputSchema validation#566
alexhancock merged 3 commits into
modelcontextprotocol:mainfrom
DaleSeo:outputschema-validation

Conversation

@DaleSeo

@DaleSeoDaleSeo commented Nov 28, 2025

Copy link
Copy Markdown
Member

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")]
fnsub(&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")]fnsub(&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")]fnsub(&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-actionsgithub-actionsBot 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
@DaleSeoDaleSeo changed the title Implements outputSchema validation to enforce MCP specification requirementImplements outputSchema validationNov 28, 2025
@github-actionsgithub-actionsBot added T-dependencies Dependencies related changes T-config Configuration file changes T-examples Example code changes labels Nov 29, 2025
@DaleSeo
DaleSeo marked this pull request as ready for review November 29, 2025 19:22
@DaleSeo

Copy link
Copy Markdown
MemberAuthor

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

@DaleSeoDaleSeo closed this Dec 4, 2025
@DaleSeoDaleSeo reopened this Dec 4, 2025
alexhancock
alexhancock previously approved these changes Dec 8, 2025

@alexhancockalexhancock left a comment

Copy link
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
MemberAuthor

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-actionsgithub-actionsBot added the T-test Testing related changes label Dec 8, 2025
@github-actionsgithub-actionsBot removed the T-test Testing related changes label Dec 8, 2025
@alexhancock
alexhancock merged commit df84555 into modelcontextprotocol:mainDec 8, 2025
11 checks passed
@github-actionsgithub-actionsBot mentioned this pull request Dec 4, 2025
@oriyadid

Copy link
Copy Markdown

Thank you for fixing this, much appreciated!

@github-actionsgithub-actionsBot 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
takumi-earth pushed a commit to earthlings-dev/rmcp that referenced this pull request Jan 27, 2026
* feat: implement output schema validation
* fix: calculator example comply MCP spec
* refactor: merge cached_schema_for_output into schema_for_output
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-configConfiguration file changesT-coreCore library changesT-dependenciesDependencies related changesT-examplesExample code changesT-handlerHandler implementation changesT-macrosMacro changesT-modelModel/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

@DaleSeo@oriyadid@alexhancock