도구

도구 (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의 descriptionSubstring to match against product names 가 담기는 것이죠 — 그것이 모델이 그 인자에 대해 얻는 유일한 문서입니다. :::

v1에서 오셨나요?

registerTooltool() 을 대체해요 — 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는 결과가 서버를 떠나기 전에 structuredContentoutputSchema 로 검증하고, 파생된 JSON Schema를 tools/list 에서 광고해서 클라이언트도 검증할 수 있게 해 줍니다.

product-details{ name: 'Travel mug' } 로 호출하면 두 렌더링이 모두 돌아옵니다.

{
  content: [ { type: 'text', text: '{"name":"Travel mug","price":24}' } ],
  structuredContent: { name: 'Travel mug', price: 24 }
}

구조화 결과의 와이어 인코딩은 프로토콜 시대에 따라 달라요 — 프로토콜 버전 을 보세요.

다른 콘텐츠 타입 반환하기

하나의 결과는 콘텐츠 블록을 섞어 낼 수 있어요. imageaudiomimeType 과 함께 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}'
    }
  }
]

블록들은 반환된 그대로 클라이언트에 도달하고, 내장된 resourceresources/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 도구 결과로 돌아오고, 핸들러는 실행되지 않아요.
  • outputSchemastructuredContent 가 기계가 읽을 결과를 더하고, 서버를 떠나기 전에 검증됩니다.
  • content 블록은 text, image, audio, resource_link, 또는 내장 resource 이고, 한 결과가 그것을 섞을 수 있어요.
  • titleannotations 은 도구를 클라이언트에 설명할 뿐 실행은 바꾸지 않습니다.