feat(mcp): add structured tool output behind mcp.output_schema flag - #3136
feat(mcp): add structured tool output behind mcp.output_schema flag#3136asoorm wants to merge 3 commits into
Conversation
Implements MCP structured tool output (spec revision 2025-06-18) in the router MCP server, opt-in via mcp.output_schema.enabled (default false, env MCP_OUTPUT_SCHEMA_ENABLED): - every tool generated from a GraphQL operation declares an outputSchema derived from the operation's selection set (aliases, fragments, @skip/@include/@defer, abstract types, custom scalars); schemas are deliberately permissive so a valid response is never rejected, and a build failure only degrades the tool to schema-less registration - successful tool results additionally carry the response as structuredContent mirroring the text content (also for execute_graphql, which declares no output schema) Behavioral change, applied unconditionally (not gated by the flag): a response body that cannot be a GraphQL response (non-JSON, empty, or literal null) now returns IsError: true instead of a success-looking text result. Spec-valid GraphQL error envelopes and partial results keep their existing IsError semantics. Opt-in because output schemas inflate tools/list payloads and structured content roughly doubles result sizes, both of which consume MCP client context budgets.
Adds a Structured Tool Output section to the MCP tools page and the output_schema.enabled key to the configuration reference (options table, environment variables, full example), including the tools/list and result payload-size tradeoff.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
Router image scan passed✅ No security vulnerabilities found in image: |
WalkthroughAdds the ChangesMCP structured output
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3136 +/- ##
==========================================
+ Coverage 62.37% 62.57% +0.20%
==========================================
Files 262 263 +1
Lines 31003 31232 +229
==========================================
+ Hits 19337 19544 +207
- Misses 10158 10165 +7
- Partials 1508 1523 +15
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
router/pkg/mcpserver/server.go (1)
1024-1030: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winCap the upstream body echoed into the tool result.
The error branch interpolates the whole response body into the tool result text. A proxy error page or an HTML gateway response can be large, and the result is sent to the AI model, where it consumes the context window. Truncate the body before formatting it.
♻️ Proposed truncation
var graphqlResponse *GraphQLResponse if err := json.Unmarshal(body, &graphqlResponse); err != nil || graphqlResponse == nil { + const maxEchoedBodyBytes = 2048 + echoedBody := body + if len(echoedBody) > maxEchoedBodyBytes { + echoedBody = append(echoedBody[:maxEchoedBodyBytes:maxEchoedBodyBytes], []byte("... (truncated)")...) + } return &mcp.CallToolResult{ - Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("Response error: unexpected response from GraphQL endpoint: %s", body)}}, + Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("Response error: unexpected response from GraphQL endpoint: %s", echoedBody)}}, IsError: true, }, nil }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@router/pkg/mcpserver/server.go` around lines 1024 - 1030, Cap the response body before interpolating it into the error text in the GraphQL response parsing branch around json.Unmarshal. Truncate oversized body content to a bounded length while preserving the existing unexpected-response error and IsError behavior, and use the truncated value in fmt.Sprintf rather than the full body.docs-website/router/mcp/tools.mdx (1)
305-305: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSplit the packed sentences and remove the filler word.
Three places pack multiple distinct facts into one sentence. The documentation guidelines require short declarative sentences, structured lists for multiple distinct items, and no filler words. Line 321 also uses "just", which the guidelines list as a filler word to avoid.
✏️ Proposed rewrite
-This lets MCP clients know a tool's response shape ahead of time, validate results against the declared schema, and bind results to typed code instead of re-parsing a text blob. +MCP clients use the output schema to: + +- Know the response shape before calling the tool. +- Validate results against the declared schema. +- Bind results to typed code instead of parsing the text content.<Warning> - Output schemas are included in every `tools/list` response, which grows with the size of your operations' selection - sets, and structured content roughly doubles the size of each successful tool result because the response is carried - both as text and as structured content. Both consume the AI model's context window. Keep this feature disabled (the - default) unless your MCP clients consume output schemas or structured content. + This feature increases payload sizes in two ways. Every `tools/list` response carries the output schemas, which grow + with the size of your operations' selection sets. Every successful tool result carries the response twice, as text and + as structured content. Both consume the AI model's context window. Keep this feature disabled (the default) unless + your MCP clients consume output schemas or structured content. </Warning>-The generated schemas are intentionally permissive: they describe what the router returns without over-constraining it, so a valid GraphQL response is never rejected by a client validating against the schema. Fields behind `@skip`, `@include`, or `@defer` directives and fragments on abstract types are marked optional, and custom scalars accept any JSON value. If a schema cannot be derived for an operation, the tool is still registered, just without an output schema. +The generated schemas are permissive. A valid GraphQL response is never rejected by a client that validates against the schema: + +- Fields behind `@skip`, `@include`, or `@defer` directives are marked optional. +- Fields inside fragments on abstract types are marked optional. +- Custom scalars accept any JSON value. + +If a schema cannot be derived for an operation, the router still registers the tool without an output schema.As per path instructions: "Prefer short, declarative sentences. If a sentence has more than one comma-separated clause, consider splitting it.", "Use structured lists when presenting multiple distinct items. Do not pack them into a single paragraph." and "Avoid filler and hedging words like 'simply', 'just', 'easily'".
Also applies to: 314-321
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs-website/router/mcp/tools.mdx` at line 305, Revise the MCP tool response documentation around the sentence beginning “This lets MCP clients” and the related content through the “just” usage near the end of the section. Split sentences containing multiple comma-separated facts into short declarative sentences, use a structured list for distinct benefits or capabilities, and remove filler words such as “just” without changing the documented behavior.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs-website/router/mcp/configuration.mdx`:
- Line 46: Qualify the output_schema.enabled documentation to state that
operation tools declare a derived output schema when schema derivation succeeds,
while retaining the tool without outputSchema when derivation fails. Apply this
wording consistently in docs-website/router/mcp/configuration.mdx at line 46 and
the configuration comment in router/pkg/config/config.go at lines 1365-1367.
---
Nitpick comments:
In `@docs-website/router/mcp/tools.mdx`:
- Line 305: Revise the MCP tool response documentation around the sentence
beginning “This lets MCP clients” and the related content through the “just”
usage near the end of the section. Split sentences containing multiple
comma-separated facts into short declarative sentences, use a structured list
for distinct benefits or capabilities, and remove filler words such as “just”
without changing the documented behavior.
In `@router/pkg/mcpserver/server.go`:
- Around line 1024-1030: Cap the response body before interpolating it into the
error text in the GraphQL response parsing branch around json.Unmarshal.
Truncate oversized body content to a bounded length while preserving the
existing unexpected-response error and IsError behavior, and use the truncated
value in fmt.Sprintf rather than the full body.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7b338c3c-9047-4d45-9bf2-cd898da78880
📒 Files selected for processing (13)
docs-website/router/mcp/configuration.mdxdocs-website/router/mcp/tools.mdxrouter-tests/protocol/mcp_test.gorouter/core/router.gorouter/pkg/config/config.gorouter/pkg/config/config.schema.jsonrouter/pkg/config/fixtures/full.yamlrouter/pkg/config/testdata/config_defaults.jsonrouter/pkg/config/testdata/config_full.jsonrouter/pkg/mcpserver/response_schema.gorouter/pkg/mcpserver/response_schema_test.gorouter/pkg/mcpserver/server.gorouter/pkg/mcpserver/server_test.go
| | `enable_arbitrary_operations` | Enables the `execute_graphql` built-in tool, allowing clients to run arbitrary GraphQL operations beyond the pre-defined operation set. | `false` | | ||
| | `expose_schema` | Enables the `get_schema` built-in tool, exposing the full GraphQL schema to MCP clients. | `false` | | ||
| | `omit_tool_name_prefix` | When enabled, MCP tool names omit the `execute_operation_` prefix. For example, `GetUser` becomes `get_user` instead of `execute_operation_get_user`. See [Tools - Omitting the Tool Name Prefix](/router/mcp/tools#omitting-the-tool-name-prefix). | `false` | | ||
| | `output_schema.enabled` | When enabled, every operation tool declares an output schema derived from its selection set, and successful tool results additionally carry the response as structured content. Increases `tools/list` and result payload sizes. See [Tools - Structured Tool Output](/router/mcp/tools#structured-tool-output). | `false` | |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Document the schema-less fallback.
Schema derivation can fail. The server then keeps the operation tool registered without outputSchema. The current text says that every operation tool declares an output schema.
docs-website/router/mcp/configuration.mdx#L46-L46: qualify the guarantee and mention the schema-less fallback.router/pkg/config/config.go#L1365-L1367: apply the same qualification to the configuration comment.
📍 Affects 2 files
docs-website/router/mcp/configuration.mdx#L46-L46(this comment)router/pkg/config/config.go#L1365-L1367
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs-website/router/mcp/configuration.mdx` at line 46, Qualify the
output_schema.enabled documentation to state that operation tools declare a
derived output schema when schema derivation succeeds, while retaining the tool
without outputSchema when derivation fails. Apply this wording consistently in
docs-website/router/mcp/configuration.mdx at line 46 and the configuration
comment in router/pkg/config/config.go at lines 1365-1367.
Motivation
MCP clients can't know a tool's response shape ahead of time. Tools declare only an input schema, and results are a single text blob containing the raw GraphQL response as a string. The structured tool output feature of the MCP spec (revision 2025-06-18) closes this gap: tools declare an
outputSchema, and results carry machine-readablestructuredContent.Changes
router/pkg/mcpserver/response_schema.gobuilds a JSON schema for the{"data": ...}response envelope from each operation's selection set. Handles aliases, fragments,@skip/@include/@defer, interface and union type conditions, fragment cycles, custom scalars, and field descriptions. Schemas are permissive by design: a valid response never fails validation. A round-trip test against a real JSON schema validator pins this.mcp.output_schema.enabled, defaultfalse, envMCP_OUTPUT_SCHEMA_ENABLED. When enabled, every operation tool declares an output schema, and successful results carrystructuredContentmirroring the text content.execute_graphqlalso returns structured content; the spec allows this without a declared schema. If a schema can't be built for an operation, the tool is registered without one and a warning is logged.full.yamlfixture, and golden files updated. Flag wired throughcore/router.go.docs-website/router/mcp/tools.mdx, plus the key indocs-website/router/mcp/configuration.mdx.Behavioral change
This part applies whether the flag is on or off. A response body that can't be a GraphQL response (non-JSON, empty, or literal
null) now returnsIsError: true. Before, it was wrapped in a success-looking text result and agents would parse garbage as data. The GraphQL over HTTP spec requires a JSON body, so this is transport breakage, andIsErroris MCP's channel for a failed tool run.GraphQL error envelopes and partial results are unchanged: they returned
IsError: truebefore and still do.Why opt-in
Output schemas grow every
tools/listpayload, and structured content roughly doubles successful result payloads. Both consume client context budgets. Default is off.Test plan
cd router && go test ./pkg/mcpserver/covers the schema builder (24 cases including the validator round-trip), registration fallback, and an 8-case table pinning the result boundary above, plus a flag-off table proving results stay text-only and the error boundary is flag-independentcd router && go test ./pkg/config/golden files showEnabled: falseby default andtruefromfull.yamlcd router-tests && go test ./protocol/ -run 'TestMCP'covers tools/list output schemas on and off, structured content mirroring text on success,execute_graphqlstructured content, none when disabled, none on error resultsNot in scope
AddToolleaves that to the caller, so a permissive schema can never reject a response at runtime.Blockers: None.
Summary by CodeRabbit
MCP_OUTPUT_SCHEMA_ENABLEDenvironment variable.