도구
도구 (Tools)
도구(tool) 는 연결된 클라이언트 — 그리고 그것을 구동하는 모델 — 이 여러분의 서버에서 호출할 수 있는 액션이에요.
도구 추가하기
registerTool 은 이름, 설정, 핸들러를 받아요. inputSchema 는 Zod 스키마이고, 여러분이 직접 쓰는 유일한 스키마예요.
import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const catalog = [
{ name: 'Espresso cup', price: 12 },
{ name: 'Travel mug', price: 24 },
{ name: 'Mug rack', price: 36 }
];
const server = new McpServer({ name: 'catalog', version: '1.0.0' });
server.registerTool(
'search',
{
description: 'Search the product catalog',
inputSchema: z.object({
query: z.string().describe('Substring to match against product names'),
limit: z.number().int().max(50).optional()
})
},
async ({ query, limit }) => {
const hits = catalog.filter(product => product.name.toLowerCase().includes(query.toLowerCase()));
const names = hits.slice(0, limit ?? 10).map(product => product.name);
return { content: [{ type: 'text', text: names.join('\n') }] };
}
);
SDK는 이 하나의 스키마에서 모델이 보게 될 JSON Schema를 만들고, 핸들러가 실행되기 전에 인자를 검증하며, 핸들러의 인자 타입을 추론합니다.
tools/list 가 이제 search 를 광고하고, SDK는 핸들러에 도달하는 모든 호출을 이미 파싱해 두었어요.
::: tip
.describe() 는 변환을 살아남아요. query 에 대해 광고되는 JSON Schema의 description 에 Substring to match against product names 가 담기는 것이죠 — 그것이 모델이 그 인자에 대해 얻는 유일한 문서입니다.
:::
v1에서 오셨나요?
registerTool이tool()을 대체해요 — codemod 를 실행한 뒤 업그레이드 가이드를 보세요.
호출하기
이 페이지의 모든 호출은 위 서버에 연결된 메모리 안의 Client 에서 와요. 서버 테스트하기 가 그 연결을 보여 주고, MCP 호스트도 stdio 또는 HTTP 위에서 똑같이 합니다. 유효한 인자로 도구를 호출해 볼게요.
const result = await client.callTool({ name: 'search', arguments: { query: 'mug' } });
console.log(result.content);
핸들러의 content 가 그대로 돌아옵니다.
[ { type: 'text', text: 'Travel mug\nMug rack' } ]
스키마가 거부하는 인자 보내기
인자 하나를 바꿔 볼게요 — 스키마가 50으로 제한하는 limit 를 넘깁니다.
const rejected = await client.callTool({ name: 'search', arguments: { query: 'mug', limit: 999 } });
console.log(rejected);
SDK는 핸들러가 실행되기 전에 인자를 거부해요.
{
content: [
{
type: 'text',
text: 'Input validation error: Invalid arguments for tool search: limit: Too big: expected number to be <=50'
}
],
isError: true
}
이 거부는 isError: true 가 붙은 평범한 도구 결과예요. 그래서 모델은 메시지를 읽고 스키마에 맞는 인자로 다시 시도합니다. 던져진 예외와 프로토콜 수준 실패는 별개 주제라 오류 에서 다룹니다.
구조화된 출력 반환하기
outputSchema 를 추가하고, 사람이 읽을 content 옆에 그에 맞는 값을 structuredContent 로 돌려줍니다.
server.registerTool(
'product-details',
{
description: 'Look up one product by its exact name',
inputSchema: z.object({ name: z.string() }),
outputSchema: z.object({ name: z.string(), price: z.number() })
},
async ({ name }) => {
const product = catalog.find(candidate => candidate.name === name);
if (!product) throw new Error(`No product named ${name}`);
const output = { name: product.name, price: product.price };
return {
content: [{ type: 'text', text: JSON.stringify(output) }],
structuredContent: output
};
}
);
SDK는 결과가 서버를 떠나기 전에 structuredContent 를 outputSchema 로 검증하고, 파생된 JSON Schema를 tools/list 에서 광고해서 클라이언트도 검증할 수 있게 해 줍니다.
product-details 를 { name: 'Travel mug' } 로 호출하면 두 렌더링이 모두 돌아옵니다.
{
content: [ { type: 'text', text: '{"name":"Travel mug","price":24}' } ],
structuredContent: { name: 'Travel mug', price: 24 }
}
구조화 결과의 와이어 인코딩은 프로토콜 시대에 따라 달라요 — 프로토콜 버전 을 보세요.
다른 콘텐츠 타입 반환하기
하나의 결과는 콘텐츠 블록을 섞어 낼 수 있어요. image 와 audio 는 mimeType 과 함께 base64 data 를 싣고, resource 는 리소스의 내용을 인라인으로 담으며, resource_link 는 바이트 없이 uri 로 리소스를 가리킵니다.
// Base64 payloads; read yours from disk: readFileSync('card.png').toString('base64')
const cardPng = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
const spokenNameWav = 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=';
server.registerTool(
'product-card',
{
description: 'Render one product as an image, a spoken name, and its catalog record',
inputSchema: z.object({ name: z.string() })
},
async ({ name }) => {
const product = catalog.find(candidate => candidate.name === name);
if (!product) throw new Error(`No product named ${name}`);
return {
content: [
{ type: 'image', data: cardPng, mimeType: 'image/png' },
{ type: 'audio', data: spokenNameWav, mimeType: 'audio/wav' },
{
type: 'resource',
resource: {
uri: `catalog://products/${encodeURIComponent(product.name)}`,
mimeType: 'application/json',
text: JSON.stringify(product)
}
}
]
};
}
);
product-card 를 { name: 'Travel mug' } 로 호출하면 세 블록이 그대로 돌아옵니다.
[
{
type: 'image',
data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=',
mimeType: 'image/png'
},
{
type: 'audio',
data: 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=',
mimeType: 'audio/wav'
},
{
type: 'resource',
resource: {
uri: 'catalog://products/Travel%20mug',
mimeType: 'application/json',
text: '{"name":"Travel mug","price":24}'
}
}
]
블록들은 반환된 그대로 클라이언트에 도달하고, 내장된 resource 는 resources/read 왕복 없이 도착해요.
도구에 주석 달기
title 은 표시 이름이고, annotations 는 클라이언트를 위한 동작 힌트예요.
server.registerTool(
'clear-catalog',
{
title: 'Clear the catalog',
description: 'Remove every product from the catalog',
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true }
},
async () => {
catalog.length = 0;
return { content: [{ type: 'text', text: 'Catalog cleared' }] };
}
);
인자를 받지 않는 도구는 inputSchema 를 생략해요. 주석은 SDK가 도구를 실행하는 방식을 결코 바꾸지 않습니다 — 클라이언트가 최종 사용자 앞에 무엇을 놓을지 정할 때 쓸 뿐이죠. 호스트는 읽기 전용 도구는 자동 승인하고, 파괴적인 도구는 확인을 요구할 수 있어요.
요약
registerTool(name, config, handler)로 도구를 등록하고,inputSchema는 Zod 객체 스키마예요.- 그 하나의 스키마가 광고되는 JSON Schema, 인자 검증, 핸들러 인자 타입을 모두 만들어 냅니다.
- 스키마에 어긋나는 인자는
isError: true도구 결과로 돌아오고, 핸들러는 실행되지 않아요. outputSchema와structuredContent가 기계가 읽을 결과를 더하고, 서버를 떠나기 전에 검증됩니다.content블록은text,image,audio,resource_link, 또는 내장resource이고, 한 결과가 그것을 섞을 수 있어요.title과annotations은 도구를 클라이언트에 설명할 뿐 실행은 바꾸지 않습니다.