Skip to content
Muhammad Musa
← All writing

Open source / MCP / TypeScript / Performance / Testing · 4 min read

Only send the schema a tool needs

A Notion MCP pull request reduced the tool-list response by pruning unreachable definitions. The interesting work was proving what remained.

Muhammad Musa

The response carried more than the tool used

In the Notion MCP server, each generated tool input schema carried all nine component definitions from the OpenAPI specification. A read tool with no object parameters still received structures for writing blocks, rich text and parents. The converter was attaching a shared inventory where it needed a dependency set.

I opened a pull request to retain only the definitions a tool could reach from its input parameters. On the real specification, the measured tools/list JSON-RPC response fell from 76,259 bytes to 23,749 bytes. The important unit is bytes: I did not measure tokenizer output, model latency or a particular client’s improvement.

Reachability, not a special case for read tools

It would have been easy to remove a few known definitions from a few known tool names. That would have encoded today’s specification in the fix. Instead, the converter starts from the input schema’s references and follows references within each definition until it has reached every dependency.

A visited set makes cycles finite. References inside unions, arrays, additional properties and query parameters still count. The traversal is conservative: a reference-looking field inside a default or enum can keep a definition even if that means retaining more than strictly necessary. Removing a needed structure would be a worse failure than keeping an extra one.

pending = references in the tool parameters
while pending is not empty:
  visit one previously unseen definition
  add its references to pending
keep the visited definitions

Keep the shape, reduce the contents

The patch keeps the $defs key even when it becomes an empty object. Definitions that remain keep their original structure. Return schemas are unchanged, because this change targets the input contracts sent in tools/list. That gives a reviewer a narrower behavior to evaluate.

The tests cover synthetic dependency graphs and the actual generated tool list. The latter starts the proxy, connects an SDK client over an in-memory transport, calls listTools and checks the definitions each tool exposes. A separate walk in the test computes the expected reachable set instead of simply asserting that the helper returns its own output.

What the tests established

Six of the seven new tests failed against the original behavior. With the patch, the recorded run passed 119 tests across 15 files and built successfully. A manual stdio session measured the response itself after initialization, rather than estimating savings from source files.

This does not prove the patch fixes every client report associated with schema size. I did not test the client named in the original issue, and the local run was on Windows with Node 22. The contribution is a bounded improvement with a reproducible measurement and a clear review surface.

The status is part of the story

At the November 4 review, the upstream pull request was open and unmerged. I can describe the patch I wrote and the behavior I measured, but not present it as a released upstream feature.

What I like about this kind of engineering is that the before and after can be inspected. The code sends less irrelevant structure, preserves what a tool needs, and carries tests that fail when those promises stop holding. The reduction matters because the boundary is better defined, not just because the response is smaller.

Sources & further reading

Read the project notes