Skip to content

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

Draft
asoorm wants to merge 5 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 5 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

The router MCP server exposes GraphQL operations as MCP tools. Today, these tools declare only an input schema. MCP clients do not know the shape of a tool result before they call the tool. Results are one text blob that contains 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. Results carry machine-readable structuredContent. This PR adds opt-in support for both to the router MCP server.

Changes

  • The new file router/pkg/mcpserver/response_schema.go builds a JSON schema for the {"data": ...} response envelope. It derives the schema from the selection set of each operation. It supports:
    • aliases and fragments
    • @skip, @include, and @defer
    • interface and union type conditions
    • fragment cycles
    • custom scalars
    • field descriptions
  • The schemas are permissive by design. A valid GraphQL response always passes validation against its schema. A round-trip test against a real JSON schema validator verifies this.
  • The new config flag mcp.output_schema.enabled controls the feature. The default is false. The environment variable is MCP_OUTPUT_SCHEMA_ENABLED.
  • When the flag is on, each operation tool declares an output schema. Successful results carry structuredContent that mirrors the text content. The execute_graphql built-in tool has no declared output schema, but its successful results also carry structured content. The spec permits structured content without a declared schema.
  • If the router cannot build a schema for an operation, it registers the tool without an output schema and logs a warning.
  • The config JSON schema, the full.yaml fixture, and the config golden files are updated. The flag is wired through core/router.go.
  • Docs: a new Structured Tool Output section in docs-website/router/mcp/tools.mdx, and the new key in docs-website/router/mcp/configuration.mdx.

Behavioral change: non-GraphQL response bodies are now tool errors

The MCP server runs each tool call as a request against the router GraphQL endpoint. This change applies when the flag is on and when it is off. A response body that is not a GraphQL response (non-JSON, empty, or literal null) now returns IsError: true. Before this change, the router wrapped such a body in a text result that looked successful. Agents then parsed garbage as data. The GraphQL over HTTP spec requires a JSON body, so a non-JSON body is a transport failure, not a GraphQL result. IsError is the MCP channel for a failed tool run.

GraphQL error envelopes and partial results do not change. They returned IsError: true before this change. They still do.

Why opt-in

Output schemas make every tools/list payload larger. Structured content roughly doubles the payload of each successful result. Both consume the context budget of the client. The 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)
    • the fallback that registers a tool without an output schema when schema derivation fails
    • an 8-case table for the error boundary described in "Behavioral change" above
    • a flag-off table that proves results stay text-only and that the error boundary does not depend on the flag
  • 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:
    • output schemas in tools/list with the flag on and off
    • structured content that mirrors text on success
    • structured content from execute_graphql
    • no structured content when the flag is off
    • no structured content on error results

Not in scope

  • Partial results still return their data as a string in the error text. A follow-up can attach the envelope as structured content on error results if clients need it.
  • The router does not validate results against the declared schema on the server side. The low-level AddTool in go-sdk v1.7.0 leaves validation to the caller. A permissive schema never rejects 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 YAML and MCP_OUTPUT_SCHEMA_ENABLED configuration; the feature is disabled by default.
  • 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-nonroot image scan passed

✅ No security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-ddc9d00ac2f2e55a416bd9e4f7c4221f27c5364f-nonroot

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 799408fd-396f-4911-b202-89d5241d5da5

📥 Commits

Reviewing files that changed from the base of the PR and between 1ebc609 and d0bdaf5.

📒 Files selected for processing (1)
  • router-tests/protocol/mcp_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • router-tests/protocol/mcp_test.go

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 configuration types, environment mapping, defaults, fixtures, schema definitions, 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 construction, invalid fields, fragment cycles, and response validation.
MCP server output integration
router/pkg/mcpserver/server.go, router/pkg/mcpserver/server_test.go
Registers derived schemas when enabled, preserves tools 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: opt-in structured MCP tool output controlled by an output schema configuration flag.
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.
✨ 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.58%. Comparing base (64eaf60) to head (d0bdaf5).
⚠️ 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.58%   +0.20%     
==========================================
  Files         262      263       +1     
  Lines       31003    31232     +229     
==========================================
+ Hits        19337    19545     +208     
- Misses      10158    10165       +7     
- Partials     1508     1522      +14     
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 5 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

Comment thread docs-website/router/mcp/configuration.mdx Outdated
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