MCP Server Tools
@tanstack/ai-mcp는 TanStack AI를 위한 호스트 측 Model Context Protocol 클라이언트입니다. 서버 라우트를 MCP 호환 서버에 연결하고 해당 서버의 도구, 리소스, 프롬프트를 chat() 내부에서 사용할 수 있게 합니다.
MCP 도구 실행은 서버 측에서만 가능합니다.
createMCPClient호출은 서버 라우트(또는 서버리스 함수)에 있어야 하며 브라우저 코드에 작성하면 안 됩니다.
설치
pnpm add @tanstack/ai-mcp @modelcontextprotocol/sdk
빠른 시작
가장 간단한 통합 방법은 관리형 mcp 옵션을 사용하는 것입니다. 클라이언트를 chat()에 전달하면 도구를 검색하고 실행이 끝날 때 연결을 닫으므로 수명 주기 코드가 전혀 필요하지 않습니다.
// src/routes/api.chat.ts (TanStack Start)
import { createFileRoute } from '@tanstack/react-router'
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'
export const Route = createFileRoute('/api/chat')({
server: {
handlers: {
POST: async ({ request }) => {
const { messages } = await request.json()
const mcp = await createMCPClient({
transport: {
type: 'http',
url: 'https://my-mcp-server.example.com/mcp',
},
})
// chat() discovers the tools and closes the client when the run ends.
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
mcp: { clients: [mcp] },
})
return toServerSentEventsResponse(stream)
},
},
},
})
완전히 타입이 지정된 도구 인수, 리소스, 프롬프트 또는 직접 관리하는 수명 주기가 필요합니까? 대신 도구를 수동으로 펼치십시오. 수동 MCP: 타입이 지정된 도구, 리소스 및 프롬프트와 아래 수명 주기 섹션을 참고하십시오.
클라이언트 측에서는 다른 TanStack AI 엔드포인트와 동일하게 useChat으로 스트림을 소비합니다.
// src/components/Chat.tsx
import { useChat } from '@tanstack/ai-react'
import { fetchServerSentEvents } from '@tanstack/ai-client'
export function Chat() {
const { messages, sendMessage, status } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong>{' '}
{m.parts.find((p) => p.type === 'text')?.content}
</div>
))}
<button
onClick={() => sendMessage({ content: 'Hello' })}
disabled={status === 'streaming'}
>
Send
</button>
</div>
)
}
전송
HTTP (스트리밍 가능한 HTTP)
원격 서버에 권장되는 전송입니다. MCP Streamable HTTP 프로토콜을 사용합니다.
import { createMCPClient } from '@tanstack/ai-mcp'
const mcp = await createMCPClient({
transport: {
type: 'http',
url: 'https://my-mcp-server.example.com/mcp',
headers: { Authorization: `Bearer ${process.env.MCP_TOKEN}` },
},
})
SSE (서버 전송 이벤트)
레거시 SSE 전송을 구현하는 서버에 사용합니다.
import { createMCPClient } from '@tanstack/ai-mcp'
const mcp = await createMCPClient({
transport: {
type: 'sse',
url: 'https://my-mcp-server.example.com/sse',
headers: { Authorization: `Bearer ${process.env.MCP_TOKEN}` },
},
})
stdio (Node.js 전용)
로컬 MCP 프로세스를 생성할 때 사용합니다. stdio는 Node 네이티브 모듈을 가져오므로 엣지 번들을 깨끗하게 유지하도록 서브패스 import 뒤에 격리되어 있습니다.
import { stdioTransport } from '@tanstack/ai-mcp/stdio'
import { createMCPClient } from '@tanstack/ai-mcp'
const mcp = await createMCPClient({
transport: stdioTransport({
command: 'node',
args: ['./my-mcp-server.js'],
env: { API_KEY: process.env.API_KEY ?? '' },
}),
})
사용자 지정 전송 (탈출구)
Transport 인스턴스를 무엇이든 transport 옵션으로 직접 전달할 수 있습니다. 프로세스 내부 테스트를 위해 InMemoryTransport가 @tanstack/ai-mcp에서 다시 내보내집니다.
import { createMCPClient, InMemoryTransport } from '@tanstack/ai-mcp'
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
const mcp = await createMCPClient({ transport: clientTransport })
사용자 지정 네트워크 전송에는 SDK의 Transport와 호환되는 인스턴스를 전달합니다.
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
const transport = new StreamableHTTPClientTransport(new URL('https://example.com/mcp'))
const mcp = await createMCPClient({ transport })
인증
정적 토큰 (헤더)
미리 발급된 API 키 또는 bearer 토큰을 받는 서버에는 http/sse 전송 설정에 headers를 전달합니다. 모든 요청에 해당 헤더가 전송됩니다.
import { createMCPClient } from '@tanstack/ai-mcp'
const mcp = await createMCPClient({
transport: {
type: 'http',
url: 'https://my-mcp-server.example.com/mcp',
headers: { Authorization: `Bearer ${process.env.MCP_TOKEN}` },
},
})
OAuth (authProvider)
MCP 인증 사양(OAuth 2.1)을 구현하는 서버에는 http/sse 전송 설정에 authProvider를 전달합니다. 공식 SDK(@modelcontextprotocol/sdk/client/auth.js)의 모든 OAuthClientProvider를 받으며, SDK 전송이 토큰 첨부, 갱신, 401 재시도를 처리하므로 TanStack AI에서 추가로 연결할 필요가 없습니다.
import { createMCPClient } from '@tanstack/ai-mcp'
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
import { myTokenStore } from './token-store'
// Server-side: back the provider with tokens you persist (database, KV, ...).
// `tokens()` returning a valid (or refreshable) token set is all the SDK
// needs to authenticate requests.
const myOAuthProvider: OAuthClientProvider = myTokenStore.provider()
const mcp = await createMCPClient({
transport: {
type: 'http',
url: 'https://my-mcp-server.example.com/mcp',
authProvider: myOAuthProvider,
},
})
인터랙티브 인증 (리디렉션 흐름). 사용자가 리디렉션되어 돌아온 후
finishAuth(code)을 전송 계층에서 호출해야 하며,createMCPClient는 내부적으로 전송 계층을 구성하므로 이를 노출할 수 없습니다. 인터랙티브 흐름이 필요한 경우 자체적으로 전송 계층을 구축하여 전달하세요 (위에서 언급한 탈출구):StreamableHTTPClientTransport을authProvider와 함께 생성하고 참조를 유지한 후, OAuth 콜백 라우트에서transport.finishAuth(code)를 호출한 다음 전송 계층을createMCPClient({ transport })에 전달합니다. 일반적인 서버 측 사용 — 사전 준비되거나 저장된 토큰으로 작동하는 리프레시를 기반으로 하는 제공자 — 의 경우 위와 같은 설정 양식이 모두 필요합니다.
타입 안전성의 세 가지 모드
모드 1 — 자동 발견 (client.tools())
인수 없이 tools()를 호출하면 서버가 제공하는 모든 도구를 검색합니다. 추가 설정은 필요하지 않습니다. 도구 인수 타입은 컴파일 시점에 unknown이며 MCP JSON Schema는 런타임 검증에 사용됩니다.
import { createMCPClient } from '@tanstack/ai-mcp'
const mcp = await createMCPClient({
transport: { type: 'http', url: 'https://my-mcp-server.example.com/mcp' },
})
const tools = await mcp.tools()
// tools: ServerTool[] — args typed unknown at compile time
작업 기반 도구는 제외됩니다.
execution.taskSupport: 'required'(실험적인 MCP 작업 기능) 을 선언하는 도구는 SDK 의tasks/callToolStream흐름을 통해서만 실행할 수 있으며,@tanstack/ai-mcp는 아직 지원하지 않으므로 평범한callTool은 서버에서-32600로 거부됩니다. 발견은 이를 건너뛰므로 모델이 성공할 수 없는 도구를 제안받지 않습니다.
모드 2 — 명시적 정의 (client.tools([...defs]))
TanStack toolDefinition() 인스턴스를 전달하면 완전한 TypeScript 타입과 Zod 검증을 얻습니다. 이름을 지정한 도구만 반환됩니다(허용 목록). 서버에 이름이 없으면 MCPToolNotFoundError가 발생하고, 지정한 도구에 태스크 기반 실행이 필요하면 MCPTaskRequiredToolError가 발생합니다(모드 1 참고).
import { toolDefinition } from '@tanstack/ai'
import { createMCPClient } from '@tanstack/ai-mcp'
import { z } from 'zod'
const searchDef = toolDefinition({
name: 'search',
description: 'Search for items',
inputSchema: z.object({ query: z.string() }),
outputSchema: z.array(z.object({ id: z.string(), title: z.string() })),
})
const mcp = await createMCPClient({
transport: { type: 'http', url: 'https://my-mcp-server.example.com/mcp' },
})
const tools = await mcp.tools([searchDef])
// tools[0].execute is typed: (args: { query: string }) => ...
모드 3 — 생성된 타입 (createMCPClient<GeneratedServer>)
실행 중인 서버를 대상으로 CLI를 실행해 서버별 interface 타입을 생성한 다음, 생성된 타입을 제네릭으로 전달합니다. 도구 이름이 서버의 리터럴 이름으로 좁혀지고 풀 설정 키가 컴파일 시점에 검사되며 런타임 오버헤드는 없습니다. (검색 경로에서 도구 인수는 타입이 지정되지 않으므로 타입이 지정된 인수에는 모드 2를 함께 사용하십시오.)
전체
mcp.config.ts설정,generateCLI, 생성된 타입을createMCPClient및createMCPClients에 연결하는 방법은 MCP 타입 생성을 참고하십시오.
도구 제목 및 주석
MCP 서버는 각 도구와 함께 표시 및 동작 메타데이터를 제공할 수 있습니다. 여기에는 사람이 읽을 수 있는 title과 annotations 힌트 집합(readOnlyHint, destructiveHint, idempotentHint, openWorldHint, 그리고 레거시 annotations.title)이 포함됩니다. @tanstack/ai-mcp는 자동 검색과 명시적 정의 경로 모두에서 이를 검색된 각 도구의 metadata.mcp로 전달하므로 UI에서 도구에 레이블을 지정하고 확인 단계가 필요한 도구를 결정할 수 있습니다.
metadata.mcp 필드 | 값 |
|---|---|
title | string — 스펙의 우선순위로 해결되는 디스플레이 이름: title → annotations.title → name 도구의 도구 호출. 항상 설정해야 합니다. |
annotations | 서버의 annotations 객체로, 그대로 전달됩니다. 서버가 없음을 선언하면 부재합니다. |
serverToolName | string — 서버 네이티브 (프래픽 없는) 도구 이름. |
serverId | 클라이언트의 prefix (없을 경우 정의되지 않음). |
uiResourceUri | MCP Apps 위젯 링크, 도구가 하나를 선언할 때. |
이 블록은 타입이 지정되어 있으므로 그대로 읽으면 됩니다. tools()는 McpServerTool을 반환합니다. 이는 일반 ServerTool이며(chat({ tools })에 바로 전달할 수 있음), metadata.mcp가 항상 존재하고 위 표의 형태라는 사실이 정적으로 알려져 있습니다. 어노테이션이나 캐스트가 필요 없고 필드 이름을 잘못 쓰면 컴파일 오류가 발생합니다.
import { createMCPClient } from '@tanstack/ai-mcp'
const url = 'https://my-mcp-server.example.com/mcp'
// Trust comes from YOUR configuration — an allowlist of servers you operate or
// have vetted — never from anything the server itself sends.
const trustedServers = new Set(['https://my-mcp-server.example.com/mcp'])
const serverIsTrusted = trustedServers.has(url)
const mcp = await createMCPClient({ transport: { type: 'http', url } })
const tools = (await mcp.tools()).map((tool) => {
const meta = tool.metadata.mcp
const advertisedReadOnly = meta.annotations?.readOnlyHint === true
return {
...tool,
// Approval is the default. A hint may only relax it for a server whose
// trust you established independently; on any other server the same hint
// is a label/recommendation and changes nothing about approval.
needsApproval: !(serverIsTrusted && advertisedReadOnly),
}
})
어노테이션은 권고일 뿐 보안 경계가 아닙니다. MCP 사양은
title을 포함한 모든 필드가 도구의 실제 동작을 정확히 설명하지 않을 수 있는 힌트라고 명시합니다. 악의적이거나 침해된 서버는 무엇이든 주장할 수 있습니다(레코드를 삭제하는 도구에readOnlyHint: true를 지정하는 경우 포함). 신뢰할 수 없는 서버의 보안 경계로 사용하지 말고, 힌트만으로 승인, 샌드박스 또는 권한 부여를 면제하지 마십시오. 독립적으로 신뢰할 수 있다고 확인한 서버에서는 위와 같이 힌트로 확인 단계를 완화할 수 있지만, 그 외에는 어노테이션을 표시 레이블과 권고로만 취급하십시오.readOnlyHint는 이에 따라 동작시키지 말고 배지로 표시하십시오(아래 UI 예 참고).
제목은 표시 전용입니다. 모델에 전송되는 도구 name을 변경하지 않으며, prefix도 제목이 아니라 이름(wx_get_weather)에 적용됩니다.
자체 시그니처에서 형태의 이름을 지정해야 한다면 McpToolMetadata와 McpServerTool이 모두 내보내집니다(ToolAnnotations도 MCP SDK에서 다시 내보내집니다). 단순히 블록을 읽는 데는 필요하지 않습니다.
UI에서 도구에 레이블을 지정하려면 서버 라우트에서 전달된 메타데이터를 노출하십시오. MCP 클라이언트 자체는 서버 측에 있어야 합니다.
// src/routes/api.mcp-tools.ts
import { createFileRoute } from '@tanstack/react-router'
import { createMCPClient } from '@tanstack/ai-mcp'
export const Route = createFileRoute('/api/mcp-tools')({
server: {
handlers: {
GET: async () => {
await using mcp = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
})
const catalog = (await mcp.tools()).map((tool) => ({
name: tool.name,
// `title` is always set — the fallback chain already ran.
title: tool.metadata.mcp.title,
description: tool.description,
readOnly: tool.metadata.mcp.annotations?.readOnlyHint === true,
}))
return Response.json({ tools: catalog })
},
},
},
})
// src/components/ToolCatalog.tsx
import { useEffect, useState } from 'react'
interface ToolSummary {
name: string
title: string
description?: string
readOnly: boolean
}
export function ToolCatalog() {
const [tools, setTools] = useState<Array<ToolSummary>>([])
useEffect(() => {
fetch('/api/mcp-tools')
.then((res) => res.json())
.then((body: { tools: Array<ToolSummary> }) => setTools(body.tools))
}, [])
return (
<ul>
{tools.map((tool) => (
<li key={tool.name}>
{/* Server-declared title, with the hint driving the badge */}
<strong>{tool.title}</strong> {tool.readOnly ? '(read-only)' : '(writes)'}
<div>{tool.description}</div>
</li>
))}
</ul>
)
}
다중 서버 풀
createMCPClients는 여러 서버에 병렬로 연결하고 도구를 하나의 평면 배열로 병합합니다. 이름 충돌을 방지하기 위해 각 서버의 도구에는 설정 키가 자동으로 접두사로 붙습니다.
import { createMCPClients } from '@tanstack/ai-mcp'
const pool = await createMCPClients({
github: { transport: { type: 'http', url: process.env.GITHUB_MCP_URL! } },
linear: { transport: { type: 'http', url: process.env.LINEAR_MCP_URL! } },
})
// tools: [github_search_repos, github_create_issue, linear_create_issue, ...]
const tools = await pool.tools()
pool.tools()는 모든 서버의 도구를 수집하며 접두사를 붙인 뒤 이름이 충돌하면 DuplicateToolNameError를 발생시킵니다.
서버별 접근
import { createMCPClients } from '@tanstack/ai-mcp'
const pool = await createMCPClients({
github: { transport: { type: 'http', url: process.env.GITHUB_MCP_URL! } },
linear: { transport: { type: 'http', url: process.env.LINEAR_MCP_URL! } },
})
const linearTools = await pool.clients.linear!.tools()
const resources = await pool.clients.github!.resources()
접두어 비활성화 또는 오버라이드
import { createMCPClients } from '@tanstack/ai-mcp'
const pool = await createMCPClients({
github: {
transport: { type: 'http', url: process.env.GITHUB_MCP_URL! },
prefix: 'gh', // override: "gh_search_repos"
},
internal: {
transport: { type: 'http', url: process.env.INTERNAL_MCP_URL! },
prefix: '', // disable prefix entirely
},
})
풀 닫기
await pool.close()
// or
await using pool = await createMCPClients({ /* server configs */ })
서버 중 하나라도 연결에 실패하면 오류가 발생하기 전에 이미 연결된 클라이언트를 닫으므로 누수가 없습니다.
수명 주기
빠른 시작처럼
mcp옵션으로 클라이언트를chat()에 전달하면 이 섹션 전체를 건너뛸 수 있습니다.chat()이 도구를 검색하고 실행이 끝날 때 연결을 닫습니다. 관리형 MCP와chat()을 참고하십시오. 도구를 수동으로 펼치고 직접close()를 관리하는 경우에만 계속 읽으십시오.
클라이언트를 수동으로 관리할 때 클라이언트의 소유자는 호출자이며 chat()은 절대 닫지 않습니다.
도구는 응답 스트림을 소비하는 동안 지연 실행되므로 스트림을 완전히 소비한 후에만 클라이언트를 닫으십시오. 스트리밍 Response를 반환하는 라우트 핸들러에서 return 주변의 try/finally(또는 함수 스코프의 await using)는 본문 스트리밍이 시작되기 전에 클라이언트를 닫으므로 진행 중인 도구 호출이 실패합니다. 대신 미들웨어의 종료 훅에서 닫으십시오.
스트리밍 라우트 핸들러 — 미들웨어로 닫기
에이전트 루프가 끝난 후 실행마다 onFinish/onAbort/onError 중 정확히 하나가 실행됩니다.
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'
export async function POST(request: Request) {
const { messages } = await request.json()
const url = 'https://my-mcp-server.example.com/mcp'
const mcp = await createMCPClient({ transport: { type: 'http', url } })
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
tools: await mcp.tools(),
middleware: [
{
name: 'mcp-close',
onFinish: () => mcp.close(),
onAbort: () => mcp.close(),
onError: () => mcp.close(),
},
],
})
return toServerSentEventsResponse(stream)
}
수동 닫기 — 스코프 안에서 스트림을 소비할 때
스코프가 종료되기 전에 스트림을 소비한다면 try/finally가 올바른 방법입니다.
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const url = 'https://my-mcp-server.example.com/mcp'
const mcp = await createMCPClient({ transport: { type: 'http', url } })
try {
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
tools: await mcp.tools(),
})
for await (const chunk of stream) {
// handle chunks — the stream is fully consumed inside this block
}
} finally {
await mcp.close()
}
await using (명시적 리소스 관리)
런타임이 Symbol.asyncDispose를 지원한다면(Node 18.2 이상에서 TypeScript target: "es2022" 및 lib: ["esnext"] 사용), 동일한 스코프 내 소비 규칙이 적용됩니다. 블록이 종료될 때 클라이언트가 닫힙니다.
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient } from '@tanstack/ai-mcp'
const messages = [{ role: 'user' as const, content: 'Hello' }]
const url = 'https://my-mcp-server.example.com/mcp'
await using mcp = await createMCPClient({ transport: { type: 'http', url } })
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
tools: await mcp.tools(),
})
for await (const chunk of stream) {
// handle chunks
}
// mcp.close() is called automatically when the block exits
도구 이름 충돌
여러 소스의 도구를 섞을 때 이름이 중복되면 DuplicateToolNameError가 발생합니다.
import { createMCPClients, DuplicateToolNameError } from '@tanstack/ai-mcp'
const pool = await createMCPClients({
github: { transport: { type: 'http', url: process.env.GITHUB_MCP_URL! } },
linear: { transport: { type: 'http', url: process.env.LINEAR_MCP_URL! } },
})
try {
const tools = await pool.tools()
} catch (err) {
if (err instanceof DuplicateToolNameError) {
console.error('Conflicting tool name:', err.toolName)
// Fix: set a unique prefix on one of the clients
}
}
충돌을 피하려면 각 클라이언트에 고유한 prefix를 사용하십시오. createMCPClients는 설정 키를 사용해 이를 자동으로 처리합니다.
게으른 도구 발견
{ lazy: true }를 전달하면 LLM이 명시적으로 요청할 때까지 도구 스키마 전송을 지연합니다. 도구가 많은 서버를 사용할 때 토큰 사용량을 줄일 수 있습니다.
import { createMCPClient } from '@tanstack/ai-mcp'
const mcp = await createMCPClient({
transport: { type: 'http', url: 'https://my-mcp-server.example.com/mcp' },
})
const tools = await mcp.tools({ lazy: true })
// All tools are marked lazy: true
풀과도 호환됩니다:
import { createMCPClients } from '@tanstack/ai-mcp'
const pool = await createMCPClients({
github: { transport: { type: 'http', url: process.env.GITHUB_MCP_URL! } },
linear: { transport: { type: 'http', url: process.env.LINEAR_MCP_URL! } },
})
const tools = await pool.tools({ lazy: true })
LLM이 런타임에 지연 도구를 검색하는 방법은 지연 도구 검색을 참고하십시오.
chat()을(를) 사용한 MCP
위의 빠른 시작에서는 tools: await mcp.tools()를 통해 도구를 chat()에 수동으로 전달하고 클라이언트를 직접 닫습니다. 이어지는 두 가이드에서 더 풍부한 통합을 다룹니다.
검색과 수명 주기를
chat()에 맡기십시오.mcp옵션으로 실행 중인 클라이언트와 풀을chat()에 전달하면 도구를 검색하고 연결을 대신 닫으므로 라우트마다try/finally를 작성할 필요가 없습니다. 관리형 MCP와chat()을 참고하십시오.
리소스, 프롬프트 및 완전히 타입이 지정된 수동 도구. MCP 리소스와 프롬프트를
chat()실행에 주입하고, 진행 중인 MCP 호출을 취소하며,toolDefinition으로 타입이 지정된 도구를 펼칠 수 있습니다. 수동 MCP: 타입이 지정된 도구, 리소스 및 프롬프트를 참고하십시오.
오류 참조
| 에러 클래스 | 발생 조건 |
|---|---|
MCPConnectionError | createMCPClient 가 연결에 실패하거나 close() 호출 후 메서드가 호출될 경우 |
DuplicateToolNameError | 한 클라이언트 내 또는 풀을 가로질러 두 도구가 동일한 이름을 가질 경우 |
MCPToolNotFoundError | toolDefinition 이름이 tools([...defs]) 에 전달되었으나 서버에서 찾을 수 없는 경우 |
MCPTaskRequiredToolError | toolDefinition 가 tools([...defs]) 로 전달된 도구는 작업 기반 실행이 필요한 도구 (execution.taskSupport: 'required') — 이러한 도구는 tools() 자동 발견에서도 제외됩니다. |
chat({ mcp }) 실행에서 여러 소스의 도구를 병합할 때 발생하는 MCPDuplicateToolNameError는 chat()을 사용한 관리형 MCP를 참고하십시오.