본문으로 건너뛰기

chat()으로 관리하는 MCP

하나 이상의 활성 MCP 클라이언트(또는 풀)가 있고, 각 라우트마다 상용구 await client.tools() 호출과 try/finally close() 블록을 작성하지 않고 모델이 해당 도구를 사용하게 하려는 경우를 살펴봅니다. 이 가이드를 마치면 mcp 옵션을 통해 해당 클라이언트를 chat()에 전달하고 검색과 수명 주기를 모두 chat()이 처리하도록 할 수 있습니다.

관리형(mcp prop)과 수동(tools spread) 비교

  • 검색 + 수명 주기를 관리하고 런타임 타입(unknown 인수) 도구로 충분하다면 mcp: { clients: [...] }를 사용합니다.
  • 완전한 타입의 MCP 도구가 필요하다면 tools: [...await client.tools([toolDefinition(...)])]를 사용합니다. defs 오버로드를 사용하면 Zod로 검증되고 TypeScript 타입이 지정된 인수를 얻을 수 있습니다. 수동 MCP: 타입이 지정된 도구, 리소스 및 프롬프트타입 안전성의 세 가지 모드를 참고합니다.

둘 다 동일한 chat() 호출에서 함께 사용할 수 있습니다. mcp.clients의 도구는 tools를 통해 명시적으로 전달한 도구와 병합됩니다.

클라이언트를 chat()에 전달하기

가장 간단한 방법은 클라이언트를 생성해 chat()에 전달하고 실행이 정리하도록 맡기는 것입니다. connection의 기본값은 'close'이므로 성공, 오류 또는 중단 여부와 관계없이 실행이 끝나면 클라이언트가 자동으로 닫힙니다.

// src/routes/api.chat.ts
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 mcpClient = await createMCPClient({
transport: {
type: 'http',
url: process.env.MCP_URL!,
headers: { Authorization: `Bearer ${process.env.MCP_TOKEN}` },
},
})

// chat() discovers mcpClient's tools and closes the connection when done.
// No try/finally needed.
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
mcp: {
clients: [mcpClient],
// connection: 'close' is the default — shown here for clarity
connection: 'close',
},
})

return toServerSentEventsResponse(stream)
},
},
},
})

아래 예제는 변경되는 부분인 클라이언트 설정과 chat() 호출만 보여줍니다. 모두 위와 동일한 라우트 핸들러 형태에 넣어 사용할 수 있습니다.

여러 서버와 풀

MCPClient 인스턴스와 MCPClients 풀을 원하는 조합으로 전달합니다. 도구는 병렬로 검색되어 하나의 평면 도구 집합으로 병합됩니다. 풀은 이름 충돌을 방지하기 위해 각 서버의 도구에 설정 키를 자동으로 접두사로 붙입니다.

import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient, createMCPClients } from '@tanstack/ai-mcp'

const messages = [{ role: 'user' as const, content: 'Hello' }]

// A pool of two servers — their tools are prefixed "github_" and "linear_"
const githubLinearPool = await createMCPClients({
github: {
transport: {
type: 'http',
url: process.env.GITHUB_MCP_URL!,
headers: { Authorization: `Bearer ${process.env.GITHUB_MCP_TOKEN}` },
},
},
linear: {
transport: {
type: 'http',
url: process.env.LINEAR_MCP_URL!,
headers: { Authorization: `Bearer ${process.env.LINEAR_MCP_TOKEN}` },
},
},
})

// A standalone client for an internal server
const internalClient = await createMCPClient({
transport: { type: 'http', url: process.env.INTERNAL_MCP_URL! },
})

// All three servers' tools are merged: github_*, linear_*, plus internal tools
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
mcp: {
clients: [githubLinearPool, internalClient],
connection: 'close',
},
})

연결 유지하기

요청마다 새 MCP 연결을 생성하면 지연 시간이 늘어납니다. 요청 빈도가 높은 프로덕션 라우트에서는 모듈 수준에서 풀을 한 번 생성하고 connection: 'keep-alive'를 전달하여 chat()이 풀을 닫지 않도록 합니다. 풀이 다음 요청을 처리할 준비가 된 상태로 유지됩니다. (핵심이 모듈 범위와 핸들러 범위 중 어디에 배치하는지에 있으므로 전체 라우트를 보여줍니다.)

서버 라우트(src/routes/api.chat.ts):

import { createFileRoute } from '@tanstack/react-router'
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClients } from '@tanstack/ai-mcp'

// Created once when the module loads. Shared across all requests.
const sharedPool = await createMCPClients({
github: {
transport: {
type: 'http',
url: process.env.GITHUB_MCP_URL!,
headers: { Authorization: `Bearer ${process.env.GITHUB_MCP_TOKEN}` },
},
},
linear: {
transport: {
type: 'http',
url: process.env.LINEAR_MCP_URL!,
headers: { Authorization: `Bearer ${process.env.LINEAR_MCP_TOKEN}` },
},
},
})

export const Route = createFileRoute('/api/chat')({
server: {
handlers: {
POST: async ({ request }) => {
const { messages } = await request.json()

// keep-alive: sharedPool is never closed by chat(); stays warm for next call
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
mcp: {
clients: [sharedPool],
connection: 'keep-alive',
},
})

return toServerSentEventsResponse(stream)
},
},
},
})

클라이언트 컴포넌트(src/components/Chat.tsx):

import { useChat } from '@tanstack/ai-react'
import { fetchServerSentEvents } from '@tanstack/ai-client'

const chatOptions = {
connection: fetchServerSentEvents('/api/chat'),
}

export function Chat() {
const { messages, sendMessage, status } = useChat(chatOptions)

return (
<div>
<ul>
{messages.map((m) => (
<li key={m.id}>
<strong>{m.role}:</strong>{' '}
{m.parts.find((p) => p.type === 'text')?.content}
</li>
))}
</ul>
<button
onClick={() => sendMessage({ content: 'List my open GitHub issues' })}
disabled={status === 'streaming'}
>
Ask
</button>
</div>
)
}

지연 도구 검색

MCP 서버가 수십 개의 도구를 노출하면 모든 스키마를 모델에 전송할 때 프롬프트 크기와 비용이 증가합니다. lazyTools: true로 설정하면 모델이 명시적으로 요청할 때까지 도구 스키마 전송을 지연할 수 있습니다.

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 mcpClient = await createMCPClient({
transport: { type: 'http', url: process.env.LARGE_MCP_URL! },
})

const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
mcp: {
clients: [mcpClient],
connection: 'close',
// Tools are registered but schemas are withheld until the model asks
lazyTools: true,
},
})

lazyTools: true는 각 소스의 tools({ lazy: true }) 호출에 전달됩니다. 모델이 런타임에 지연 도구를 검색하고 로드하는 방법은 지연 도구 검색을, { lazy: true }client.tools()와 직접 사용하는 방법은 독립적인 지연 검색 섹션을 참고합니다.

검색 실패 처리

기본적으로 검색 중 어느 소스에서든 실패하면 chat()이 즉시 예외를 발생시킵니다(fail-fast). connection: 'close'인 경우 실제로 연결된 소스는 오류가 전파되기 전에 정리되므로 연결이 누수되지 않습니다.

즉시 실패(기본값):

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 mcpClient = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
})

// If discovery fails, chat() throws before the first model call.
// mcpClient is closed automatically (connection: 'close' default).
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
mcp: {
clients: [mcpClient],
},
})

불안정한 서버를 건너뛰고 계속 진행하기:

onDiscoveryError를 사용해 문제를 기록하고 정상적으로 반환하면 실패한 소스를 건너뛰고 나머지 클라이언트 도구로 실행을 계속합니다.

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 primaryClient = await createMCPClient({
transport: { type: 'http', url: process.env.PRIMARY_MCP_URL! },
})

const optionalClient = await createMCPClient({
transport: { type: 'http', url: process.env.OPTIONAL_MCP_URL! },
})

const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
mcp: {
clients: [primaryClient, optionalClient],
connection: 'close',
onDiscoveryError(error, source) {
// Log the failure but let the run proceed without this source's tools.
// Throw here (or re-throw `error`) to fail the whole run instead.
console.warn('MCP discovery failed for a source, skipping.', error)
},
},
})

onDiscoveryError에 전달된 소스는 검색이 실패하기 전에 이미 연결되었을 수 있습니다. connection: 'close'인 경우 해당 도구를 건너뛰었더라도 실행이 끝날 때 연결이 닫힙니다.

도구 이름 충돌

mcp.clients의 두 소스가 같은 이름의 도구를 노출하면 검색된 도구를 병합한 뒤 MCPDuplicateToolNameError(@tanstack/ai에서 export됨)가 발생하여 실행이 실패합니다. chat()은 지연 실행된다는 점에 유의합니다. 스트림을 처음 소비할 때 검색이 수행되므로 오류는 chat() 호출 위치에서 try/catch할 수 있는 동기 예외가 아니라 스트림을 통해(SSE 응답 오류로) 나타납니다. 충돌을 미리 방지하려면 클라이언트 중 하나에 prefix를 지정하거나 createMCPClients를 사용합니다(설정 키를 사용해 접두사를 자동으로 붙입니다).

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' }]

// Both servers expose a tool called "search". Without prefixes the run
// would fail with MCPDuplicateToolNameError. The prefix option resolves
// the clash.
const serverA = await createMCPClient({
transport: { type: 'http', url: process.env.SERVER_A_URL! },
prefix: 'alpha', // tools become "alpha_search", etc.
})

const serverB = await createMCPClient({
transport: { type: 'http', url: process.env.SERVER_B_URL! },
prefix: 'beta', // tools become "beta_search", etc.
})

const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
mcp: {
clients: [serverA, serverB],
connection: 'close',
},
})

독립적인 pool.tools()의 충돌 동작과 일반적인 prefix 전략은 도구 이름 충돌접두사 비활성화 또는 재정의를 참고합니다.

더 알아보기

실행에서 완전한 타입의 도구, 리소스 또는 프롬프트가 필요하신가요? mcp prop은 런타임 타입 도구와 검색을 제공합니다. toolDefinition 타입의 MCP 도구를 spread하거나, MCP 리소스와 프롬프트를 주입하거나, 진행 중인 MCP 호출을 취소하려면 수동 MCP: 타입이 지정된 도구, 리소스 및 프롬프트를 참고합니다.