Repository navigation
fix(api): the served tools advertise no output schema - #136
Merged
Merged
Conversation
A client compiles a validator from each output schema it lists and refuses a result that no longer matches it. The brain serves MCP without sessions (src/mcp/mcp-routes.ts:80) and announces listChanged: false (src/mcp/mcp-connection.ts:43), so a client that listed the tools before an upgrade refused the new results until it was restarted. - ToolDefinition loses outputSchema and definitionWith builds none. The guide tool takes ToolDefinition, which its own type now equalled. - advertisedSchemaOf goes: nothing outside its own test has called it since f4a7d7f. selfContainedSchemaOf and advertisedSchema stay for the input schemas. - The test helpers lose outputConformsTo and schemasOf. Each test client records the output schemas it compiles a validator from (compiledOutputSchemas) and refuses every result checked against one. - check_lines names the type of its items, so the check that the input schemas are self-contained meets a definition reference. - served-schemas.test.ts, over the real server: every tool of the three endpoints has an input schema and no output schema; the listings are pinned at 40,000, 9,000 and 32,000 bytes (measured 38,792, 8,157 and 30,696, from 152,633, 21,905 and 133,578); and each test client lists the tools, then accepts list_tool_servers, list_brains and execute_spec without compiling an output validator. - The api README says a tool has no outputSchema. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hema The public and the engineering MCP reference say that a result carries a plain-language summary, the output as JSON text and the same output as structured content, and that no tool advertises an output schema. The decision of 2026-10-09 is appended verbatim to the record on speaking to agents, whose status line and index row now say it was amended that day. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tput A registration carried `output`, the JSON Schema document of the operation's output, built by jsonSchemaDocumentOf in operation.ts. Its one reader built the output schema of an MCP tool, which the served tools no longer advertise, so nothing reads it but tests. - RegistrationOf loses `output`, and defineOperation builds none. The operation keeps outputSchema, the effect schema that validates and encodes its output, and `input`, which the input schema of a tool still takes. - The tests that pinned the output document go: its named definitions in operation.test.ts, and the shared definitions of the brain and spec operations in brain-operations.test.ts and spec-operations.test.ts. - The operations README says a registration keeps the JSON Schema of the input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wer it measures - served-schemas.test.ts measured whatever body came back, so a refused request of about 261 bytes passed the three limits. It now checks that each tools/list answer is 200 with the endpoint's 25, 8 or 19 tools beside its byte limit. - The test clients' refusal reads "A result was checked against an advertised output schema", neutral against a server that advertises one. - guides.test.ts no longer repeats the check that the guide tool has no output schema, which the listing tests make for every tool. - The api README lists the testing exports as one list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gures The 2026-10-09 section is replaced with its corrected text, verbatim: tool-definition.ts:55, every operation tool, 24 of the 25 on /mcp, and the schemas' bytes of each listing in place of about 117 KB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- docs/decisions/README.md: main's index, with record 20 and record 10 amended 2026-10-08, and record 16 amended 2026-10-09 from this branch. - answering-what-runs-wait-on.test.ts: main's pinned length of the list_interactions description, 724 characters in five sentences, stays; its checks of the field descriptions of answer_schema, delivery, conversation, answerer and reply_refusals read them from the tool's output schema, which the tools no longer advertise, so they go with their decoder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
What a client saw
A desktop client stayed connected to a brain while the brain was upgraded underneath it. From then on, every call of
list_tool_serversand of the run tools failed in the client with "The connector returned an error or an invalid response". The server logged no error, and its answers were valid.Why it happened
Every operation tool the brain served over MCP, 24 of the 25 on
/mcp, carried anoutputSchema: the operation's output JSON Schema, with every object closed (additionalProperties: false, 150 of them across those 24 tools). An MCP client compiles a validator from each output schema it lists and checks every result'sstructuredContentagainst it, refusing any result that does not match. The official client does this, and so does the desktop client, with a validator of its own.The desktop client had listed the tools once and kept that listing for twenty-one hours. In between, the server behind its bridge process was replaced by a newer build in which two tools answered in a new shape, so the client checked new results against old shapes and refused them on every call until it was restarted.
The protocol has one way to tell a client that the tools changed, a
notifications/tools/list_changedmessage over a session's stream. The brain serves MCP without sessions and announceslistChanged: false, so that message cannot reach a client, and a restart of the brain is invisible to a client behind a bridge. Because the schemas were closed, even a field added to a result would have broken every connected client.Nothing that talks to the brain reads those schemas: agents read the text blocks of a result, other programs call the HTTP API, and the MCP reference describes the results themselves.
What changes on the wire
/mcp,/orgs/{org}/mcpor/orgs/{org}/brains/{brain}/mcpcarries anoutputSchema, so a client compiles no output validator and has nothing to refuse a result against.tools/listanswer over HTTP, in local mode with no model provider or tool server configured:/mcp/orgs/{org}/mcp/orgs/{org}/brains/{brain}/mcpWhat does not change
inputSchema, self-contained with its definitions, and its title, description and annotations. A client that still holds an old input schema after an upgrade sends old arguments, and the brain's refusal says what is wrong.structuredContent, which the protocol allows without an output schema. A failure is stillisErrorwith plain words and the problem document as JSON text.A client connected before this change
A client that listed the tools before this change still holds the old listing, output schemas included, and checks results against them until it is restarted once more, together with any bridge process it connects through. After that one restart, no later upgrade of the brain can fail a result this way.
The code
packages/api/src/tools/tool-definition.ts:ToolDefinitionhas nooutputSchema, and a tool's definition builds none. The guide tool now usesToolDefinitiontoo, since its own definition type differed only by lacking that field.advertisedSchemaOfintools/tool-schema.ts, which nothing outside its own test called, is gone;advertisedSchemaandselfContainedSchemaOfstay for the input schemas.packages/api/src/testing:outputConformsToandschemasOfare gone. Each test client, the official client on both protocol revisions it speaks and on its previous major line, records incompiledOutputSchemasthe output schemas it compiles a validator from, and refuses every result it checks against one.packages/operations: an operation keeps no JSON Schema document of its output.Registration.outputhad one reader, the tool's output schema, so it is gone andoperation.tsno longer makes it; the operation keepsoutputSchema, the effect schema that validates and encodes its output, andinput, the JSON Schema document the tool's input schema is built from. The operations README says a registration keeps the JSON Schema of the input, and the three tests that pinned the output document go: its named definitions inoperation.test.ts, and the shared definitions of the brain and the spec operations inbrain-operations.test.tsandspec-operations.test.ts.list_interactionsoutput schema keeps its checks of the tool's description.Tests
packages/server/src/mcp/served-schemas.test.ts, new, over the real server: every tool on the three endpoints carries an input schema with an object root and no output schema (25, 8 and 19 tools); eachtools/listanswer is checked to be200with that many tools and pinned at most 40,000, 9,000 and 32,000 bytes; and each test client lists the tools, then callslist_tool_servers,list_brainsandexecute_spec, compiles no output validator, and accepts each result, whose first text block is plain words without internal terms, whose second is the JSON of itsstructuredContent.packages/api/src/mcp/mcp-clients.test.ts: each client lists the tools and accepts a command's and a query's results without compiling an output validator; and, against a small server that does advertise an output schema, each client compiles a validator from it and refuses the result, the failure this change removes.check_linestest operation names the type of its items, so the check that input schemas are self-contained meets a definition reference.pnpm checkpasses without PostgreSQL: 55 of 55 tasks, with format, lint, typecheck, 100% coverage per file, knip and sherif; 5,122 tests pass and 183 skip, the ones that need a PostgreSQL server, which this change does not reach; the 30 documentation checks pass.Documents