Skip to content

fix: preserve inputSchema $defs/definitions when converting preloaded MCP tools - #1030

Open
akushonkamen wants to merge 3 commits into
truefoundry:mainfrom
akushonkamen:fix/945-mcp-inputschema-defs
Open

akushonkamen wants to merge 3 commits into
truefoundry:mainfrom
akushonkamen:fix/945-mcp-inputschema-defs

Conversation

@akushonkamen

@akushonkamen akushonkamen commented Oct 10, 2026 •

Copy link
Copy Markdown

Summary

convertMCPServersToTools rebuilt the function-tool parameters from only inputSchema.properties/required (packages/trueforge-core/src/core/mcp/convertMCPServers.ts:86-87 at 078808f4), dropping the top-level $defs / legacy definitions containers while keeping the $refs that point into them. FastMCP-style servers (official MCP Python SDK) hoist every nested pydantic model to top-level $defs, so any preloaded tool with structured args reached the LLM with dangling #/$defs/... refs — failing provider schema validation or hiding the argument shape from the model.

Fix: carry both containers over only when present (one conditional spread each, convertMCPServers.ts:79-92), so schemas without them are emitted byte-identically to before.

Fixes #945

Changes

  • packages/trueforge-core/src/core/mcp/convertMCPServers.ts: pass inputSchema.$defs and draft-04 inputSchema.definitions through into the function-tool parameters, each only when present.
  • packages/trueforge-core/tests/core/mcp/convertMCPServersSchema.test.ts: regression cases — FastMCP-style ($defs + #/$defs/Address ref), draft-04 definitions, and no-container schema; each local $ref is asserted resolvable, and the default (no-container) output is asserted unchanged.

How was this tested?

  • New test file verified red before the fix and green after:
    • red (base 078808f4 + this test file): timeout 180 pnpm jest --config jest.config.cjs tests/core/mcp/convertMCPServersSchema.test.ts → 2 failed ($defs/definitions lost), 1 passed / 3 total
    • green (this branch): same command → 3 passed / 3 total
  • timeout 300 pnpm jest --config jest.config.cjs tests/core/mcp → 6 suites passed, 40 passed / 40 total
  • Full trueforge-core: timeout 600 pnpm jest --config jest.config.cjs → 51 suites passed, 507 passed + 1 skipped / 508 total, exit 0
  • Monorepo: pnpm build exit 0, pnpm test exit 0, pnpm typecheck exit 0, pnpm lint:ci exit 0 (2 pre-existing warnings in an unrelated trueforge-ui file), pnpm format:check exit 0

Checklist

  • I have read the contributing guidelines
  • pnpm build, pnpm test, pnpm typecheck, pnpm lint:ci, and pnpm format:check pass locally
  • Tests added/updated where it makes sense
  • No hand-edits to generated code (packages/trueforge-sdk, python/trueforge_sdk, .github/fern/openapi/openapi.json, docs/openapi.json) — fork PRs omit SDK regen; maintainers regenerate after merge
  • Docs / .env.example updated if configuration or behavior changed

Note

Implementation was done with an AI coding agent under my direction (root cause analysis, patch, and tests reviewed by me); also disclosed in the issue claim comment.


Note

Low Risk
Narrow change to preloaded MCP tool schema shaping with conditional spreads and tests; no auth or persistence impact.

Overview
Fixes preloaded MCP tool conversion so JSON Schema $ref pointers keep working when servers hoist nested models into top-level $defs (FastMCP / Python SDK) or legacy definitions (draft-04).

convertMCPServersToTools previously rebuilt function-tool parameters from only properties and required, leaving dangling #/$defs/... refs and broken or incomplete tool schemas for the LLM. It now copies $defs and definitions through only when present, so plain schemas without those keys stay unchanged.

Adds regression tests for container preservation and resolvable local $refs, plus checks that emitted parameters stay within the top-level keywords expected across supported model vendors.

Reviewed by Cursor Bugbot for commit 1d64078. Bugbot is set up for automated code reviews on this repo. Configure here.

@changeset-bot

changeset-bot Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1d64078

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

This PR includes changesets to release 2 packages
Name Type
@truefoundry/trueforge-core Patch
@truefoundry/trueforge 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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bcb5a52. Configure here.

Comment thread packages/trueforge-core/src/core/mcp/convertMCPServers.ts
Comment thread packages/trueforge-core/src/core/mcp/convertMCPServers.ts
akushonkamen added a commit to akushonkamen/trueforge that referenced this pull request Oct 13, 2026
Review ask on truefoundry#1030: check model-vendor compatibility before merging. The
converted parameters flow convertTools() -> jsonSchema() -> the configured
@ai-sdk provider package. Against the pinned provider sources: @ai-sdk/anthropic
preserves both $defs and legacy definitions recursively; @ai-sdk/google passes
unknown keywords (and $defs explicitly) through unstripped; @ai-sdk/openai and
the OpenAI-compatible vendors ship the parameters JSON Schema verbatim. The
tests pin what the converter itself controls: emitted parameters stay on the
vendor-accepted top-level surface (type/properties/required plus the two
container spellings and nothing else), every local $ref resolves inside the
emitted document for both spellings, and container-less schemas are emitted
byte-identically so vendor behavior for them is unchanged.
@akushonkamen

Copy link
Copy Markdown
Author

Vendor compatibility check done (ffb3373) — new packages/trueforge-core/tests/core/mcp/convertMCPServersVendors.test.ts pins the compatibility contract, and I verified the pinned @ai-sdk/* provider sources in this repo's lockfile for how each vendor receives the containers this PR now carries over.

Pipeline: convertMCPServersToTools → convertTools() in src/core/llm/VercelAILLM.ts (wraps parameters via the AI SDK's jsonSchema()) → the provider package from the buildLanguageModel switch (openai, anthropic, google-gemini, moonshot, alibaba + OpenAI-compatible fireworks/zai/together/custom/truefoundry).

Per-vendor findings:

  • anthropic (@ai-sdk/anthropic): its schema sanitizer explicitly preserves both $defs and legacy definitions, recursively, into the tool input_schema. Fully compatible with both spellings.
  • google-gemini (@ai-sdk/google): sanitizeResponseJsonSchema spreads unknown keywords through (...result) and handles $defs explicitly with recursive sanitization, so $ref, definitions and $defs all reach functionDeclarations unstripped. No regression — and strictly better than the pre-fix state, where such tools shipped dangling #/$defs/... refs.
  • openai + OpenAI-compatible (@ai-sdk/openai chat/responses, @ai-sdk/openai-compatible for fireworks, zai, together, custom, truefoundry; also moonshot/alibaba): the tool parameters JSON Schema ships verbatim; $defs/$ref is part of the OpenAI tool-calling schema dialect.

What the tests pin (the part our converter itself controls): emitted parameters stay on the vendor-accepted top-level surface — type/properties/required plus the two container spellings and nothing else; every local $ref resolves inside the emitted schema document for both $defs and draft-04 definitions; and container-less schemas are emitted byte-identically to the pre-change output, so vendor behavior for them cannot change.

tests/core/mcp: 7 suites, 45 passed; tsc --noEmit clean; prettier clean.

@muhammadaliqasmi

Copy link
Copy Markdown

Reporter of #945 here. I re-ran my original reproduction end to end against this branch, including the wire-level step that the vendor check above reasoned about from source.

Setup: the FastMCP-shaped create_contact tool from #945 → convertMCPServersToTools → convertTools() → generateText with @ai-sdk/openai (chat), @ai-sdk/anthropic and @ai-sdk/google (lockfile versions; google 4.0.82). Each provider gets a stub fetch that captures the request body. The check: every local $ref in the tool schema that goes on the wire resolves inside that schema.

provider, field on the wire base 078808f this branch ffb3373
OpenAI tools[].function.parameters #/$defs/Address dangling $defs present, refs resolve
Anthropic tools[].input_schema dangling resolves
Gemini functionDeclarations[].parametersJsonSchema dangling resolves

With the probe added, tests/core/mcp gives 8 suites and 48 passed.

On Gemini: @ai-sdk/google sends the schema as parametersJsonSchema (the JSON Schema field), not as the OpenAPI-subset parameters. $defs/$ref arrive unchanged, which matches the vendor note above. These were stub fetches, not live provider calls, so they show what goes on the wire, not how each API then validates it.

#1046 carries the same source and tests plus the changeset (its diff against ffb3373 touches only .changeset/), so this applies there as well. I'm happy to contribute the wire-level probe as a test if that's useful.

Investigated with AI assistance. The results above come from real runs.

@chiragjn

Copy link
Copy Markdown
Member

@akushonkamen can you please add changeset file and rebase against main? Will merge this after that

@akushonkamen

akushonkamen commented Oct 18, 2026 •

Copy link
Copy Markdown
Author

Thanks @chiragjn! Sure — I'll add a .changeset entry with a patch bump for @truefoundry/trueforge-core (as cursor[bot] also flagged) and rebase the branch against main, then push shortly. Once updated, happy to address anything else that comes up during the vendor compatibility check.

…d MCP tools

convertMCPServersToTools rebuilt function-tool parameters from only
inputSchema.properties/required, dropping top-level $defs/definitions while
keeping the $refs that point into them. FastMCP-style servers (official MCP
Python SDK) hoist every nested pydantic model to top-level $defs, so any
preloaded tool with structured args reached the LLM with dangling
#/defs-style refs — failing provider schema validation or hiding the
argument shape from the model.

Carry both containers over only when present, so schemas without them are
emitted byte-identically to before.

Fixes truefoundry#945
Review ask on truefoundry#1030: check model-vendor compatibility before merging. The
converted parameters flow convertTools() -> jsonSchema() -> the configured
@ai-sdk provider package. Against the pinned provider sources: @ai-sdk/anthropic
preserves both $defs and legacy definitions recursively; @ai-sdk/google passes
unknown keywords (and $defs explicitly) through unstripped; @ai-sdk/openai and
the OpenAI-compatible vendors ship the parameters JSON Schema verbatim. The
tests pin what the converter itself controls: emitted parameters stay on the
vendor-accepted top-level surface (type/properties/required plus the two
container spellings and nothing else), every local $ref resolves inside the
emitted document for both spellings, and container-less schemas are emitted
byte-identically so vendor behavior for them is unchanged.
@akushonkamen
akushonkamen force-pushed the fix/945-mcp-inputschema-defs branch from ffb3373 to 1d64078 Compare October 19, 2026 15:47
@akushonkamen

Copy link
Copy Markdown
Author

Done — both items from your request are in:

  1. Changeset added: .changeset/mcp-preserve-inputschema-defs.md — a patch bump for @truefoundry/trueforge-core, following the existing .changeset/ format, describing the MCP inputSchema $defs/definitions fix (1d64078).
  2. Rebased onto main: branch now sits on aa584420 with no conflicts; history rewritten, so this was a force-push (ffb3373f → 1d640787).

Verification (local): ran the trueforge-core test suite scoped to the MCP/runtime tests via pnpm test -- run tests/core/mcp in packages/trueforge-core — 14 suites / 68 tests passed, including convertMCPServersSchema.test.ts and the vendor-compatibility pin convertMCPServersVendors.test.ts from the earlier check.

This branch has not been deployed

No deployments
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.

Preloaded MCP tools drop inputSchema.$defs, so tool schemas sent to the LLM contain dangling $refs

3 participants