Skip to content

fix(api): the served tools advertise no output schema - #136

Merged
rami-hatoum merged 6 commits into
mainfrom
fix/tools-without-output-schema
Oct 9, 2026
Merged

rami-hatoum merged 6 commits into
mainfrom
fix/tools-without-output-schema

Conversation

@rami-hatoum

@rami-hatoum rami-hatoum commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

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_servers and 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 an outputSchema: 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's structuredContent against 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_changed message over a session's stream. The brain serves MCP without sessions and announces listChanged: 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

  • No tool on /mcp, /orgs/{org}/mcp or /orgs/{org}/brains/{brain}/mcp carries an outputSchema, so a client compiles no output validator and has nothing to refuse a result against.
  • Each listing shrinks by the schemas' bytes. Measured as the bytes of the tools/list answer over HTTP, in local mode with no model provider or tool server configured:
Endpoint Before After
/mcp 152,633 38,792
/orgs/{org}/mcp 21,905 8,157
/orgs/{org}/brains/{brain}/mcp 133,578 30,696

What does not change

  • Every tool keeps its 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.
  • Every successful result still carries its plain-language summary as the first text block, the output as JSON text as the second, and the same output as structuredContent, which the protocol allows without an output schema. A failure is still isError with plain words and the problem document as JSON text.
  • The brain still serves MCP without sessions and announces no change to its tools. The HTTP API is untouched.

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: ToolDefinition has no outputSchema, and a tool's definition builds none. The guide tool now uses ToolDefinition too, since its own definition type differed only by lacking that field. advertisedSchemaOf in tools/tool-schema.ts, which nothing outside its own test called, is gone; advertisedSchema and selfContainedSchemaOf stay for the input schemas.
  • packages/api/src/testing: outputConformsTo and schemasOf are gone. Each test client, the official client on both protocol revisions it speaks and on its previous major line, records in compiledOutputSchemas the 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.output had one reader, the tool's output schema, so it is gone and operation.ts no longer makes it; the operation keeps outputSchema, the effect schema that validates and encodes its output, and input, 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 in operation.test.ts, and the shared definitions of the brain and the spec operations in brain-operations.test.ts and spec-operations.test.ts.
  • The tests that checked a result against the listed output schema no longer do; the one that pinned the length of a field description inside the list_interactions output 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); each tools/list answer is checked to be 200 with that many tools and pinned at most 40,000, 9,000 and 32,000 bytes; and each test client lists the tools, then calls list_tool_servers, list_brains and execute_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 its structuredContent.
  • 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.
  • The listing tests of the api package check that no tool carries an output schema, and the check_lines test operation names the type of its items, so the check that input schemas are self-contained meets a definition reference.
  • pnpm check passes 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

  • The MCP reference, public and engineering, and the api README say that a result carries the plain-language summary, the output as JSON text and the structured content, and that no tool advertises an output schema.
  • The design record on what the brain says to agents gains a dated section with this decision, and its status line and index row say so.

rami-hatoum and others added 6 commits October 9, 2026 08:17
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>
@rami-hatoum
rami-hatoum merged commit b140126 into main Oct 9, 2026
18 checks passed
@rami-hatoum
rami-hatoum deleted the fix/tools-without-output-schema branch October 9, 2026 09:10
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.

1 participant