Skip to content

fix(mcp): circular-ref-safe schema deref, quiet child stderr, structuredContent fallback - #33

Open
itsbrex wants to merge 1 commit into
universal-tool-calling-protocol:mainfrom
itsbrex:fix/mcp-child-stderr-circular-refs-structured-content
Open

fix(mcp): circular-ref-safe schema deref, quiet child stderr, structuredContent fallback#33
itsbrex wants to merge 1 commit into
universal-tool-calling-protocol:mainfrom
itsbrex:fix/mcp-child-stderr-circular-refs-structured-content

Conversation

@itsbrex

@itsbrex itsbrex commented Jul 19, 2026

Copy link
Copy Markdown

Three independent robustness fixes to McpCommunicationProtocol, all battle-tested for several weeks as a patch-package shim over @utcp/mcp 1.1.1→1.1.3 in a code-mode deployment federating 15+ MCP servers / 400+ tools.

1. Circular-reference-safe schema dereferencing

$RefParser.dereference(schema) throws on recursive JSON Schemas (e.g. self-referencing SOQL/SOSL filter grammars returned by Salesforce MCP servers). Because the error propagates before the per-schema try/catch can help every time the same schema is re-encountered, the affected manual's tool discovery degrades. Passing { dereference: { circular: 'ignore' } } keeps the cycle as a live reference and discovery succeeds; acyclic schemas are byte-identical.

2. Child stderr: default 'ignore', opt-in 'inherit'

StdioClientTransport is constructed without a stderr option, so every stdio MCP child inherits the host's stderr and floods the terminal during discovery (banners, telemetry notices, auth chatter — multiplied by N servers).

  • Default is now 'ignore'.
  • UTCP_MCP_CHILD_STDERR=inherit restores the old behavior for debugging.
  • Deliberately not 'pipe': with no reader attached the OS pipe buffer fills and a chatty child deadlocks.

3. structuredContent fallback in _processMcpToolResult

Some MCP servers return results with an empty content array and the payload only in structuredContent (an MCP spec field). Today that collapses to [] and the payload is silently lost. When content is empty and structuredContent != null, return structuredContent. Results that carry content are untouched, and the existing structured_output branch still wins.

Notes

  • One file changed: packages/mcp/src/mcp_communication_protocol.ts.
  • The esbuild JS bundle builds clean with these changes. The tsup dts step currently fails on a clean checkout of main too (an @types/node resolution issue under bun's node_modules layout) — pre-existing and unrelated.
  • Happy to split into three PRs if you prefer smaller reviews.

Summary by cubic

Improve MCP stability by handling circular JSON Schemas, quieting child stderr by default, and preserving results returned via structuredContent. Reduces discovery failures and terminal noise when federating many MCP servers.

  • Bug Fixes
    • Schema deref: use $RefParser.dereference(..., { dereference: { circular: 'ignore' } }) to safely handle recursive schemas without breaking discovery.
    • Child stderr: default to 'ignore'; set UTCP_MCP_CHILD_STDERR=inherit to debug. Not 'pipe' to avoid deadlocks.
    • Tool results: when content is empty and structuredContent exists, return structuredContent so payloads aren’t lost.

Written for commit 7df273b. Summary will update on new commits.

Review in cubic

…redContent fallback

Three independent robustness fixes to McpCommunicationProtocol, all battle-
tested for several weeks as a patch-package shim over @utcp/mcp 1.1.1-1.1.3
in a code-mode deployment federating 15+ MCP servers / 400+ tools:

1. $RefParser.dereference(..., { dereference: { circular: 'ignore' } })
   Recursive JSON Schemas (e.g. self-referencing SOQL/SOSL filter grammars
   from Salesforce MCP servers) throw on dereference and take down the whole
   manual's tool discovery. 'ignore' keeps the cycle as a live reference and
   discovery succeeds.

2. StdioClientTransport stderr: default 'ignore', opt-in 'inherit' via
   UTCP_MCP_CHILD_STDERR=inherit. Child MCP servers inherit the host's stderr
   today and flood the terminal during discovery. Deliberately NOT 'pipe':
   with no reader attached the OS pipe buffer fills and a chatty child
   deadlocks.

3. _processMcpToolResult: when a result carries an empty content array but a
   non-null structuredContent (MCP spec field), return structuredContent
   instead of collapsing to [] and silently losing the payload.

Note: the package's dts build step currently fails on an @types/node
resolution issue on a clean checkout of main as well (bun install layout) —
unrelated to this change; the esbuild JS bundle builds clean.
Copilot AI review requested due to automatic review settings July 19, 2026 12:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR improves the robustness of the MCP integration by making schema dereferencing resilient to recursive JSON Schemas, reducing terminal noise from stdio MCP child processes, and preserving tool-call payloads that are returned via MCP’s structuredContent field.

Changes:

  • Make JSON Schema dereferencing tolerant of circular $ref graphs by setting $RefParser.dereference(..., { dereference: { circular: 'ignore' } }).
  • Default stdio MCP child stderr to 'ignore', with UTCP_MCP_CHILD_STDERR=inherit to opt back into inherited stderr for debugging.
  • Add a fallback in _processMcpToolResult to return structuredContent when content is empty.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 1 file

Re-trigger cubic

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants