수동 MCP: 타입이 지정된 도구, 리소스 및 프롬프트
실행 중인 MCP 클라이언트가 있고 도구를 자동 검색하는 것 이상을 수행하려면, 타입이 완전히 지정된 도구를 chat() 실행에 펼쳐 넣고 서버의 리소스와 프롬프트를 대화에 주입하며 실행이 중단될 때 진행 중인 MCP 호출을 취소할 수 있습니다. 이 가이드를 마치면 이 모든 기능을 하나의 chat() 호출에 연결하게 됩니다.
수동(
tools펼치기)과 관리형(mcpprop) 비교이 페이지에서는 수동 경로를 다룹니다. 즉, 직접
client.tools()/client.resources()/client.getPrompt()를 호출하고close()를 직접 관리합니다. 검색과 수명 주기 관리를 자동으로 처리하는 런타임 타입 도구만 필요하다면 대신mcpprop을 사용하세요. 관리형 MCP와chat()을 참고하세요. 두 경로 모두createMCPClient기초를 기반으로 합니다.
tools 펼치기를 통한 완전한 타입 지정 도구
client.tools([...])에 toolDefinition() 인스턴스를 전달하면 Zod로 검증되고 TypeScript 타입이 지정된 인수를 얻을 수 있습니다(Mode 2). 그런 다음 결과를 chat()의 tools 옵션에 펼쳐 넣습니다. 클라이언트를 직접 관리하므로 닫아야 하지만, 스트림이 소비되기 전에는 닫으면 안 됩니다. chat()은 응답을 스트리밍하는 동안 도구를 지연 실행하므로 return을 둘러싼 finally에서 닫으면 진행 중인 도구 호출이 종료됩니다. 대신 미들웨어의 종료 훅에서 닫으세요(실행마다 onFinish/onAbort/onError 중 정확히 하나가 호출됩니다).
// src/routes/api.chat.ts
import { createFileRoute } from '@tanstack/react-router'
import { chat, toServerSentEventsResponse, toolDefinition } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
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() })),
})
export const Route = createFileRoute('/api/chat')({
server: {
handlers: {
POST: async ({ request }) => {
const { messages } = await request.json()
const mcp = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
})
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
// Fully-typed MCP tools, merged with any other tools you pass
tools: [...(await mcp.tools([searchDef]))],
// Close after the run ends — tools execute while the response streams.
middleware: [
{
name: 'mcp-close',
onFinish: () => mcp.close(),
onAbort: () => mcp.close(),
onError: () => mcp.close(),
},
],
})
return toServerSentEventsResponse(stream)
},
},
},
})
리소스
MCP 리소스는 서버가 제공하는 컨텍스트 문서(파일, 데이터베이스 레코드, 웹 페이지)입니다. 이를 가져와 콘텐츠 파트로 chat()에 주입합니다.
import { chat } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient, mcpResourceToContentPart } from '@tanstack/ai-mcp'
const mcp = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
})
const resources = await mcp.resources()
// resources: Array<{ uri: string; name: string; ... }>
const readResult = await mcp.readResource(resources[0]!.uri)
const parts = readResult.contents.map(mcpResourceToContentPart)
// Inject as part of a user message
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages: [
{
role: 'user',
content: [
...parts,
{ type: 'text', content: 'Summarize the above document.' },
],
},
],
})
mcpResourceToContentPart는 각 MCP 콘텐츠 블록을 ContentPart로 매핑합니다.
text필드가 있으면 →{ type: 'text', content: text }blob필드가 있으면 →{ type: 'text', content: '[binary resource <uri>]' }- 그 외에는 →
{ type: 'text', content: JSON.stringify(content) }
리소스 템플릿
import { createMCPClient } from '@tanstack/ai-mcp'
const mcp = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
})
const templates = await mcp.resourceTemplates()
// templates: Array<ResourceTemplate>
프롬프트
MCP 프롬프트는 서버가 제공하는 재사용 가능한 메시지 템플릿입니다. 프롬프트를 가져와 mcpPromptToMessages로 ModelMessage[]로 변환한 다음 chat()에 펼쳐 넣어 서버에서 정의한 컨텍스트나 지침으로 대화를 초기화합니다.
import { createFileRoute } from '@tanstack/react-router'
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { createMCPClient, mcpPromptToMessages } 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: process.env.MCP_URL! },
})
try {
// List all available prompts on the server
const available = await mcp.prompts()
// available: Array<{ name: string; description?: string; arguments?: ... }>
// Fetch a specific prompt, optionally passing template arguments
const prompt = await mcp.getPrompt('summarize', { language: 'english' })
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages: [
// Seed the conversation with the server-defined prompt messages
...mcpPromptToMessages(prompt),
// Then append the user's own messages
...messages,
],
})
return toServerSentEventsResponse(stream)
} finally {
// Safe here: all MCP calls (prompts/getPrompt) completed before chat()
// started, and no MCP tools are passed to the run. If you also spread
// MCP tools into `tools`, close in a middleware terminal hook instead
// (see "Fully-typed tools via the `tools` spread" above).
await mcp.close()
}
},
},
},
})
mcpPromptToMessages는 각 MCP 프롬프트 메시지를 ModelMessage로 매핑합니다.
role === 'assistant'→{ role: 'assistant', content: text }- 그 외의 모든 역할 →
{ role: 'user', content: text } - 텍스트가 아닌 콘텐츠 →
content는JSON.stringify됩니다
getPrompt(name, args?)는 프롬프트에 선언된 템플릿 변수를 채우기 위한 선택적 args 매개변수를 Record<string, string> 타입으로 받습니다.
취소
채팅 실행이 취소되면(예: 사용자가 다른 페이지로 이동하거나 AbortController가 실행되는 경우) 진행 중인 MCP callTool 요청이 자동으로 취소됩니다. 채팅 실행의 중단 신호는 ToolExecutionContext.abortSignal을 통해 각 도구의 execute 함수로 전달됩니다.
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 mcp = await createMCPClient({
transport: { type: 'http', url: process.env.MCP_URL! },
})
const controller = new AbortController()
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
tools: await mcp.tools(),
abortController: controller,
})
// Cancel the run and all in-flight MCP tool calls:
controller.abort()
전체 서버 및 클라이언트 예시
다음은 두 MCP 서버에 연결하고 응답을 브라우저로 스트리밍하는 완전한 TanStack Start API 라우트입니다.
서버 라우트(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'
export const Route = createFileRoute('/api/chat')({
server: {
handlers: {
POST: async ({ request }) => {
const body = await request.json()
if (typeof body !== 'object' || body === null || !Array.isArray(body.messages)) {
return new Response('Bad request', { status: 400 })
}
const pool = 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}` },
},
},
})
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages: body.messages,
tools: await pool.tools(),
// Close after the run ends — tools execute while the response streams.
middleware: [
{
name: 'mcp-close',
onFinish: () => pool.close(),
onAbort: () => pool.close(),
onError: () => pool.close(),
},
],
})
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>
)
}
더 알아보기
chat()이 도구를 검색하고 클라이언트를 닫도록 하시겠습니까? 수동tools펼치기, 리소스 또는 프롬프트가 필요하지 않다면mcpprop을 사용해 close 미들웨어 보일러플레이트를 완전히 제거할 수 있습니다. 관리형 MCP와chat()을 참고하세요.
검색 경로에서 도구 이름을 컴파일 시점에 검사하시겠습니까? 실행 중인 서버에서 서버별 인터페이스 타입을 생성하고 이를
createMCPClient의 제네릭으로 전달하세요. 검색된 도구 이름이 서버의 리터럴 이름으로 좁혀지며 런타임 오버헤드가 없습니다. MCP 타입 생성을 참고하세요.