Skip to content

feat(ai-mcp): forward clientOptions to the MCP SDK client - #1070

Merged
AlemTuzlak merged 3 commits into
TanStack:mainfrom
L4Ph:feat/ai-mcp-client-options
Aug 10, 2026
Merged

feat(ai-mcp): forward clientOptions to the MCP SDK client#1070
AlemTuzlak merged 3 commits into
TanStack:mainfrom
L4Ph:feat/ai-mcp-client-options

Conversation

@L4Ph

@L4Ph L4Ph commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Changes

createMCPClient and createMCPClientFromTransport now accept clientOptions, forwarded verbatim to the MCP SDK's Client. They previously built it as new Client({ name, version }) with no second argument, so nothing in ClientOptions was reachable from this package.

The option that motivated this is jsonSchemaValidator.

The SDK validates a tool's structuredContent against its declared outputSchema, and its default AJV provider compiles each schema by building JavaScript source and handing it to new Function. Edge runtimes forbid that. On Cloudflare Workers, connecting to any MCP server whose tools declare an outputSchema fails with Error compiling schema: const schema0 = scope.schema[0]; return function validate0(data, … — AJV's wrapper around Code generation from strings disallowed for this context.

It takes down a whole run rather than one tool call: validators are built in cacheToolMetadata during tools/list, so the throw lands in discovery, before the model has seen a single tool. In practice that means chat({ mcp }) fails on the first turn for every operator who has an MCP server connected.

The SDK already ships the fix — CfWorkerJsonSchemaValidator, backed by the optional peer @cfworker/json-schema — and its own ClientOptions docblock documents installing it. It was simply not installable through this package.

import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker'

const mcp = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  clientOptions: { jsonSchemaValidator: new CfWorkerJsonSchemaValidator() },
})

Additive and optional — omitting it keeps the SDK's defaults. createMCPClients picks it up through MCPClientOptions for free.

Found while running an operator agent on Cloudflare Workers against Sentry / Stripe / Cloudflare Observability MCP servers, all of which declare outputSchema on their tools.

Test plan

Unitpackages/ai-mcp/tests/client.test.ts, against a new in-memory server helper whose tool declares an outputSchema and returns structuredContent (the only path that reaches a validator):

  • forwards a custom jsonSchemaValidator to the SDK client — a recording provider is consulted, and the assertion is on tools() rather than callTool, which is where the SDK actually builds validators.
  • accepts clientOptions through createMCPClient — same, through the public factory.
  • falls back to the SDK default when no clientOptions are given — the payload still round-trips, so the addition changes nothing for existing callers.

E2Etesting/e2e/tests/mcp.spec.ts + an opt-in flag on api.mcp-test:

  • clientOptions reaches the SDK client — a custom validator changes the outcome

    Same mock server, same aimock fixture, one forwardedProps flag. The route installs a validator that REFUSES everything through clientOptions. get_guitar_price returns structuredContent the default AJV provider accepts, so an option dropped on the floor would leave the run indistinguishable from the existing test. With it wired, TOOL_CALL_RESULT carries MCP error -32602: Structured content does not match the tool's output schema: rejected by the injected validator and not the payload.

    The negative assertion is scoped to the tool result rather than the transcript on purpose: the fixture's final answer is a recorded script that names the price whether or not the tool succeeded.

Reproduce:

pnpm run build:all
pnpm --filter @tanstack/ai-mcp test:lib   # 100 passed
pnpm --filter @tanstack/ai-mcp test:types
pnpm test:e2e -- mcp.spec.ts              # 2 passed

pnpm test:pr@tanstack/ai-mcp is clean across test:lib / test:types / test:eslint / test:build. Two unrelated tasks fail on this machine and fail identically on a clean main (verified by stashing): @tanstack/ai-sandbox-local-process:test:lib (3 reaper-conformance cases shelling out to stat -c '%Y %n', which is GNU syntax and not what macOS stat accepts) and one case in @tanstack/ai:test:lib.

pnpm format run; no lockfile changes (no dependencies added).

Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

minor on @tanstack/ai-mcp — new opt-in public API, backwards compatible.

Summary by CodeRabbit

  • New Features

    • Added support for configuring MCP clients with custom client options.
    • Added support for custom JSON Schema validators, including edge-runtime-compatible validators.
    • Preserved client configuration when reconnecting to configured MCP servers or using existing transports.
  • Bug Fixes

    • Improved structured tool output validation and reporting of validator errors.
    • Ensured configured validation behavior is consistently applied across client connections.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a34596a8-dcde-4b14-9c37-2dfc77f8cabe

📥 Commits

Reviewing files that changed from the base of the PR and between 81d29d1 and fad5f41.

📒 Files selected for processing (2)
  • packages/ai-mcp/src/pool.ts
  • packages/ai-mcp/tests/client.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/ai-mcp/src/pool.ts
  • packages/ai-mcp/tests/client.test.ts

📝 Walkthrough

Walkthrough

The MCP client API now accepts optional MCP SDK ClientOptions through both creation functions. Server descriptors preserve these options for later client creation. Unit and E2E tests verify custom JSON Schema validator propagation.

Changes

MCP client options

Layer / File(s) Summary
Client options contract and forwarding
packages/ai-mcp/src/types.ts, packages/ai-mcp/src/client.ts, .changeset/mcp-client-options.md
MCPClientOptions exposes optional SDK clientOptions. Both client creation paths forward these options to the MCP SDK Client. getInfo() reports configured options.
Descriptor and pool preservation
packages/ai-mcp/src/apps/session-store.ts, packages/ai-mcp/src/apps/call-handler.ts, packages/ai-mcp/src/pool.ts
Server descriptors and pooled server metadata retain clientOptions. Per-call client creation forwards the stored options.
Structured tool validation coverage
packages/ai-mcp/tests/helpers/in-memory-server.ts, packages/ai-mcp/tests/client.test.ts
The tests add a schema-validated lookup_user tool and verify custom validator propagation, schema capture, default validation behavior, and getInfo() output.
End-to-end validator propagation
testing/e2e/src/routes/api.mcp-test.ts, testing/e2e/tests/mcp.spec.ts
The E2E route conditionally injects a rejecting validator. The test verifies the validator error in TOOL_CALL_RESULT and the absence of the normal result.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant E2ETest
  participant MCPRoute
  participant MCPClient
  participant MCPValidator
  E2ETest->>MCPRoute: Send rejectStructuredOutput=true
  MCPRoute->>MCPClient: Create client with clientOptions
  MCPClient->>MCPValidator: Validate structured tool output
  MCPValidator-->>MCPClient: Return rejection error
  MCPClient-->>E2ETest: Emit TOOL_CALL_RESULT with validator error
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: forwarding clientOptions to the MCP SDK client.
Description check ✅ Passed The description explains the change, motivation, testing, checklist completion, and release impact, including the required changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ai-mcp/src/client.ts`:
- Around line 110-117: The MCPClientImpl restart metadata currently omits
clientOptions, causing reconstructed clients to lose custom settings such as
jsonSchemaValidator. Update MCPClientImpl.getInfo() to retain clientOptions in
its restart descriptor, then have createMcpAppCallHandler forward that
descriptor value when constructing the replacement Client.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: af5c54ac-22d7-4488-9b6a-1e089d912033

📥 Commits

Reviewing files that changed from the base of the PR and between fdb791a and 750daa7.

📒 Files selected for processing (7)
  • .changeset/mcp-client-options.md
  • packages/ai-mcp/src/client.ts
  • packages/ai-mcp/src/types.ts
  • packages/ai-mcp/tests/client.test.ts
  • packages/ai-mcp/tests/helpers/in-memory-server.ts
  • testing/e2e/src/routes/api.mcp-test.ts
  • testing/e2e/tests/mcp.spec.ts

Comment thread packages/ai-mcp/src/client.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ai-mcp/src/pool.ts`:
- Line 155: Update the public return type of MCPClients.getServers() to include
the optional clientOptions field alongside transport and prefix, matching the
descriptor returned by the implementation and allowing TypeScript consumers to
access or forward it.

In `@packages/ai-mcp/tests/client.test.ts`:
- Line 353: Update the assertion for client.getInfo() so it verifies the
returned object shape omits clientOptions entirely, rather than merely checking
that its value is undefined. Preserve the existing getInfo() expectations while
asserting the expected object directly.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ff221a24-28d6-46a8-afc9-32a594d28beb

📥 Commits

Reviewing files that changed from the base of the PR and between 750daa7 and 81d29d1.

📒 Files selected for processing (5)
  • packages/ai-mcp/src/apps/call-handler.ts
  • packages/ai-mcp/src/apps/session-store.ts
  • packages/ai-mcp/src/client.ts
  • packages/ai-mcp/src/pool.ts
  • packages/ai-mcp/tests/client.test.ts

Comment thread packages/ai-mcp/src/pool.ts
Comment thread packages/ai-mcp/tests/client.test.ts Outdated
L4Ph added a commit to L4Ph/ai that referenced this pull request Aug 10, 2026
`MCPClients.getServers()` returns `clientOptions` since the previous commit,
but its declared return type did not, so a TypeScript consumer could not read
or forward it. `createMcpAppCallHandler` reads pools through exactly this
method — the value was there at runtime while the public type said it was not.

Also pins the omission contract in the `getInfo()` test. `toBeUndefined()`
passes whether the key is absent or present-and-undefined, and the
implementation deliberately omits it so a descriptor round-trips unchanged;
`toStrictEqual` is what tells the two apart.

Both reported by CodeRabbit on TanStack#1070.
L4Ph added 3 commits August 10, 2026 23:45
`createMCPClient` and `createMCPClientFromTransport` built their SDK `Client`
with `new Client({ name, version })` and no second argument, so nothing in
`ClientOptions` was reachable from this package.

The one that matters is `jsonSchemaValidator`. The SDK validates a tool's
`structuredContent` against its declared `outputSchema`, and its default AJV
provider compiles each schema by building JavaScript source and handing it to
`new Function`. Edge runtimes forbid that: on Cloudflare Workers, connecting to
any MCP server whose tools declare an `outputSchema` fails with `Error
compiling schema` — AJV's wrapper around `Code generation from strings
disallowed for this context`.

That fails a whole run rather than one tool call. Validators are built in
`cacheToolMetadata` during `tools/list`, so the throw lands in discovery, before
the model has seen a single tool.

The SDK already ships the fix — `CfWorkerJsonSchemaValidator`, backed by the
optional peer `@cfworker/json-schema` — and its `ClientOptions` docblock
documents installing it. It was simply not installable through this package.

```ts
import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/cfworker'

const mcp = await createMCPClient({
  transport: { type: 'http', url: 'https://mcp.example.com/mcp' },
  clientOptions: { jsonSchemaValidator: new CfWorkerJsonSchemaValidator() },
})
```

Additive and optional: omitting it keeps the SDK's defaults, which the third
unit test pins. `createMCPClients` picks it up through `MCPClientOptions` for
free.

E2E installs a validator that REFUSES everything, which is what makes the
pass-through observable — the mock server's `get_guitar_price` returns
`structuredContent` the default provider accepts, so an option dropped on the
floor would leave the run indistinguishable from the existing test.
`createMcpAppCallHandler` rebuilds a client per call from `getInfo()`, and that
descriptor carried only `transport` and `prefix`. So a client created with
`clientOptions` served widget tool calls through a REBUILT client that had none
— back on the SDK's AJV validator, which is the failure the option exists to
avoid, reintroduced for every MCP Apps call on an edge runtime.

`getInfo()` now reports the options the client was built with, the descriptor
carries them, and the handler forwards them. `MCPClients.getServers()` reports
them the same way, since the handler reads pools through it.

Optional on the return type rather than required: a hand-rolled `MCPClient`
would otherwise stop compiling, and it is omitted entirely when the client was
built without options, so a descriptor round-trips unchanged.
`MCPClients.getServers()` returns `clientOptions` since the previous commit,
but its declared return type did not, so a TypeScript consumer could not read
or forward it. `createMcpAppCallHandler` reads pools through exactly this
method — the value was there at runtime while the public type said it was not.

Also pins the omission contract in the `getInfo()` test. `toBeUndefined()`
passes whether the key is absent or present-and-undefined, and the
implementation deliberately omits it so a descriptor round-trips unchanged;
`toStrictEqual` is what tells the two apart.

Both reported by CodeRabbit on TanStack#1070.
@L4Ph
L4Ph force-pushed the feat/ai-mcp-client-options branch from fad5f41 to 5586c27 Compare August 10, 2026 14:45
@nx-cloud

nx-cloud Bot commented Aug 10, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 5586c27

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 1m 19s View ↗
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 5s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-10 14:50:17 UTC

@pkg-pr-new

pkg-pr-new Bot commented Aug 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@1070

@tanstack/ai-acp

npm i https://pkg.pr.new/@tanstack/ai-acp@1070

@tanstack/ai-angular

npm i https://pkg.pr.new/@tanstack/ai-angular@1070

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@1070

@tanstack/ai-bedrock

npm i https://pkg.pr.new/@tanstack/ai-bedrock@1070

@tanstack/ai-byteplus

npm i https://pkg.pr.new/@tanstack/ai-byteplus@1070

@tanstack/ai-claude-code

npm i https://pkg.pr.new/@tanstack/ai-claude-code@1070

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@1070

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@1070

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@1070

@tanstack/ai-codex

npm i https://pkg.pr.new/@tanstack/ai-codex@1070

@tanstack/ai-cohere

npm i https://pkg.pr.new/@tanstack/ai-cohere@1070

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@1070

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/@tanstack/ai-durable-stream@1070

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@1070

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@1070

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@1070

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@1070

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@1070

@tanstack/ai-grok-build

npm i https://pkg.pr.new/@tanstack/ai-grok-build@1070

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@1070

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@1070

@tanstack/ai-isolate-daytona

npm i https://pkg.pr.new/@tanstack/ai-isolate-daytona@1070

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@1070

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@1070

@tanstack/ai-isolate-quickjs-bun

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs-bun@1070

@tanstack/ai-mcp

npm i https://pkg.pr.new/@tanstack/ai-mcp@1070

@tanstack/ai-memory

npm i https://pkg.pr.new/@tanstack/ai-memory@1070

@tanstack/ai-mistral

npm i https://pkg.pr.new/@tanstack/ai-mistral@1070

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@1070

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@1070

@tanstack/ai-opencode

npm i https://pkg.pr.new/@tanstack/ai-opencode@1070

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@1070

@tanstack/ai-persistence

npm i https://pkg.pr.new/@tanstack/ai-persistence@1070

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@1070

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@1070

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@1070

@tanstack/ai-sandbox

npm i https://pkg.pr.new/@tanstack/ai-sandbox@1070

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-sandbox-cloudflare@1070

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/@tanstack/ai-sandbox-daytona@1070

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/@tanstack/ai-sandbox-docker@1070

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/@tanstack/ai-sandbox-local-process@1070

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/@tanstack/ai-sandbox-sprites@1070

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/@tanstack/ai-sandbox-vercel@1070

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@1070

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@1070

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@1070

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@1070

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@1070

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@1070

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@1070

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@1070

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@1070

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@1070

commit: 5586c27

@AlemTuzlak
AlemTuzlak merged commit 347a3f6 into TanStack:main Aug 10, 2026
9 checks passed
@L4Ph
L4Ph deleted the feat/ai-mcp-client-options branch August 11, 2026 02:24
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.

2 participants