Skip to content

Publish only referenced $defs in MCP tool schemas - #8553

Merged
tim-smart merged 11 commits into
Effect-TS:mainfrom
Floriferous:mcp-unreferenced-defs
Oct 21, 2026
Merged

tim-smart merged 11 commits into
Effect-TS:mainfrom
Floriferous:mcp-unreferenced-defs

Conversation

@Floriferous

@Floriferous Floriferous commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Type

  • Refactor
  • Feature
  • Bug Fix
  • Optimization
  • Documentation Update

Description

MCP needs an object root, so toolJsonSchema inlines a top-level $ref. It then keeps every definition, including the one it just inlined. So Tool.make("GetNote", { success: Note }), where Note is a Schema.Class, publishes:

{
  "type": "object",
  "properties": { "id": { "type": "string" } },
  "required": ["id"],
  "additionalProperties": true,
  "$defs": { "NoteEncoded": { "type": "object", "properties": { "id": { "type": "string" } }, "required": ["id"], "additionalProperties": true } }
}

Nothing references NoteEncoded. Every tool with a Class or an identified result or parameters sends its schema twice on each tools/list.

toolJsonSchema now passes a referencePolicy that compiles a non-recursive root inline instead of as a definition, so it is published once. Other definitions are unchanged: Array(Note) still gets its Note definition, and a recursive root keeps its definition with the top-level $ref inlined, as before.

Compatibility: a custom toJsonSchema that writes a $ref into the root's own definition, such as #/$defs/Note/properties/title when Note is the tool schema, no longer resolves, because that definition is no longer published. References to other definitions are unaffected.

Tests:

  • Two snapshot tests that included the unused IdentifiedResult definition are updated.
  • The McpServer snapshot test also covers a Schema.Class root with a Date field (no EventEncoded definition) and a recursive root that keeps its Tree definition.
  • A test compiles the published output schema with AJV and validates a value when a field references an $anchor in another definition.

OpenAiStructuredOutput and AnthropicStructuredOutput still publish the root definition the same way; they can adopt the same policy in a follow-up.

Validated with pnpm lint-fix, tsc -b for packages/effect, and pnpm test --run packages/effect/test/ai packages/effect/test/schema.

Related

Closes EFF-2698

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5ceb9ae

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 33 packages
Name Type
effect Patch
@effect/opentelemetry Patch
@effect/vitest Patch
@effect/ai-anthropic Patch
@effect/ai-cloudflare Patch
@effect/ai-openai-compat Patch
@effect/ai-openai Patch
@effect/ai-openrouter Patch
@effect/ai-typesafe Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node-shared Patch
@effect/platform-node Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/graphql-generator Patch
@effect/openapi-generator Patch

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
arbitrary-combinators.ts 39.13 KB 39.13 KB 0.00 KB (0.00%)
basic.ts 7.04 KB 7.04 KB 0.00 KB (0.00%)
batching.ts 10.91 KB 10.91 KB 0.00 KB (0.00%)
brand.ts 6.71 KB 6.71 KB 0.00 KB (0.00%)
cache.ts 11.35 KB 11.35 KB 0.00 KB (0.00%)
config.ts 22.51 KB 22.51 KB 0.00 KB (0.00%)
differ.ts 21.66 KB 21.66 KB 0.00 KB (0.00%)
http-client.ts 26.02 KB 26.02 KB 0.00 KB (0.00%)
http-router.ts 35.69 KB 35.69 KB 0.00 KB (0.00%)
logger.ts 11.70 KB 11.70 KB 0.00 KB (0.00%)
metric.ts 9.27 KB 9.27 KB 0.00 KB (0.00%)
optic.ts 6.93 KB 6.93 KB 0.00 KB (0.00%)
pubsub.ts 15.79 KB 15.79 KB 0.00 KB (0.00%)
queue.ts 12.61 KB 12.61 KB 0.00 KB (0.00%)
schedule.ts 11.60 KB 11.60 KB 0.00 KB (0.00%)
schema-bigdecimal.ts 13.84 KB 13.84 KB 0.00 KB (0.00%)
schema-binary.ts 40.58 KB 40.58 KB 0.00 KB (0.00%)
schema-class.ts 21.36 KB 21.36 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 32.33 KB 32.33 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 27.52 KB 27.52 KB 0.00 KB (0.00%)
schema-string-transformation.ts 15.09 KB 15.09 KB 0.00 KB (0.00%)
schema-string.ts 12.10 KB 12.10 KB 0.00 KB (0.00%)
schema-template-literal.ts 16.02 KB 16.02 KB 0.00 KB (0.00%)
schema-toArbitrary.ts 38.66 KB 38.66 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 25.76 KB 25.76 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 20.59 KB 20.59 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 20.77 KB 20.77 KB 0.00 KB (0.00%)
schema-toFormatter.ts 20.88 KB 20.88 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 25.46 KB 25.46 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 20.88 KB 20.88 KB 0.00 KB (0.00%)
schema.ts 20.55 KB 20.55 KB 0.00 KB (0.00%)
stm.ts 13.89 KB 13.89 KB 0.00 KB (0.00%)
stream.ts 10.14 KB 10.14 KB 0.00 KB (0.00%)

Comment thread packages/effect/src/ai/McpServer.ts Outdated
Comment on lines +1960 to +1976
const reachableDefinitions = (
schema: JsonSchema.JsonSchema,
definitions: JsonSchema.Definitions
): Set<string> => {
const reached = new Set<string>()
const visit = (schema: JsonSchema.JsonSchema): void => {
JsonSchema.rewriteRefs(schema, ($ref) => {
const key = JsonSchema.getReferenceKey($ref)
if (key !== undefined && !reached.has(key) && Object.hasOwn(definitions, key)) {
reached.add(key)
visit(definitions[key])
}
return $ref
})
}
visit(schema)
return reached

@lloydrichards lloydrichards Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: I think this can remove a definition that is still in use

getReferenceKey recognizes #/$defs/Root, but misses #/$defs/Root/properties/value. This means we delete Root even though the schema still points to part of it.

A custom toJsonSchema callback can produce this reference. I tested it: AJV accepts the schema before pruning and rejects it afterward because Root is missing.

question: Could we keep Root when a reference points anywhere inside it, and add a test for that case? Or might be something to look into with JsonSchema so we don't need to do this pruning in McpServer?

@tim-smart
tim-smart enabled auto-merge (squash) October 21, 2026 22:05
@tim-smart
tim-smart merged commit 688d302 into Effect-TS:main Oct 21, 2026
16 of 18 checks passed

This branch is waiting to be deployed

1 waiting deployment
fork — 5ceb9ae1 Waiting Oct 21, 2026 by effect-bot via approval-gate #22355
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.

4 participants