feat: host-side MCP client (@tanstack/ai-mcp)#700
Conversation
…duplicate detection
…op redundant lazy cast
Update: also adds
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/ai/src/activities/chat/index.ts (1)
2606-2632:⚠️ Potential issue | 🟠 Major | ⚡ Quick winExpand the
try/finallyscope so MCP clients are disposed if engine setup throws.
mcpManager.dispose()currently runs only afterengine.run()starts. Ifnew TextEngine(...)throws after successful discovery, clients opened underconnection: 'close'are never disposed.💡 Proposed fix pattern (apply to all three run helpers)
const mcpManager = MCPManager.from(mcp) -const mcpTools = await mcpManager.discover() -if (mcpTools.length > 0) { - textOptions.tools = [...(textOptions.tools ?? []), ...mcpTools] -} - -const engine = new TextEngine( - { ... }, - logger, -) - try { + const mcpTools = await mcpManager.discover() + if (mcpTools.length > 0) { + textOptions.tools = [...(textOptions.tools ?? []), ...mcpTools] + } + + const engine = new TextEngine( + { ... }, + logger, + ) + for await (const chunk of engine.run()) { yield chunk } } finally { await mcpManager.dispose() }Also applies to: 2710-2743, 2995-3028
🤖 Prompt for 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. In `@packages/ai/src/activities/chat/index.ts` around lines 2606 - 2632, The MCP clients created by MCPManager.from/discover may leak if the TextEngine constructor throws because mcpManager.dispose() is only in the finally after engine.run(); expand the try/finally so the try begins before creating the TextEngine (i.e., wrap MCPManager.from, mcpManager.discover and the new TextEngine(...) in the try) and keep mcpManager.dispose() in the finally to guarantee disposal on any error; apply the same pattern to the other run helpers that construct TextEngine and call engine.run (refer to MCPManager.from, mcpManager.discover, new TextEngine(...), engine.run, and mcpManager.dispose to locate the spots).
🤖 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/src/activities/chat/mcp/manager.ts`:
- Around line 29-30: The call to the optional private handler
this.#onDiscoveryError is invoked but not awaited, which allows async handlers
to reject without stopping discovery; update the invocation site(s) where
this.#onDiscoveryError(...) is called (search for the call inside the discovery
flow) to await the result (e.g. await this.#onDiscoveryError?.(error, source))
and ensure the surrounding function is async (propagate the promise by marking
the containing method as async and awaiting the call) so async rejections are
observed and discovery can fail-fast.
In `@packages/ai/tests/chat-mcp.test.ts`:
- Around line 1-7: Reorder and alphabetize the imports in the test file so
ESLint import/order and sort-imports are satisfied: move value imports (e.g.,
chat, MCPDuplicateToolNameError, ev, createMockAdapter, collectChunks) above
type-only imports (StreamChunk, MCPToolSource, ServerTool) and ensure the named
imports from './test-utils' (ev, collectChunks, createMockAdapter) are
alphabetized; update the import statement ordering and spacing accordingly in
packages/ai/tests/chat-mcp.test.ts, keeping existing symbols and no behavioral
changes.
---
Outside diff comments:
In `@packages/ai/src/activities/chat/index.ts`:
- Around line 2606-2632: The MCP clients created by MCPManager.from/discover may
leak if the TextEngine constructor throws because mcpManager.dispose() is only
in the finally after engine.run(); expand the try/finally so the try begins
before creating the TextEngine (i.e., wrap MCPManager.from, mcpManager.discover
and the new TextEngine(...) in the try) and keep mcpManager.dispose() in the
finally to guarantee disposal on any error; apply the same pattern to the other
run helpers that construct TextEngine and call engine.run (refer to
MCPManager.from, mcpManager.discover, new TextEngine(...), engine.run, and
mcpManager.dispose to locate the spots).
🪄 Autofix (Beta)
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
Run ID: 3a8aba1d-f5d7-41a0-9ba6-6335fb14e22a
📒 Files selected for processing (17)
.changeset/chat-mcp-option.mddocs/tools/mcp.mdknip.jsonpackages/ai-mcp/skills/ai-mcp/SKILL.mdpackages/ai-mcp/tests/mcp-tool-source.test-d.tspackages/ai/skills/ai-core/chat-experience/SKILL.mdpackages/ai/skills/ai-core/tool-calling/SKILL.mdpackages/ai/src/activities/chat/index.tspackages/ai/src/activities/chat/mcp/manager.tspackages/ai/src/activities/chat/mcp/types.tspackages/ai/src/index.tspackages/ai/tests/chat-mcp-manager.test.tspackages/ai/tests/chat-mcp-types.test-d.tspackages/ai/tests/chat-mcp.test.tstesting/e2e/src/routeTree.gen.tstesting/e2e/src/routes/api.mcp-managed-test.tstesting/e2e/tests/mcp-managed.spec.ts
✅ Files skipped from review due to trivial changes (9)
- packages/ai/tests/chat-mcp-types.test-d.ts
- packages/ai-mcp/tests/mcp-tool-source.test-d.ts
- knip.json
- .changeset/chat-mcp-option.md
- packages/ai/skills/ai-core/chat-experience/SKILL.md
- packages/ai/skills/ai-core/tool-calling/SKILL.md
- packages/ai-mcp/skills/ai-mcp/SKILL.md
- docs/tools/mcp.md
- testing/e2e/src/routeTree.gen.ts
| readonly #onDiscoveryError?: (error: unknown, source: MCPToolSource) => void | ||
|
|
There was a problem hiding this comment.
Await onDiscoveryError so async handlers can truly fail-fast.
At Line 60, onDiscoveryError is called but not awaited. If a consumer provides an async handler that rejects, discovery may continue and the rejection can surface as unhandled.
💡 Proposed fix
--- a/packages/ai/src/activities/chat/mcp/manager.ts
+++ b/packages/ai/src/activities/chat/mcp/manager.ts
@@
- readonly `#onDiscoveryError`?: (error: unknown, source: MCPToolSource) => void
+ readonly `#onDiscoveryError`?: (
+ error: unknown,
+ source: MCPToolSource,
+ ) => void | Promise<void>
@@
- } else if (this.#onDiscoveryError) {
+ } else if (this.#onDiscoveryError) {
// throw inside handler ⇒ propagate (fail-fast); return ⇒ skip
- this.#onDiscoveryError(result.reason, source)
+ await this.#onDiscoveryError(result.reason, source)
} else {
throw result.reason
}--- a/packages/ai/src/activities/chat/mcp/types.ts
+++ b/packages/ai/src/activities/chat/mcp/types.ts
@@
- onDiscoveryError?: (error: unknown, source: MCPToolSource) => void
+ onDiscoveryError?: (
+ error: unknown,
+ source: MCPToolSource,
+ ) => void | Promise<void>Also applies to: 58-60
🤖 Prompt for 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.
In `@packages/ai/src/activities/chat/mcp/manager.ts` around lines 29 - 30, The
call to the optional private handler this.#onDiscoveryError is invoked but not
awaited, which allows async handlers to reject without stopping discovery;
update the invocation site(s) where this.#onDiscoveryError(...) is called
(search for the call inside the discovery flow) to await the result (e.g. await
this.#onDiscoveryError?.(error, source)) and ensure the surrounding function is
async (propagate the promise by marking the containing method as async and
awaiting the call) so async rejections are observed and discovery can fail-fast.
| import { describe, expect, it, vi } from 'vitest' | ||
| import { chat } from '../src/activities/chat/index' | ||
| import { MCPDuplicateToolNameError } from '../src/activities/chat/mcp/manager' | ||
| import type { StreamChunk } from '../src/types' | ||
| import type { MCPToolSource } from '../src/activities/chat/mcp/types' | ||
| import type { ServerTool } from '../src/activities/chat/tools/tool-definition' | ||
| import { ev, createMockAdapter, collectChunks } from './test-utils' |
There was a problem hiding this comment.
Fix header imports to satisfy ESLint import/order and sort-imports.
Line 7 currently places a value import after type imports, and the named imports from ./test-utils are not alphabetically sorted.
💡 Proposed fix
import { describe, expect, it, vi } from 'vitest'
import { chat } from '../src/activities/chat/index'
import { MCPDuplicateToolNameError } from '../src/activities/chat/mcp/manager'
+import { collectChunks, createMockAdapter, ev } from './test-utils'
import type { StreamChunk } from '../src/types'
import type { MCPToolSource } from '../src/activities/chat/mcp/types'
import type { ServerTool } from '../src/activities/chat/tools/tool-definition'
-import { ev, createMockAdapter, collectChunks } from './test-utils'As per coding guidelines: **/*.{ts,tsx,js,jsx}: Use ESLint with custom TanStack config for linting.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import { describe, expect, it, vi } from 'vitest' | |
| import { chat } from '../src/activities/chat/index' | |
| import { MCPDuplicateToolNameError } from '../src/activities/chat/mcp/manager' | |
| import type { StreamChunk } from '../src/types' | |
| import type { MCPToolSource } from '../src/activities/chat/mcp/types' | |
| import type { ServerTool } from '../src/activities/chat/tools/tool-definition' | |
| import { ev, createMockAdapter, collectChunks } from './test-utils' | |
| import { describe, expect, it, vi } from 'vitest' | |
| import { chat } from '../src/activities/chat/index' | |
| import { MCPDuplicateToolNameError } from '../src/activities/chat/mcp/manager' | |
| import { collectChunks, createMockAdapter, ev } from './test-utils' | |
| import type { StreamChunk } from '../src/types' | |
| import type { MCPToolSource } from '../src/activities/chat/mcp/types' | |
| import type { ServerTool } from '../src/activities/chat/tools/tool-definition' |
🧰 Tools
🪛 ESLint
[error] 7-7: ./test-utils import should occur before type import of ../src/types
(import/order)
[error] 7-7: Member 'createMockAdapter' of the import declaration should be sorted alphabetically.
(sort-imports)
🤖 Prompt for 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.
In `@packages/ai/tests/chat-mcp.test.ts` around lines 1 - 7, Reorder and
alphabetize the imports in the test file so ESLint import/order and sort-imports
are satisfied: move value imports (e.g., chat, MCPDuplicateToolNameError, ev,
createMockAdapter, collectChunks) above type-only imports (StreamChunk,
MCPToolSource, ServerTool) and ensure the named imports from './test-utils' (ev,
collectChunks, createMockAdapter) are alphabetized; update the import statement
ordering and spacing accordingly in packages/ai/tests/chat-mcp.test.ts, keeping
existing symbols and no behavioral changes.
…custom-transport escape hatch
…cp, manual chat integration)
There was a problem hiding this comment.
🧹 Nitpick comments (1)
docs/tools/mcp.md (1)
310-312: 💤 Low valueOptional: Fix blank line between blockquotes.
The markdownlint tool flagged a blank line between two consecutive blockquote lines (MD028). While this doesn't affect rendering, removing the blank line would align with markdown best practices.
The two blockquotes should be consecutive without a blank line between them, or they should be separated by a non-blockquote element if they represent distinct sections.
🤖 Prompt for 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. In `@docs/tools/mcp.md` around lines 310 - 312, Remove the blank line between the two consecutive blockquote lines so they are consecutive blockquotes; specifically, join the lines starting "**Let `chat()` own discovery and lifecycle.** Pass live clients and pools to `chat()` via the `mcp` option..." and "**Resources, prompts, and fully-typed manual tools.** Inject MCP resources and prompts into a `chat()` run..." by deleting the empty line between them (or insert a non-blockquote separator if you intend them as distinct sections).
🤖 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.
Nitpick comments:
In `@docs/tools/mcp.md`:
- Around line 310-312: Remove the blank line between the two consecutive
blockquote lines so they are consecutive blockquotes; specifically, join the
lines starting "**Let `chat()` own discovery and lifecycle.** Pass live clients
and pools to `chat()` via the `mcp` option..." and "**Resources, prompts, and
fully-typed manual tools.** Inject MCP resources and prompts into a `chat()`
run..." by deleting the empty line between them (or insert a non-blockquote
separator if you intend them as distinct sections).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f06b813b-12c3-4154-a366-f77140162fc4
📒 Files selected for processing (6)
docs/config.jsondocs/tools/mcp-chat.mddocs/tools/mcp-codegen.mddocs/tools/mcp-with-chat.mddocs/tools/mcp.mdpackages/ai-mcp/src/index.ts
✅ Files skipped from review due to trivial changes (4)
- docs/tools/mcp-codegen.md
- docs/tools/mcp-chat.md
- docs/tools/mcp-with-chat.md
- packages/ai-mcp/src/index.ts
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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 `@examples/ts-react-chat/src/lib/mcp-servers.ts`:
- Around line 11-29: The three transport factories (everythingTransport,
memoryTransport, sequentialThinkingTransport) call npx with unpinned package
names; change each args entry from '`@modelcontextprotocol/server-`...' to an
exact version string like '`@modelcontextprotocol/server-memory`@<exact-version>'
(and similarly for server-everything and server-sequential-thinking) so npx
installs a deterministic release. Locate the args arrays in everythingTransport,
memoryTransport, and sequentialThinkingTransport and replace the package tokens
with the exact versions used by this project (pull versions from package.json or
the lockfile) before committing.
In `@examples/ts-react-chat/src/routes/api.mcp-chat.ts`:
- Around line 51-95: The route can leak an MCP client if Promise.all rejects
partway; change the client creation so you track and clean up any
successfully-created clients on failure (e.g. use Promise.allSettled on
createMCPClient(...) for the two clients or create them sequentially), inspect
results, and if any creation failed call dispose() (or the client's teardown
method) on each fulfilled client before throwing/returning; ensure the
successful clients are only passed into chat({ mcp: { clients: [...] } }) after
you've validated both were created so MCPManager.discover()/dispose() inside
chat runs correctly.
In `@examples/ts-react-chat/src/routes/api.mcp-manual.ts`:
- Around line 23-24: Reorder the imported symbols so named exports are
alphabetized per the project's sort-imports rule: in the import from
'`@tanstack/ai-mcp`' ensure the members are sorted (createMCPClient,
mcpPromptToMessages, mcpResourceToContentPart) and in the import from
'`@tanstack/ai`' ensure (ModelMessage, StreamChunk) are sorted; update the import
lines that declare createMCPClient, mcpResourceToContentPart,
mcpPromptToMessages and the import that declares StreamChunk, ModelMessage so
their member lists are alphabetically ordered to satisfy ESLint.
- Around line 69-158: The MCP client created by createMCPClient(...) can leak if
an error happens after creation (e.g., during chat(...) or
toServerSentEventsResponse(...)); ensure client.close() is always called on
error paths by moving client declaration into the outer scope and adding a
finally or ensuring the outer catch closes the client if it exists and is not
already closed (reference createMCPClient, client.tools, client.close,
closeMcpOnDrain, chat, toServerSentEventsResponse, abortController); keep
existing early-close where client.tools() fails and still close in the outer
catch/finally so the stdio MCP process is not leaked.
In `@examples/ts-react-chat/src/routes/mcp-demo.tsx`:
- Around line 115-120: The code passes remarkGfm into the rehypePlugins array,
so GFM parsing won't run; update the component props to remove remarkGfm from
rehypePlugins and instead add it to remarkPlugins (i.e., keep rehypePlugins as
[rehypeRaw, rehypeSanitize, rehypeHighlight] and add
remarkPlugins={[remarkGfm]}), ensuring the component uses the remarkPlugins prop
with remarkGfm.
🪄 Autofix (Beta)
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
Run ID: c11c09ae-c0ba-444e-bb41-dcde07e19dca
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (7)
examples/ts-react-chat/package.jsonexamples/ts-react-chat/src/lib/mcp-servers.tsexamples/ts-react-chat/src/routeTree.gen.tsexamples/ts-react-chat/src/routes/api.mcp-chat.tsexamples/ts-react-chat/src/routes/api.mcp-manual.tsexamples/ts-react-chat/src/routes/api.mcp-pool.tsexamples/ts-react-chat/src/routes/mcp-demo.tsx
| export const everythingTransport = (): Transport => | ||
| stdioTransport({ | ||
| command: 'npx', | ||
| args: ['-y', '@modelcontextprotocol/server-everything'], | ||
| }) | ||
|
|
||
| /** @modelcontextprotocol/server-memory — persistent knowledge-graph memory tool. */ | ||
| export const memoryTransport = (): Transport => | ||
| stdioTransport({ | ||
| command: 'npx', | ||
| args: ['-y', '@modelcontextprotocol/server-memory'], | ||
| }) | ||
|
|
||
| /** @modelcontextprotocol/server-sequential-thinking — step-by-step reasoning tool. */ | ||
| export const sequentialThinkingTransport = (): Transport => | ||
| stdioTransport({ | ||
| command: 'npx', | ||
| args: ['-y', '@modelcontextprotocol/server-sequential-thinking'], | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="examples/ts-react-chat/src/lib/mcp-servers.ts"
echo "== File exists? =="
ls -l "$FILE" || exit 1
echo "== Lines 1-120 =="
nl -ba "$FILE" | sed -n '1,120p'
echo "== Search for npx server packages in repo =="
rg -n "npx|`@modelcontextprotocol/server-`" "$FILE"Repository: TanStack/ai
Length of output: 249
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="examples/ts-react-chat/src/lib/mcp-servers.ts"
echo "== Lines 1-140 (with line numbers) =="
cat -n "$FILE" | sed -n '1,140p'
echo "== npx / server packages in file =="
rg -n "`@modelcontextprotocol/server-`|command: 'npx'|args: \\[" "$FILE"Repository: TanStack/ai
Length of output: 1945
Pin exact MCP server versions in npx args.
examples/ts-react-chat/src/lib/mcp-servers.ts launches unpinned MCP server packages via npx at lines 14, 21, and 28 (e.g., @modelcontextprotocol/server-memory), making runtime behavior non-deterministic and increasing supply-chain risk. Pin each to an exact version in the args.
🤖 Prompt for 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.
In `@examples/ts-react-chat/src/lib/mcp-servers.ts` around lines 11 - 29, The
three transport factories (everythingTransport, memoryTransport,
sequentialThinkingTransport) call npx with unpinned package names; change each
args entry from '`@modelcontextprotocol/server-`...' to an exact version string
like '`@modelcontextprotocol/server-memory`@<exact-version>' (and similarly for
server-everything and server-sequential-thinking) so npx installs a
deterministic release. Locate the args arrays in everythingTransport,
memoryTransport, and sequentialThinkingTransport and replace the package tokens
with the exact versions used by this project (pull versions from package.json or
the lockfile) before committing.
| const [everything, memory] = await Promise.all([ | ||
| createMCPClient({ | ||
| transport: everythingTransport(), | ||
| prefix: 'everything', | ||
| }), | ||
| createMCPClient({ | ||
| transport: memoryTransport(), | ||
| prefix: 'memory', | ||
| }), | ||
| ]) | ||
|
|
||
| // chat() discovers tools from both clients and closes them when the | ||
| // stream drains — connection: 'close' (the default; shown explicitly). | ||
| // The model is encoded in the adapter; do not pass it separately. | ||
| const stream = chat({ | ||
| adapter: openaiText('gpt-4o'), | ||
| messages: params.messages, | ||
| mcp: { | ||
| clients: [everything, memory], | ||
| connection: 'close', | ||
| }, | ||
| agentLoopStrategy: maxIterations(20), | ||
| threadId: params.threadId, | ||
| runId: params.runId, | ||
| abortController, | ||
| }) | ||
|
|
||
| return toServerSentEventsResponse(stream, { abortController }) | ||
| } catch (error: any) { | ||
| console.error('[api.mcp-chat] Error:', { | ||
| message: error?.message, | ||
| name: error?.name, | ||
| stack: error?.stack, | ||
| }) | ||
| if (error.name === 'AbortError' || abortController.signal.aborted) { | ||
| return new Response(null, { status: 499 }) | ||
| } | ||
| return new Response( | ||
| JSON.stringify({ error: error.message || 'An error occurred' }), | ||
| { | ||
| status: 500, | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }, | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="examples/ts-react-chat/src/routes/api.mcp-chat.ts"
echo "== File: $FILE =="
wc -l "$FILE"
echo "== Extract lines 1-140 =="
nl -ba "$FILE" | sed -n '1,140p'
echo "== Search for createMCPClient usage in repo =="
rg -n "createMCPClient\(" -S .
echo "== Search for close() usage on MCP clients in repo =="
rg -n "\.close\(" -S examples/ts-react-chat/src . | head -n 200
echo "== Search for toServerSentEventsResponse implementation =="
rg -n "toServerSentEventsResponse" -S .
echo "== Search for chat(...) mcp cleanup/close behavior references =="
rg -n "agentLoopStrategy|maxIterations|connection: 'close'|mcp:\s*\{|clients:\s*\[" -S examples/ts-react-chat/src .Repository: TanStack/ai
Length of output: 257
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="examples/ts-react-chat/src/routes/api.mcp-chat.ts"
echo "== File: $FILE =="
wc -l "$FILE"
echo "== Extract lines 1-140 =="
awk '{printf "%6d %s\n", NR, $0}' "$FILE" | sed -n '1,140p'
echo "== Search for createMCPClient usage in repo =="
rg -n "createMCPClient\(" -S .
echo "== Search for close() usage on MCP clients in repo =="
rg -n "\.close\(" -S examples/ts-react-chat/src | head -n 200
echo "== Search for toServerSentEventsResponse implementation =="
rg -n "toServerSentEventsResponse" -S .
echo "== Inspect how chat(...) is imported/used in this file =="
rg -n "import .*chat|from .*chat|chat\(" -S "$FILE"
echo "== Inspect createMCPClient symbol definition (if in repo) =="
rg -n "function createMCPClient|const createMCPClient|createMCPClient\s*=" -S . || trueRepository: TanStack/ai
Length of output: 50367
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Inspect `@tanstack/ai-mcp` createMCPClient implementation =="
rg -n "export (async )?function createMCPClient|createMCPClient\s*=\s*async|function createMCPClient" packages/ai-mcp/src -S
rg -n "connection:\s*'close'|connection\W*===\W*'close'|connection\W*:\W*'close'" packages -S || true
rg -n "mcp:\s*\{|clients:\s*\[|agentLoopStrategy|maxIterations|close\(\)\s*when|drain" packages/ai packages/ai-mcp -S || true
echo "== Find where MCP clients are closed inside chat() =="
rg -n "mcp.*close|close.*mcp|clients.*close|for .*client.*close|await .*close\(" packages/ai packages/ai-mcp -S || true
echo "== Inspect docs claiming chat closes clients when stream drains =="
rg -n "closes.*clients|stream drains|connection: 'close'|mcp.*connection" docs/tools/mcp-chat.md docs/tools/mcp-with-chat.md docs/tools/mcp.md -S || true
echo "== Print relevant source files around hits (limit output) =="
# Show top matches with line numbers; then we’ll narrow further if needed
rg -n "mcp.*connection|connection:\s*'close'" packages/ai -S --no-heading --context 2 | head -n 50 || true
rg -n "mcp.*close|clients.*close" packages/ai-mcp -S --no-heading --context 2 | head -n 50 || trueRepository: TanStack/ai
Length of output: 20963
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show createMCPClient implementation + close method
FILE="packages/ai-mcp/src/client.ts"
echo "== $FILE =="
wc -l "$FILE"
rg -n "export async function createMCPClient|createMCPClientFromTransport|class|close\(" "$FILE" -n
echo "== Relevant createMCPClient section (lines ~140-240) =="
sed -n '130,230p' "$FILE"
echo "== Relevant client close implementation (search for 'close()' around definitions) =="
rg -n "async close\(|close\(\)\s*\{|close:" "$FILE"
# Print around the first close hit
LINE=$(rg -n "async close\(" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${LINE:-}" ]; then
START=$((LINE-20))
END=$((LINE+40))
sed -n "${START},${END}p" "$FILE"
fi
# Inspect createMCPClients (plural) which claims no leaks on parallel failure
FILE2="packages/ai-mcp/src/pool.ts"
echo "== $FILE2 =="
wc -l "$FILE2"
rg -n "createMCPClients|Promise\.allSettled|close\(\)" "$FILE2"
sed -n '1,160p' "$FILE2"
sed -n '160,260p' "$FILE2"Repository: TanStack/ai
Length of output: 8103
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/ai-mcp/src/client.ts"
echo "== Show MCPClientImpl.connect implementation =="
# Print early portion of file to include class and connect()
sed -n '1,120p' "$FILE"
sed -n '120,190p' "$FILE"
echo "== Search for '.connect(' and error handling in client.ts =="
rg -n "connect\(" "$FILE"
rg -n "try\s*\{|catch\s*\(" "$FILE"
# If connect is too early or scattered, locate 'connect' method definition more precisely
# (rg 'async connect' pattern)
rg -n "async connect" "$FILE" || trueRepository: TanStack/ai
Length of output: 6486
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect MCP manager
FILE="packages/ai/src/activities/chat/mcp/manager.ts"
echo "== $FILE =="
wc -l "$FILE"
sed -n '1,220p' "$FILE"
sed -n '220,420p' "$FILE"
# Inspect where MCPManager is constructed/used in chat()
CHAT_FILE="packages/ai/src/activities/chat/index.ts"
echo "== $CHAT_FILE (search for MCPManager usage) =="
rg -n "MCPManager|mcpManager|mcp:\s*\\{|dispose\\(|discover\\(" "$CHAT_FILE" | head -n 200
# Print regions around those hits (limit output)
rg -n "MCPManager|mcpManager" "$CHAT_FILE" --context 3 | head -n 80
rg -n "dispose\\(" "$CHAT_FILE" --context 3 | head -n 80
rg -n "discover\\(" "$CHAT_FILE" --context 3 | head -n 80Repository: TanStack/ai
Length of output: 6889
Handle partial-connect failures to avoid leaking MCP clients.
If either createMCPClient(...) rejects inside the route’s Promise.all, the other successfully-created client never reaches chat({ mcp: { ... } }), so chat()’s MCP cleanup (MCPManager.discover()/dispose()) won’t run. The route catch block only returns a Response, leaving the connected client unclosed.
Suggested fix pattern
+ let everything: Awaited<ReturnType<typeof createMCPClient>> | undefined
+ let memory: Awaited<ReturnType<typeof createMCPClient>> | undefined
try {
- const [everything, memory] = await Promise.all([
- createMCPClient({
- transport: everythingTransport(),
- prefix: 'everything',
- }),
- createMCPClient({
- transport: memoryTransport(),
- prefix: 'memory',
- }),
- ])
+ everything = await createMCPClient({
+ transport: everythingTransport(),
+ prefix: 'everything',
+ })
+ memory = await createMCPClient({
+ transport: memoryTransport(),
+ prefix: 'memory',
+ })
@@
} catch (error: any) {
+ await Promise.allSettled(
+ [everything, memory].filter(Boolean).map((c) => c!.close()),
+ )
console.error('[api.mcp-chat] Error:', {
message: error?.message,
name: error?.name,
stack: error?.stack,
})🤖 Prompt for 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.
In `@examples/ts-react-chat/src/routes/api.mcp-chat.ts` around lines 51 - 95, The
route can leak an MCP client if Promise.all rejects partway; change the client
creation so you track and clean up any successfully-created clients on failure
(e.g. use Promise.allSettled on createMCPClient(...) for the two clients or
create them sequentially), inspect results, and if any creation failed call
dispose() (or the client's teardown method) on each fulfilled client before
throwing/returning; ensure the successful clients are only passed into chat({
mcp: { clients: [...] } }) after you've validated both were created so
MCPManager.discover()/dispose() inside chat runs correctly.
| import { createMCPClient, mcpResourceToContentPart, mcpPromptToMessages } from '@tanstack/ai-mcp' | ||
| import type { StreamChunk, ModelMessage } from '@tanstack/ai' |
There was a problem hiding this comment.
Fix import member ordering to satisfy lint.
These imports currently violate sort-imports (Line 23 and Line 24), which will fail ESLint for this TS file.
As per coding guidelines, **/*.{ts,tsx,js,jsx}: Use ESLint with custom TanStack config for linting.
🧰 Tools
🪛 ESLint
[error] 23-23: Member 'mcpPromptToMessages' of the import declaration should be sorted alphabetically.
(sort-imports)
[error] 24-24: Member 'ModelMessage' of the import declaration should be sorted alphabetically.
(sort-imports)
🤖 Prompt for 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.
In `@examples/ts-react-chat/src/routes/api.mcp-manual.ts` around lines 23 - 24,
Reorder the imported symbols so named exports are alphabetized per the project's
sort-imports rule: in the import from '`@tanstack/ai-mcp`' ensure the members are
sorted (createMCPClient, mcpPromptToMessages, mcpResourceToContentPart) and in
the import from '`@tanstack/ai`' ensure (ModelMessage, StreamChunk) are sorted;
update the import lines that declare createMCPClient, mcpResourceToContentPart,
mcpPromptToMessages and the import that declares StreamChunk, ModelMessage so
their member lists are alphabetically ordered to satisfy ESLint.
| try { | ||
| // --- MCP: create and connect to the everything server (keyless, stdio) --- | ||
| const client = await createMCPClient({ | ||
| transport: everythingTransport(), | ||
| }) | ||
|
|
||
| let tools | ||
| try { | ||
| // Auto-discover all tools from the MCP server. | ||
| tools = await client.tools() | ||
| } catch (error) { | ||
| await client.close() | ||
| throw error | ||
| } | ||
|
|
||
| // --- MCP: resources — inject the first resource as context (if any) --- | ||
| const contextMessages: Array<ModelMessage> = [] | ||
|
|
||
| try { | ||
| const resources = await client.resources() | ||
| if (resources.length > 0) { | ||
| // Read the first resource and convert each content block to a ContentPart. | ||
| const readResult = await client.readResource(resources[0]!.uri) | ||
| const parts = readResult.contents.map(mcpResourceToContentPart) | ||
| if (parts.length > 0) { | ||
| contextMessages.push({ | ||
| role: 'user', | ||
| content: [ | ||
| ...parts, | ||
| { | ||
| type: 'text', | ||
| content: | ||
| '[MCP resource context injected from server-everything — use this as background information if relevant]', | ||
| }, | ||
| ], | ||
| }) | ||
| } | ||
| } | ||
| } catch { | ||
| // Resources are optional — proceed without them if unavailable. | ||
| } | ||
|
|
||
| // --- MCP: prompts — prepend the first available prompt (if any) --- | ||
| try { | ||
| const availablePrompts = await client.prompts() | ||
| if (availablePrompts.length > 0) { | ||
| const firstPrompt = availablePrompts[0]! | ||
| const promptResult = await client.getPrompt(firstPrompt.name) | ||
| const promptMessages = mcpPromptToMessages(promptResult) | ||
| // Prepend prompt messages before resource context and user messages. | ||
| contextMessages.unshift(...promptMessages) | ||
| } | ||
| } catch { | ||
| // Prompts are optional — proceed without them if unavailable. | ||
| } | ||
|
|
||
| // OPENAI_API_KEY is used by the LLM adapter (separate from the | ||
| // keyless MCP server transport which needs no credentials). | ||
| // The model is encoded in the adapter; do not pass it separately. | ||
| const stream = chat({ | ||
| adapter: openaiText('gpt-4o'), | ||
| messages: [...contextMessages, ...params.messages], | ||
| tools, | ||
| agentLoopStrategy: maxIterations(20), | ||
| threadId: params.threadId, | ||
| runId: params.runId, | ||
| abortController, | ||
| }) | ||
|
|
||
| // Close the MCP client only after the SSE stream fully drains. | ||
| return toServerSentEventsResponse(closeMcpOnDrain(stream, client), { | ||
| abortController, | ||
| }) | ||
| } catch (error: any) { | ||
| console.error('[api.mcp-manual] Error:', { | ||
| message: error?.message, | ||
| name: error?.name, | ||
| stack: error?.stack, | ||
| }) | ||
| if (error.name === 'AbortError' || abortController.signal.aborted) { | ||
| return new Response(null, { status: 499 }) | ||
| } | ||
| return new Response( | ||
| JSON.stringify({ error: error.message || 'An error occurred' }), | ||
| { | ||
| status: 500, | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| }, | ||
| ) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="examples/ts-react-chat/src/routes/api.mcp-manual.ts"
echo "== File exists? =="
ls -l "$FILE" || exit 1
echo
echo "== Tail/Head context (around lines 50-190) =="
nl -ba "$FILE" | sed -n '45,190p'Repository: TanStack/ai
Length of output: 284
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="examples/ts-react-chat/src/routes/api.mcp-manual.ts"
echo "== Lines 50-190 (with line numbers via awk) =="
awk 'NR>=50 && NR<=190 {printf "%6d\t%s\n", NR, $0}' "$FILE"Repository: TanStack/ai
Length of output: 5054
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "closeMcpOnDrain|createMCPClient\\b|everythingTransport\\b|mcpResourceToContentPart|mcpPromptToMessages|client\\.close\\(" examples/ts-react-chat/src/routes/api.mcp-manual.ts examples/ts-react-chat/src/routes -S || true
echo
echo "== closeMcpOnDrain definition(s) =="
rg -n "function closeMcpOnDrain|closeMcpOnDrain\\b" -S examples/ts-react-chat/src || trueRepository: TanStack/ai
Length of output: 3200
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="examples/ts-react-chat/src/routes/api.mcp-manual.ts"
echo "== File head (1-120) =="
awk 'NR>=1 && NR<=120 {printf "%6d\t%s\n", NR, $0}' "$FILE"Repository: TanStack/ai
Length of output: 5373
Close the MCP client on non-stream error paths.
client is closed only when client.tools() fails or after the SSE stream drains via closeMcpOnDrain; if an error occurs after createMCPClient(...) but before toServerSentEventsResponse(closeMcpOnDrain(...)) returns (e.g., during chat(...) or toServerSentEventsResponse(...)), the outer catch runs without calling client.close(), leaking the stdio MCP process.
Suggested fix
- try {
+ let client: MCPClient | undefined
+ try {
// --- MCP: create and connect to the everything server (keyless, stdio) ---
- const client = await createMCPClient({
+ client = await createMCPClient({
transport: everythingTransport(),
})
@@
- } catch (error: any) {
+ } catch (error: any) {
+ if (client) {
+ await client.close().catch(() => {})
+ }
console.error('[api.mcp-manual] Error:', {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| // --- MCP: create and connect to the everything server (keyless, stdio) --- | |
| const client = await createMCPClient({ | |
| transport: everythingTransport(), | |
| }) | |
| let tools | |
| try { | |
| // Auto-discover all tools from the MCP server. | |
| tools = await client.tools() | |
| } catch (error) { | |
| await client.close() | |
| throw error | |
| } | |
| // --- MCP: resources — inject the first resource as context (if any) --- | |
| const contextMessages: Array<ModelMessage> = [] | |
| try { | |
| const resources = await client.resources() | |
| if (resources.length > 0) { | |
| // Read the first resource and convert each content block to a ContentPart. | |
| const readResult = await client.readResource(resources[0]!.uri) | |
| const parts = readResult.contents.map(mcpResourceToContentPart) | |
| if (parts.length > 0) { | |
| contextMessages.push({ | |
| role: 'user', | |
| content: [ | |
| ...parts, | |
| { | |
| type: 'text', | |
| content: | |
| '[MCP resource context injected from server-everything — use this as background information if relevant]', | |
| }, | |
| ], | |
| }) | |
| } | |
| } | |
| } catch { | |
| // Resources are optional — proceed without them if unavailable. | |
| } | |
| // --- MCP: prompts — prepend the first available prompt (if any) --- | |
| try { | |
| const availablePrompts = await client.prompts() | |
| if (availablePrompts.length > 0) { | |
| const firstPrompt = availablePrompts[0]! | |
| const promptResult = await client.getPrompt(firstPrompt.name) | |
| const promptMessages = mcpPromptToMessages(promptResult) | |
| // Prepend prompt messages before resource context and user messages. | |
| contextMessages.unshift(...promptMessages) | |
| } | |
| } catch { | |
| // Prompts are optional — proceed without them if unavailable. | |
| } | |
| // OPENAI_API_KEY is used by the LLM adapter (separate from the | |
| // keyless MCP server transport which needs no credentials). | |
| // The model is encoded in the adapter; do not pass it separately. | |
| const stream = chat({ | |
| adapter: openaiText('gpt-4o'), | |
| messages: [...contextMessages, ...params.messages], | |
| tools, | |
| agentLoopStrategy: maxIterations(20), | |
| threadId: params.threadId, | |
| runId: params.runId, | |
| abortController, | |
| }) | |
| // Close the MCP client only after the SSE stream fully drains. | |
| return toServerSentEventsResponse(closeMcpOnDrain(stream, client), { | |
| abortController, | |
| }) | |
| } catch (error: any) { | |
| console.error('[api.mcp-manual] Error:', { | |
| message: error?.message, | |
| name: error?.name, | |
| stack: error?.stack, | |
| }) | |
| if (error.name === 'AbortError' || abortController.signal.aborted) { | |
| return new Response(null, { status: 499 }) | |
| } | |
| return new Response( | |
| JSON.stringify({ error: error.message || 'An error occurred' }), | |
| { | |
| status: 500, | |
| headers: { 'Content-Type': 'application/json' }, | |
| }, | |
| ) | |
| } | |
| let client: MCPClient | undefined | |
| try { | |
| // --- MCP: create and connect to the everything server (keyless, stdio) --- | |
| client = await createMCPClient({ | |
| transport: everythingTransport(), | |
| }) | |
| let tools | |
| try { | |
| // Auto-discover all tools from the MCP server. | |
| tools = await client.tools() | |
| } catch (error) { | |
| await client.close() | |
| throw error | |
| } | |
| // --- MCP: resources — inject the first resource as context (if any) --- | |
| const contextMessages: Array<ModelMessage> = [] | |
| try { | |
| const resources = await client.resources() | |
| if (resources.length > 0) { | |
| // Read the first resource and convert each content block to a ContentPart. | |
| const readResult = await client.readResource(resources[0]!.uri) | |
| const parts = readResult.contents.map(mcpResourceToContentPart) | |
| if (parts.length > 0) { | |
| contextMessages.push({ | |
| role: 'user', | |
| content: [ | |
| ...parts, | |
| { | |
| type: 'text', | |
| content: | |
| '[MCP resource context injected from server-everything — use this as background information if relevant]', | |
| }, | |
| ], | |
| }) | |
| } | |
| } | |
| } catch { | |
| // Resources are optional — proceed without them if unavailable. | |
| } | |
| // --- MCP: prompts — prepend the first available prompt (if any) --- | |
| try { | |
| const availablePrompts = await client.prompts() | |
| if (availablePrompts.length > 0) { | |
| const firstPrompt = availablePrompts[0]! | |
| const promptResult = await client.getPrompt(firstPrompt.name) | |
| const promptMessages = mcpPromptToMessages(promptResult) | |
| // Prepend prompt messages before resource context and user messages. | |
| contextMessages.unshift(...promptMessages) | |
| } | |
| } catch { | |
| // Prompts are optional — proceed without them if unavailable. | |
| } | |
| // OPENAI_API_KEY is used by the LLM adapter (separate from the | |
| // keyless MCP server transport which needs no credentials). | |
| // The model is encoded in the adapter; do not pass it separately. | |
| const stream = chat({ | |
| adapter: openaiText('gpt-4o'), | |
| messages: [...contextMessages, ...params.messages], | |
| tools, | |
| agentLoopStrategy: maxIterations(20), | |
| threadId: params.threadId, | |
| runId: params.runId, | |
| abortController, | |
| }) | |
| // Close the MCP client only after the SSE stream fully drains. | |
| return toServerSentEventsResponse(closeMcpOnDrain(stream, client), { | |
| abortController, | |
| }) | |
| } catch (error: any) { | |
| if (client) { | |
| await client.close().catch(() => {}) | |
| } | |
| console.error('[api.mcp-manual] Error:', { | |
| message: error?.message, | |
| name: error?.name, | |
| stack: error?.stack, | |
| }) | |
| if (error.name === 'AbortError' || abortController.signal.aborted) { | |
| return new Response(null, { status: 499 }) | |
| } | |
| return new Response( | |
| JSON.stringify({ error: error.message || 'An error occurred' }), | |
| { | |
| status: 500, | |
| headers: { 'Content-Type': 'application/json' }, | |
| }, | |
| ) | |
| } |
🤖 Prompt for 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.
In `@examples/ts-react-chat/src/routes/api.mcp-manual.ts` around lines 69 - 158,
The MCP client created by createMCPClient(...) can leak if an error happens
after creation (e.g., during chat(...) or toServerSentEventsResponse(...));
ensure client.close() is always called on error paths by moving client
declaration into the outer scope and adding a finally or ensuring the outer
catch closes the client if it exists and is not already closed (reference
createMCPClient, client.tools, client.close, closeMcpOnDrain, chat,
toServerSentEventsResponse, abortController); keep existing early-close where
client.tools() fails and still close in the outer catch/finally so the stdio MCP
process is not leaked.
| rehypePlugins={[ | ||
| rehypeRaw, | ||
| rehypeSanitize, | ||
| rehypeHighlight, | ||
| remarkGfm, | ||
| ]} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="examples/ts-react-chat/src/routes/mcp-demo.tsx"
if [ ! -f "$FILE" ]; then
echo "Missing file: $FILE" >&2
exit 1
fi
echo "---- file: $FILE (imports around remark-gfm / react-markdown) ----"
rg -n "remarkGfm|remark-gfm|ReactMarkdown|rehypePlugins|remarkPlugins|rehypeRaw|rehypeSanitize|rehypeHighlight" "$FILE" || true
echo
echo "---- snippet around ReactMarkdown props (lines ~90-150) ----"
sed -n '80,160p' "$FILE" | nl -ba -w2 -s':'Repository: TanStack/ai
Length of output: 806
🌐 Web query:
react-markdown remark-gfm should be used in remarkPlugins not rehypePlugins
💡 Result:
Yes, that is correct. You should use remark-gfm in the remarkPlugins array, not the rehypePlugins array, when using react-markdown [1][2]. This is because remark-gfm is a remark plugin, which operates on the markdown syntax tree (mdast) [3][4][5]. The unified ecosystem distinguishes between these two types of plugins: remark plugins work with markdown (mdast) [5][6]. rehype plugins work with HTML (hast) [5][6]. In the react-markdown pipeline, remark plugins are processed first to handle markdown parsing and extensions, followed by a transformation step (remark-rehype) that converts the markdown to HTML, and finally, rehype plugins can be used to manipulate the resulting HTML [1][7]. Placing a remark plugin in the rehypePlugins array will not work as expected because the plugin will not receive the correct data format (mdast) to process [5].
Citations:
- 1: https://github.com/remarkjs/react-markdown?tab=readme-ov-file
- 2: https://www.npmjs.com/package/react-markdown
- 3: https://github.com/remarkjs/remark-gfm
- 4: https://github.com/remarkjs/remark-gfm/blob/main/readme.md
- 5: https://github.com/remarkjs/remark/blob/HEAD/doc/plugins.md
- 6: https://github.com/remarkjs/remark/blob/main/doc/plugins.md
- 7: https://unifiedjs.com/explore/package/react-markdown/
Use remarkPlugins for remark-gfm (not rehypePlugins) in examples/ts-react-chat/src/routes/mcp-demo.tsx (around lines 115-120).
remark-gfm is a remark plugin (mdast), but it’s currently passed in rehypePlugins, so GFM parsing may not run as intended.
Suggested fix
<ReactMarkdown
rehypePlugins={[
rehypeRaw,
rehypeSanitize,
rehypeHighlight,
- remarkGfm,
]}
+ remarkPlugins={[remarkGfm]}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| rehypePlugins={[ | |
| rehypeRaw, | |
| rehypeSanitize, | |
| rehypeHighlight, | |
| remarkGfm, | |
| ]} | |
| <ReactMarkdown | |
| rehypePlugins={[ | |
| rehypeRaw, | |
| rehypeSanitize, | |
| rehypeHighlight, | |
| ]} | |
| remarkPlugins={[remarkGfm]} | |
| > |
🤖 Prompt for 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.
In `@examples/ts-react-chat/src/routes/mcp-demo.tsx` around lines 115 - 120, The
code passes remarkGfm into the rehypePlugins array, so GFM parsing won't run;
update the component props to remove remarkGfm from rehypePlugins and instead
add it to remarkPlugins (i.e., keep rehypePlugins as [rehypeRaw, rehypeSanitize,
rehypeHighlight] and add remarkPlugins={[remarkGfm]}), ensuring the component
uses the remarkPlugins prop with remarkGfm.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
examples/ts-react-chat/src/routes/api.mcp-manual.ts (1)
151-156:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't return raw backend error messages to the browser.
This endpoint is directly callable from the demo UI, so echoing
error.messagehere can expose MCP/OpenAI/stdio internals to any caller. Keep the detailed diagnostics inconsole.errorand return a fixed 500 payload instead.Suggested fix
return new Response( - JSON.stringify({ error: error.message || 'An error occurred' }), + JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }, )🤖 Prompt for 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. In `@examples/ts-react-chat/src/routes/api.mcp-manual.ts` around lines 151 - 156, The handler currently returns JSON.stringify({ error: error.message || 'An error occurred' }) which exposes internal backend errors to the browser; instead call console.error(error) (or processLogger.error) to record diagnostics and replace the response body with a fixed generic payload such as JSON.stringify({ error: 'Internal Server Error' }) while keeping status: 500 and Content-Type: 'application/json' in the new Response(...) call so no raw error.message is echoed to clients.
🤖 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.
Outside diff comments:
In `@examples/ts-react-chat/src/routes/api.mcp-manual.ts`:
- Around line 151-156: The handler currently returns JSON.stringify({ error:
error.message || 'An error occurred' }) which exposes internal backend errors to
the browser; instead call console.error(error) (or processLogger.error) to
record diagnostics and replace the response body with a fixed generic payload
such as JSON.stringify({ error: 'Internal Server Error' }) while keeping status:
500 and Content-Type: 'application/json' in the new Response(...) call so no raw
error.message is echoed to clients.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: fd79d535-7a22-4b28-91a0-5e991ce866a8
📒 Files selected for processing (4)
examples/ts-react-chat/src/routes/api.mcp-chat.tsexamples/ts-react-chat/src/routes/api.mcp-manual.tsexamples/ts-react-chat/src/routes/api.mcp-pool.tsexamples/ts-react-chat/src/routes/mcp-demo.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- examples/ts-react-chat/src/routes/api.mcp-pool.ts
- examples/ts-react-chat/src/routes/api.mcp-chat.ts
- examples/ts-react-chat/src/routes/mcp-demo.tsx
# Conflicts: # packages/ai/skills/ai-core/tool-calling/SKILL.md # packages/ai/src/activities/chat/index.ts # pnpm-lock.yaml # testing/e2e/package.json
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