Skip to content

feat(mcp): structured results ship once, OpenAPI controllers served, version from swagger#2021

Merged
samchon merged 6 commits into
masterfrom
fix/2020-mcp-text-fallback
Jul 12, 2026
Merged

feat(mcp): structured results ship once, OpenAPI controllers served, version from swagger#2021
samchon merged 6 commits into
masterfrom
fix/2020-mcp-text-fallback

Conversation

@samchon

@samchon samchon commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

createMcpServer sent every typed tool result twice: once serialized into a text content block, and once as structuredContent. A client that caps tool-result size counts both copies, so a payload well inside the cap gets rejected at double its real size — #2020 measured a 30 KB result arriving as ~64 KB and dying of it.

Structured results now ship once; the spec's duplicate text copy is opt-in. Along the way the server also learned to serve OpenAPI documents, with the handshake version taken from the swagger.

Changed behavior

createMcpServer(controller, options?: IMcpServerOptions)

export function createMcpServer<Class extends object = any>(
  controller: ILlmController<Class> | IHttpLlmController,
  options?: IMcpServerOptions,
): McpServer;

export interface IMcpServerOptions {
  textFallback?: boolean; // default false
}

Structured output (fixes #2020)

  • Default — a result with a reflected return type ships as structuredContent only (content: []). outputSchema advertisement in tools/list is unchanged.
  • textFallback: true — adds the serialized-JSON text block, the spec's backwards-compatibility fallback for clients that ignore outputSchema.
  • Results with no structured representationvoid methods ("Success"), validation failures, runtime errors — keep their text content in both modes.

The spec's SHOULD on the text copy is a backwards-compatibility recommendation; RFC 2119 allows skipping it for a weighed reason, and the doubled payload against size-capped clients is that reason. A content: "structured" | "text" | "both" knob was rejected: a text-only mode would advertise outputSchema while omitting structuredContent, which the spec forbids.

HTTP controllers

  • createMcpServer accepts IHttpLlmController from HttpLlm.controller(): every OpenAPI operation becomes an MCP tool executing through the controller's executor (or HttpLlm.execute), identical to the @typia/vercel / @typia/langchain adapters.
  • HttpLlm.application() preserves the document's info.version onto the new IHttpLlmApplication.version field, and the MCP handshake announces it as serverInfo.version.
  • A class controller announces "1.0.0" — a TypeScript class has no natural version source.

⚠️ Breaks from v13.0.2: the positional version parameter is gone (IMcpServerOptions holds only textFallback; the version now derives from the controller), and typed results no longer carry the text copy by default — clients that ignore structuredContent need { textFallback: true }.

Root cause

McpControllerRegistrar.handleToolCall unconditionally emitted the text block whenever the reflected return type existed. There was no way to send the payload once short of abandoning createMcpServer for the low-level SDK — which is what #2020's reporter did.

Validation

All local (pnpm --filter ./tests/test-mcp start and the http_llm_application slice of test-utils pass; full matrix on CI):

  • test_mcp_tool_output_schema, test_mcp_tool_markdown_structured_content — default ships structuredContent with empty content.
  • test_mcp_tool_text_fallback_enabled (new) — opt-in adds the text copy serializing the same object.
  • test_mcp_tool_void_result, test_mcp_class_controller_* — void/error/coercion paths keep their text semantics.
  • test_mcp_http_controller_execute (new) — an OpenAPI operation lists with its response outputSchema and executes end-to-end through a mocked executor, no network.
  • test_mcp_http_controller_version (new) — swagger info.version reaches the handshake.
  • test_mcp_create_server_version — a class controller announces the "1.0.0" default.
  • test_http_llm_application_version (new, test-utils) — HttpLlm.application() mirrors info.version onto application.version.
  • Docs updated: website/src/content/docs/utilization/mcp.mdx (signature, structured output, new HTTP controllers section), packages/mcp/README.md.

Fixes #2020

🤖 Generated with Claude Code

https://claude.ai/code/session_018HbtkuUSkLLuqmuibCFsac

…uctured results

createMcpServer sent every typed tool result twice — once as a serialized
JSON text block and once as structuredContent. Clients that cap tool-result
size count both copies, so a payload well inside the cap was rejected at
double its real size. The new IMcpServerOptions.textFallback (default true)
lets a server opt out of the duplicate text copy; results with no
structured representation (void methods, validation and runtime errors)
keep their text content regardless.

Fixes #2020

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HbtkuUSkLLuqmuibCFsac
samchon and others added 3 commits July 13, 2026 01:15
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HbtkuUSkLLuqmuibCFsac
Two trailing optional positional parameters forced callers to spell out
the "1.0.0" default just to reach the options bag. The required
controller stays positional and everything optional lives in
IMcpServerOptions, matching the (controller, options?) shape of
toVercelTools and toLangChainTools.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HbtkuUSkLLuqmuibCFsac
…k opt-in

The spec's duplicate text copy is a backwards-compatibility SHOULD that
doubles every typed result on the wire; size-capped clients count both
copies and reject payloads at double their real size. textFallback now
defaults to false — servers whose clients ignore outputSchema opt in
with { textFallback: true }. Void results, validation failures, and
runtime errors keep their text content as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HbtkuUSkLLuqmuibCFsac
@samchon samchon changed the title feat(mcp): add textFallback option to skip duplicate text copy of structured results feat(mcp): ship structured tool results once by default, text fallback opt-in Jul 12, 2026
samchon and others added 2 commits July 13, 2026 01:33
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HbtkuUSkLLuqmuibCFsac
…rom swagger

createMcpServer now accepts IHttpLlmController: every OpenAPI operation
becomes an MCP tool executing through the controller's executor or
HttpLlm.execute, identical to the sibling vercel/langchain adapters.
HttpLlm.application() preserves the document's info.version onto the new
IHttpLlmApplication.version, and the MCP handshake announces it; a class
controller stays at 1.0.0. IMcpServerOptions drops the version knob and
carries only textFallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018HbtkuUSkLLuqmuibCFsac
@samchon samchon changed the title feat(mcp): ship structured tool results once by default, text fallback opt-in feat(mcp): structured results ship once, OpenAPI controllers served, version from swagger Jul 12, 2026
@samchon samchon merged commit 519abee into master Jul 12, 2026
8 checks passed
@samchon samchon deleted the fix/2020-mcp-text-fallback branch July 12, 2026 17:30
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.

@typia/mcp: a tool with an output schema sends its result twice, and a large payload dies of it

1 participant