[PR #3015] [CLOSED] fix(identity): digest the advertised input schema, examples included #5559

Closed
opened 2026-10-31 01:07:34 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/3015
Author: @hemv-857
Created: 10/13/2026
Status: ❌ Closed

Base: main ← Head: fix-2578-catalog-digest-examples


📝 Commits (1)

  • be14282 fix(identity): digest the advertised input schema, examples included

📊 Changes

2 files changed (+69 additions, -6 deletions)

View changed files

➕ src/catalogIdentity.test.ts (+55 -0)
📝 src/catalogIdentity.ts (+14 -6)

📄 Description

Fixes #2578

The bug

createToolCatalog hashed z.toJSONSchema(contract.inputSchema) directly, but a client is handed the schema that toolInputSchemaWithMetadata projects — and that projection attaches advertisedInputExamples. The digest therefore covered a schema without examples while the catalog served to the client had them.

Editing a tool's examples changed what the model was shown and left both the tool and combined digests byte-identical, so createServerIdentity reported aligned against the original catalog. docs/mcp-contracts.md states the identity covers fields that change what clients receive; examples are exactly that.

The fix

Hash the advertised projection, the same value toolRegistrationOptions hands the SDK, instead of a parallel re-derivation of it:

input_schema: advertisedInputJsonSchema(contract),

where advertisedInputJsonSchema projects toolInputSchemaWithMetadata(contract) at the same draft-2020-12 target z.toJSONSchema defaulted to.

This removes a second implementation of "what the input schema looks like on the wire" rather than adding a third: the digest and the registration now read from one source, so they cannot drift again.

The output schema is deliberately left as it is. toolOutputSchemaWithMetadata adds an $id that is itself a digest of that schema, so an output change already moves the digest through the schema it is derived from — there is no equivalent blind spot to close.

Testing

src/catalogIdentity.test.ts (new), three cases:

  • The bug: drop a tool's examples and assert both digests change. Fails on the parent commit, passes here.
  • Control: flipping returns_target_content still changes both digests, so the fix above cannot regress what was already covered. Passes on both.
  • Stability: two computations of an unchanged catalog agree.

The tool with examples is discovered at runtime rather than named, so a renamed or removed tool cannot silently turn the first case into a no-op.

Verification run locally:

  • npm run test:run -- src/catalogIdentity.test.ts — 3 passed
  • npm run typecheck — clean
  • npm run verify:module-boundaries, npm run verify:test-discovery — clean
  • npx oxlint on both changed files — clean

Nothing asserts a hardcoded digest; tests/boundary/mcp/serverIdentity.test.ts reads CATALOG_IDENTITY.digests at runtime, so it picks up the new value rather than breaking on it.

One note on the pre-push hook

npm run lint currently reports 2 errors on main — max-lines-per-function on tests/boundary/filesystem/artifactComparison.test.ts:346 (202 lines) and src/dotnet/ManagedCilDecoding.test.ts:248 (162 lines), both against a 150-line maximum. Both were introduced by 9630c8f and neither is touched here. Per CONTRIBUTING.md's guidance to keep cosmetic-only work out of useful fixes, I left them alone and pushed past the local hook, but the gate is currently red for everyone. Happy to open a separate issue if that is useful.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/3015 **Author:** [@hemv-857](https://github.com/hemv-857) **Created:** 10/13/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `fix-2578-catalog-digest-examples` --- ### 📝 Commits (1) - [`be14282`](https://github.com/morluto/rea/commit/be1428292b2b3e232d636b77101ec55525d9ca50) fix(identity): digest the advertised input schema, examples included ### 📊 Changes **2 files changed** (+69 additions, -6 deletions) <details> <summary>View changed files</summary> ➕ `src/catalogIdentity.test.ts` (+55 -0) 📝 `src/catalogIdentity.ts` (+14 -6) </details> ### 📄 Description Fixes #2578 ## The bug `createToolCatalog` hashed `z.toJSONSchema(contract.inputSchema)` directly, but a client is handed the schema that `toolInputSchemaWithMetadata` projects — and that projection attaches `advertisedInputExamples`. The digest therefore covered a schema *without* `examples` while the catalog served to the client had them. Editing a tool's examples changed what the model was shown and left both the tool and combined digests byte-identical, so `createServerIdentity` reported `aligned` against the original catalog. `docs/mcp-contracts.md` states the identity covers fields that change what clients receive; examples are exactly that. ## The fix Hash the advertised projection, the same value `toolRegistrationOptions` hands the SDK, instead of a parallel re-derivation of it: ```ts input_schema: advertisedInputJsonSchema(contract), ``` where `advertisedInputJsonSchema` projects `toolInputSchemaWithMetadata(contract)` at the same `draft-2020-12` target `z.toJSONSchema` defaulted to. This removes a second implementation of "what the input schema looks like on the wire" rather than adding a third: the digest and the registration now read from one source, so they cannot drift again. The **output** schema is deliberately left as it is. `toolOutputSchemaWithMetadata` adds an `$id` that is itself a digest of that schema, so an output change already moves the digest through the schema it is derived from — there is no equivalent blind spot to close. ## Testing `src/catalogIdentity.test.ts` (new), three cases: - **The bug**: drop a tool's examples and assert both digests change. Fails on the parent commit, passes here. - **Control**: flipping `returns_target_content` still changes both digests, so the fix above cannot regress what was already covered. Passes on both. - **Stability**: two computations of an unchanged catalog agree. The tool with examples is discovered at runtime rather than named, so a renamed or removed tool cannot silently turn the first case into a no-op. Verification run locally: - `npm run test:run -- src/catalogIdentity.test.ts` — 3 passed - `npm run typecheck` — clean - `npm run verify:module-boundaries`, `npm run verify:test-discovery` — clean - `npx oxlint` on both changed files — clean Nothing asserts a hardcoded digest; `tests/boundary/mcp/serverIdentity.test.ts` reads `CATALOG_IDENTITY.digests` at runtime, so it picks up the new value rather than breaking on it. ## One note on the pre-push hook `npm run lint` currently reports 2 errors on `main` — `max-lines-per-function` on `tests/boundary/filesystem/artifactComparison.test.ts:346` (202 lines) and `src/dotnet/ManagedCilDecoding.test.ts:248` (162 lines), both against a 150-line maximum. Both were introduced by `9630c8f` and neither is touched here. Per `CONTRIBUTING.md`'s guidance to keep cosmetic-only work out of useful fixes, I left them alone and pushed past the local hook, but the gate is currently red for everyone. Happy to open a separate issue if that is useful. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-31 01:07:34 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#5559
No description provided.