feat(mcp): structured results ship once, OpenAPI controllers served, version from swagger#2021
Merged
Conversation
…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
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
createMcpServersent every typed tool result twice: once serialized into atextcontent block, and once asstructuredContent. 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
Structured output (fixes #2020)
structuredContentonly (content: []).outputSchemaadvertisement intools/listis unchanged.textFallback: true— adds the serialized-JSON text block, the spec's backwards-compatibility fallback for clients that ignoreoutputSchema.voidmethods ("Success"), validation failures, runtime errors — keep their text content in both modes.The spec's
SHOULDon 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. Acontent: "structured" | "text" | "both"knob was rejected: a text-only mode would advertiseoutputSchemawhile omittingstructuredContent, which the spec forbids.HTTP controllers
createMcpServeracceptsIHttpLlmControllerfromHttpLlm.controller(): every OpenAPI operation becomes an MCP tool executing through the controller's executor (orHttpLlm.execute), identical to the@typia/vercel/@typia/langchainadapters.HttpLlm.application()preserves the document'sinfo.versiononto the newIHttpLlmApplication.versionfield, and the MCP handshake announces it asserverInfo.version."1.0.0"— a TypeScript class has no natural version source.Root cause
McpControllerRegistrar.handleToolCallunconditionally emitted the text block whenever the reflected return type existed. There was no way to send the payload once short of abandoningcreateMcpServerfor the low-level SDK — which is what #2020's reporter did.Validation
All local (
pnpm --filter ./tests/test-mcp startand thehttp_llm_applicationslice of test-utils pass; full matrix on CI):test_mcp_tool_output_schema,test_mcp_tool_markdown_structured_content— default shipsstructuredContentwith emptycontent.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 responseoutputSchemaand executes end-to-end through a mocked executor, no network.test_mcp_http_controller_version(new) — swaggerinfo.versionreaches 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()mirrorsinfo.versionontoapplication.version.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