Skip to content

mcp: convert tool/prompt schemas eagerly at registration time#1861

Open
ravyg wants to merge 1 commit intomodelcontextprotocol:mainfrom
ravyg:fix/1847-eager-schema-conversion
Open

mcp: convert tool/prompt schemas eagerly at registration time#1861
ravyg wants to merge 1 commit intomodelcontextprotocol:mainfrom
ravyg:fix/1847-eager-schema-conversion

Conversation

@ravyg
Copy link
Copy Markdown

@ravyg ravyg commented Apr 8, 2026

Currently standardSchemaToJsonSchema() is called lazily inside the tools/list request handler, re-converting every tool's schema on every list request. The same applies to prompts via promptArgumentsFromStandardSchema() in the prompts/list handler.

Move the conversion to _createRegisteredTool() / _createRegisteredPrompt() and cache the result on RegisteredTool (inputJsonSchema, outputJsonSchema) and RegisteredPrompt (cachedArguments). The list handlers now read from these cached fields. The update() methods recompute the cache when schemas change.

This:

  • Surfaces schema conversion errors (e.g. cycle detection from fix: inline local $ref in tool inputSchema for LLM consumption #1563) at dev time when the tool is registered, not at runtime when a client first calls tools/list
  • Avoids re-converting identical schemas on every tools/list / prompts/list call
  • Matches the Go SDK and FastMCP, which both process schemas at registration time

Includes regression tests verifying eager conversion at registration, cached reuse across list calls, and cache invalidation on update() for both tools and prompts.

Fixes #1847

Motivation and Context

Quoting the issue (filed by @felixweinberger and labeled ready for work, P2):

Moving this to registerTool() / registerPrompt() time and caching the JSON Schema result on RegisteredTool/RegisteredPrompt would:

  • Surface schema errors (e.g. cycle detection from fix: inline local $ref in tool inputSchema for LLM consumption #1563) at dev time when the tool is registered, not at runtime when a client first calls tools/list
  • Avoid re-converting identical schemas on every tools/list call
  • Match Go SDK and fastmcp, which both process schemas at registration time

The current lazy behavior wastes CPU on every tools/list call (agents poll this frequently) and delays the discovery of schema-conversion bugs until a client first connects, which makes the bug look like a runtime crash instead of a registration error.

How Has This Been Tested?

Six new regression tests were added to test/integration/test/server/mcp.test.ts:

  1. should convert tool schemas eagerly at registration time — asserts tool.inputJsonSchema and tool.outputJsonSchema are populated immediately after registerTool(), with no client connection required.
  2. should reuse cached JSON Schema across tools/list calls — asserts two consecutive tools/list requests return identical JSON Schema content for the same tool.
  3. should re-cache JSON Schema when paramsSchema is updated — asserts tool.update({ paramsSchema }) invalidates and recomputes the cache.
  4. should re-cache JSON Schema when outputSchema is updated — same for outputSchema.
  5. should compute prompt arguments eagerly at registration time — asserts prompt.cachedArguments is populated immediately after registerPrompt().
  6. should re-cache prompt arguments when argsSchema is updated — asserts prompt.update({ argsSchema }) invalidates and recomputes the cache.

The tests were verified to fail on unfixed code (5 of 6 fail — the 6th doesn't exercise the bug path) and pass with the fix.

Full repo verification on the final diff:

  • pnpm typecheck:all — clean
  • pnpm lint:all — clean
  • pnpm build:all — clean
  • pnpm test:all — 1,438 tests pass across all packages (core: 489, client: 350, server: 55, middleware/*: 114, examples/shared: 2, test/integration: 428)

Breaking Changes

None. This is a purely internal optimization:

  • registerTool() / registerPrompt() public signatures and behavior are unchanged.
  • The new fields on RegisteredTool (inputJsonSchema, outputJsonSchema) and RegisteredPrompt (cachedArguments) are marked @hidden in JSDoc and are additive.
  • tools/list and prompts/list responses remain byte-identical for valid schemas.
  • One observable behavior change: schemas that previously failed conversion at first tools/list now throw synchronously inside registerTool() / registerPrompt(). Any code that today registers a tool with a knowingly-broken schema and only fails when a client connects will now fail at registration. We believe this is a net win — it's the explicitly-stated goal in the issue.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

(Documentation: no doc updates needed. The RegisteredTool/RegisteredPrompt cached fields are @hidden. Public registerTool / registerPrompt examples in docs/server.md continue to work without modification, and docs/migration.md doesn't apply because this is a backwards-compatible internal change.)

Additional context

Related: #1563 (where this came up) — the eager-conversion approach surfaces cycle-detection errors at registration time.

The implementation follows the pattern already used elsewhere in this file: cached state on the Registered* object, recomputed inside the existing update() methods alongside the related fields. No new abstractions, no new dependencies.

Currently `standardSchemaToJsonSchema()` is called lazily inside the
`tools/list` request handler, re-converting every tool's schema on every
list request. The same applies to prompts via
`promptArgumentsFromStandardSchema()` in the `prompts/list` handler.

Move the conversion to `_createRegisteredTool()` /
`_createRegisteredPrompt()` and cache the result on `RegisteredTool`
(`inputJsonSchema`, `outputJsonSchema`) and `RegisteredPrompt`
(`cachedArguments`). The list handlers now read from these cached fields.
The `update()` methods recompute the cache when schemas change.

This:
- Surfaces schema conversion errors (e.g. cycle detection from modelcontextprotocol#1563) at
  dev time when the tool is registered, not at runtime when a client first
  calls `tools/list`
- Avoids re-converting identical schemas on every `tools/list` /
  `prompts/list` call
- Matches the Go SDK and FastMCP, which both process schemas at
  registration time

Includes regression tests verifying eager conversion at registration,
cached reuse across list calls, and cache invalidation on `update()`
for both tools and prompts.

Fixes modelcontextprotocol#1847
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 8, 2026

⚠️ No Changeset found

Latest commit: 2edee91

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 8, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@1861

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@1861

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@1861

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@1861

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@1861

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@1861

commit: 2edee91

@ravyg ravyg marked this pull request as ready for review April 8, 2026 06:52
@ravyg ravyg requested a review from a team as a code owner April 8, 2026 06:52
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.

Convert tool/prompt schemas eagerly at register time instead of on tools/list

1 participant