MCP(Model Context Protocol) 서버를 하나 만들어 뒀는데, 이걸 Claude Desktop이나 Cursor 같은 클라이언트에서만 쓰다 보니 아쉬웠다. 내가 만든 Next.js 앱 안에서도 그 툴들을 그대로 LLM한테 물려주고 싶었다.
tools에 함수를 하나하나 정의하는 방식은 이미 써봤는데, 툴이 늘어날수록 스키마를 앱 코드에 중복으로 들고 있어야 하는 게 불편했다. MCP 서버가 이미 툴 목록과 스키마를 알고 있으니, 그걸 그대로 가져오면 되지 않나 싶었다. Vercel AI SDK에 experimental_createMCPClient가 있어서 이게 가능하다.
클라이언트 붙이기
MCP 서버에 연결하고, 서버가 노출하는 툴을 통째로 받아온다.
import { experimental_createMCPClient as createMCPClient } from 'ai';
import { Experimental_StdioMCPTransport as StdioTransport } from 'ai/mcp-stdio';
const mcpClient = await createMCPClient({
transport: new StdioTransport({
command: 'node',
args: ['./mcp-server/index.js'],
}),
});
const tools = await mcpClient.tools();
tools()가 돌려주는 객체는 AI SDK의 tools 형식 그대로다. 서버가 툴 5개를 노출하면 5개가 다 담겨 온다. 스키마를 앱에서 다시 적을 필요가 없다는 게 핵심이다.
generateText에 물리기
받아온 툴을 그대로 넘긴다.
import { anthropic } from '@ai-sdk/anthropic';
import { generateText, stepCountIs } from 'ai';
const { text } = await generateText({
model: anthropic('claude-sonnet-5'),
tools,
stopWhen: stepCountIs(5),
prompt: '지난주 배포 로그에서 실패한 것만 요약해줘',
});
await mcpClient.close();
stopWhen을 안 걸면 툴을 한 번 부르고 멈춘다. 툴 결과를 받아 다시 추론하게 하려면 stepCountIs로 스텝을 열어줘야 한다. 여기서 처음에 이걸 빠뜨려서, LLM이 툴만 호출하고 답을 안 내놓길래 한참 헤맸다.
SSE 트랜스포트
로컬 프로세스가 아니라 원격 MCP 서버라면 stdio 대신 SSE를 쓴다.
const mcpClient = await createMCPClient({
transport: { type: 'sse', url: 'https://my-mcp.example.com/sse' },
});
트랜스포트만 바꾸면 나머지 코드는 똑같다.
마무리
한 가지 주의할 점은 close()를 꼭 불러야 한다는 거다. 특히 stdio 트랜스포트는 자식 프로세스를 띄우기 때문에, 안 닫으면 서버리스 함수가 끝나도 프로세스가 남는다. try/finally로 감싸는 게 안전하다.
이름에 experimental_이 붙어 있는 만큼 API가 바뀔 수 있으니 프로덕션에 넣을 땐 버전을 고정해 두는 게 좋다. 그래도 MCP 서버 한 번 만들어 두면 클라이언트든 내 앱이든 같은 툴을 공유할 수 있다는 게 꽤 매력적이었다.