Skip to content

feat(mcp): add structured tool output behind mcp.output_schema flag - #3136

Draft
asoorm wants to merge 3 commits into
mainfrom
ahmet/router-592-mcp-add-structured-tool-output-outputschema
Draft

feat(mcp): add structured tool output behind mcp.output_schema flag#3136
asoorm wants to merge 3 commits into
mainfrom
ahmet/router-592-mcp-add-structured-tool-output-outputschema

Conversation

@asoorm

@asoorm asoorm commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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-readable structuredContent.

Changes

  • New router/pkg/mcpserver/response_schema.go builds 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.
  • New config flag mcp.output_schema.enabled, default false, env MCP_OUTPUT_SCHEMA_ENABLED. When enabled, every operation tool declares an output schema, and successful results carry structuredContent mirroring the text content. execute_graphql also 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.
  • Config JSON schema, full.yaml fixture, and golden files updated. Flag wired through core/router.go.
  • Docs: new Structured Tool Output section in docs-website/router/mcp/tools.mdx, plus the key in docs-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 returns IsError: 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, and IsError is MCP's channel for a failed tool run.

GraphQL error envelopes and partial results are unchanged: they returned IsError: true before and still do.

Why opt-in

Output schemas grow every tools/list payload, and structured content roughly doubles successful result payloads. Both consume client context budgets. Default is off.

mcp:
  enabled: true
  output_schema:
    enabled: true # env: MCP_OUTPUT_SCHEMA_ENABLED

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-independent
  • cd router && go test ./pkg/config/ golden files show Enabled: false by default and true from full.yaml
  • cd router-tests && go test ./protocol/ -run 'TestMCP' covers tools/list output schemas on and off, structured content mirroring text on success, execute_graphql structured content, none when disabled, none on error results

Not in scope

  • Partial results still return their data stringified in the error text. Attaching the envelope as structured content on error results is a cheap follow-up if clients need it.
  • No server-side validation of results against the declared schema. go-sdk v1.7.0's low-level AddTool leaves that to the caller, so a permissive schema can never reject a response at runtime.

Blockers: None.

Summary by CodeRabbit

  • New Features
    • MCP tools can optionally expose output schemas derived from GraphQL operations.
    • Successful MCP tool calls can include structured content matching their response data.
    • Added configuration through YAML or the MCP_OUTPUT_SCHEMA_ENABLED environment variable.
  • Bug Fixes
    • Invalid, empty, or null GraphQL responses are reported as tool errors.
    • Tools remain available when output schema generation cannot be completed.
  • Documentation
    • Added configuration guidance, usage details, supported GraphQL features, and fallback behavior.

asoorm added 2 commits August 4, 2026 10:47
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.
@mintlify

mintlify Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
wundergraphinc 🟢 Ready View Preview Aug 4, 2026, 9:57 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Router image scan passed

✅ No security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-450a3adb3824d2fabaebe8e1ce02f589c39cc641

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds the mcp.output_schema.enabled configuration. When enabled, MCP operation tools expose GraphQL-derived output schemas and successful calls return structured content. The change includes schema generation, response validation, router wiring, tests, and documentation.

Changes

MCP structured output

Layer / File(s) Summary
Configuration and router wiring
router/pkg/config/..., router/core/router.go, docs-website/router/mcp/configuration.mdx
Adds the mcp.output_schema.enabled setting, its environment variable, defaults, fixtures, schema definition, examples, and router propagation.
GraphQL response-schema generation
router/pkg/mcpserver/response_schema.go, router/pkg/mcpserver/response_schema_test.go
Builds JSON Schemas from GraphQL selections, fragments, directives, aliases, interfaces, unions, enums, scalars, and nested objects. Tests cover schema construction, invalid fields, fragment cycles, and valid response validation.
MCP server output integration
router/pkg/mcpserver/server.go, router/pkg/mcpserver/server_test.go
Registers derived schemas when enabled, falls back without a schema after derivation errors, validates response bodies, and returns structured content for successful responses.
Protocol coverage and documentation
router-tests/protocol/mcp_test.go, docs-website/router/mcp/tools.mdx
Documents structured tool output and tests enabled, disabled, arbitrary GraphQL, successful, and error-result behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically summarizes the main change: adding structured MCP tool output behind a configuration flag.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.20339% with 42 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.57%. Comparing base (64eaf60) to head (c7d38ab).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
router/pkg/mcpserver/response_schema.go 79.80% 25 Missing and 17 partials ⚠️
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     
Files with missing lines Coverage Δ
router/core/router.go 71.00% <100.00%> (+0.01%) ⬆️
router/pkg/config/config.go 83.00% <ø> (ø)
router/pkg/mcpserver/server.go 74.91% <100.00%> (+4.35%) ⬆️
router/pkg/mcpserver/response_schema.go 79.80% <79.80%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
router/pkg/mcpserver/server.go (1)

1024-1030: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Cap 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 win

Split 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

📥 Commits

Reviewing files that changed from the base of the PR and between bbf752b and 741fe08.

📒 Files selected for processing (13)
  • docs-website/router/mcp/configuration.mdx
  • docs-website/router/mcp/tools.mdx
  • router-tests/protocol/mcp_test.go
  • router/core/router.go
  • router/pkg/config/config.go
  • router/pkg/config/config.schema.json
  • router/pkg/config/fixtures/full.yaml
  • router/pkg/config/testdata/config_defaults.json
  • router/pkg/config/testdata/config_full.json
  • router/pkg/mcpserver/response_schema.go
  • router/pkg/mcpserver/response_schema_test.go
  • router/pkg/mcpserver/server.go
  • router/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` |

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.

🎯 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant