feat: host-side MCP client (@tanstack/ai-mcp)#700
Merged
Conversation
…duplicate detection
…op redundant lazy cast
# Conflicts: # packages/ai/skills/ai-core/tool-calling/SKILL.md # packages/ai/src/activities/chat/index.ts # pnpm-lock.yaml # testing/e2e/package.json
…p-alive vs close)
…-mcp-prop # Conflicts: # examples/ts-react-chat/src/lib/mcp-providers.ts
tombeckenham
reviewed
Jun 5, 2026
…t in examples
Review fixes:
- docs: correct chat() signature (adapter: openaiText('gpt-5.5'), no top-level model)
- docs/skills: fix close-before-consume lifecycle samples — close MCP clients in
middleware terminal hooks (onFinish/onAbort/onError), not try/finally or
await using around a streaming return
- skills: repair truncated/unbalanced code fence corrupting the Provider Skills
section in tool-calling SKILL.md
- example: settle parallel createMCPClient calls in api.mcp-chat.ts and close the
connected sibling on partial failure (no leaked stdio subprocess)
- example: probe capabilities in api.mcp-status.ts instead of catch-all-ing
resources()/prompts()
- ai-mcp: name the tool in MCP isError throws; short-circuit already-aborted
signals; pool.tools() now attributes the failing server by config key
- ai(chat): correct MCPConnectionPolicy JSDoc ('close' = when the run ends;
'keep-alive' = never closed by chat())
- types: collapse redundant AutomaticDescriptor; reword DescribedTool/codegen
claims to name-only typing (args stay untyped on discovery; Mode 2 types args)
New coverage (ai-mcp 30 → 46 tests):
- isError path, abort→callTool forwarding, already-aborted short-circuit,
structuredContent preference, mcpContentToTanstack branches
- replace tautological duplicate-name test with one exercising the real guard
- connect-failure wrapping, double-close idempotency, bound-defs prefix,
pool tools() failure attribution, stdio smoke test, authProvider forwarding
- types.test-d.ts pins the descriptor name-literal guarantee
Docs additions:
- Authentication section (headers + OAuth authProvider + finishAuth caveat) in
mcp.md and the ai-mcp skill
- all MCP route examples converted from Next.js App Router to TanStack Start
(createFileRoute + server handlers); no Next.js references remain in PR files
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 tasks
…licit binding Newer MCP servers (e.g. @modelcontextprotocol/server-everything's simulate-research-query) mark tools with `execution.taskSupport: 'required'` (experimental MCP tasks). Plain `callTool` — what @tanstack/ai-mcp uses — is rejected by the server with -32600, so offering these tools to the model guarantees a failed tool call. - tools(): auto-discovery now filters task-required tools via the new `requiresTaskExecution()` guard — the model is never offered a tool that cannot succeed - tools([defs]): explicitly binding a task-required tool throws the new `MCPTaskRequiredToolError` (exported) with guidance pointing at the SDK's tasks API - tests: in-memory server helper registering a real task-required tool; discovery-exclusion + binding-throws coverage (48 tests) - e2e: in-process MCP server registers a task-required tool; spec asserts it never reaches the discovered tool list - docs: Mode 1 exclusion callout, Mode 2 error mention, Error Reference row in mcp.md; matching error-list updates in the ai-mcp skill Actual task-based execution support (client.experimental.tasks.callToolStream) is tracked in #704. Also includes import-order/lint cleanup in examples/ts-react-chat api.mcp-manual.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- rename mcp-chat.md → mcp-managed.md ("Managed MCP with chat()") and
mcp-with-chat.md → mcp-manual.md ("Manual MCP: typed tools, resources &
prompts") — the old slugs differed only by a preposition while the real
distinction is managed vs manual lifecycle; new names match the vocabulary
already used by the e2e/example routes (api.mcp-managed-test, api.mcp-manual)
- update all ids, cross-links, link texts, and docs/config.json nav labels
- Quick Start (mcp.md) now leads with the managed mcp option (zero lifecycle
code) instead of the manual middleware-close pattern; Lifecycle section
opens with the managed escape hatch before the manual rules
- mcp-managed.md: keep one full route example (plus keep-warm, where module
vs handler placement is the point); convert the other five examples to
focused fragments showing just the client setup + chat() call; drop the
repeated 8-line body-validation boilerplate (449 → 306 lines)
- reorder sidebar/frontmatter to adoption order:
mcp → mcp-managed → mcp-manual → mcp-codegen
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tombeckenham
reviewed
Jun 5, 2026
|
|
||
| /** Load mcp.config.ts (via jiti) or mcp.config.json from cwd. */ | ||
| export async function loadConfig(cwd: string): Promise<MCPCodegenConfig> { | ||
| const { existsSync } = await import('node:fs') |
Contributor
There was a problem hiding this comment.
I had to look into why you had these dynamic imports. I get it. It's the one package for the bin and the lib. If you'd imported at module level it would have added node and other dependencies into the library. It looks weird, but I'm happy with it. It's clever to be able to include the cli in the same package.
…ples - emit.ts: guarantee valid/unique generated identifiers, escape string literals via JSON.stringify when emitting TypeScript - introspect.ts: move connect() inside try/finally, guard close() from masking the original error, drain cursor-paginated list endpoints - prompts.ts: never produce undefined content (JSON.stringify(x ?? null)) - manager.ts: await onDiscoveryError so async handlers fail fast - chat: combine caller + middleware abort signals so ctx.abort() reaches running tools via ctx.abortSignal (regression test added) - mcp routes (e2e + example): close MCP client on non-stream error paths, bridge request aborts during setup - mcp-demo/threads: pass remark-gfm via remarkPlugins (not rehypePlugins) - tests: fix import/order lint errors, assert pool cleanup on failure, cover hostile-name escaping and interface collisions in emit Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tombeckenham
approved these changes
Jun 5, 2026
Contributor
tombeckenham
left a comment
There was a problem hiding this comment.
This is great work. Really. I added a few small changes and cleaned up the docs. Ship it!
Merged
This was referenced Jun 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
@tanstack/ai-mcp— a host-side Model Context Protocol client — so a server-sidechat()/ agent loop can discover and run tools (and read resources/prompts) from external MCP servers, across any adapter, with optional generated end-to-end types.Built on the official
@modelcontextprotocol/sdk. The runtime stays edge-deployable (Streamable HTTP isnode:-free); the Node-only stdio transport is isolated behind a@tanstack/ai-mcp/stdiosubpath, and the codegen CLI's heavy deps are bundled into the bin only (never the library).How it works
An MCP client turns a server into ready-to-spread
ServerTool[]; you spread them intochat({ tools }). TanStack AI never knows MCP is involved.What's included
createMCPClient— connect to one server. Transports: Streamable HTTP (edge-safe), SSE, and stdio (via@tanstack/ai-mcp/stdio), plus a user-supplied-transport escape hatch.createMCPClients— multi-server pool: parallel connect, auto-prefixed tool names (collision-free by default), close-all, typed per-server access.client.tools()— auto-discovery (argsunknown).client.tools([toolDefinition(...)])— bind TanStacktoolDefinition()s → typed + runtime-validated, allowlisted (reuses the existing tool primitive; no parallel schema system).createMCPClient<GeneratedServer>(...)— generated end-to-end types vianpx @tanstack/ai-mcp generate+mcp.config.ts(emits per-server descriptors + a combinedMCPServersmap; pure types, zero runtime cost).resources()/readResource()/resourceTemplates(),prompts()/getPrompt(), plusmcpResourceToContentPart/mcpPromptToMessagesconverters for seedingchat().close()+[Symbol.asyncDispose](await using);chat()never closes the client (warm reuse supported).@tanstack/ai) — adds an optionalabortSignaltoToolExecutionContextand threads the chat run's signal through tool execution, so long-running tools (e.g. MCPcallTool) cancel with the run. Additive/backward-compatible.Testing & docs
testing/e2e): a real in-process MCP server + chat route + Playwright spec proving an MCP tool executes insidechat()and its result reaches the streamed transcript.docs/tools/mcp.md(+ nav/cross-links), updatedtool-callingskill + a newai-mcpskill, and a changeset (minor:@tanstack/ai-mcp,@tanstack/ai).Notes for reviewers
examples/typecheck failures (ts-solid-chat/ts-react-chat/ts-code-mode-web, missingfetcherfrom docs(chat): document thefetcherchat transport for server functions #681) are unrelated to this PR and excluded from thetest:prgate.🤖 Generated with Claude Code
Summary by CodeRabbit