커스텀 도구를 사용하면 인프로세스 MCP 서버를 통해 자체 기능으로 Claude Code의 기능을 확장할 수 있으며, Claude가 외부 서비스, API와 상호작용하거나 특수한 작업을 수행할 수 있게 해줍니다.

커스텀 도구 생성

createSdkMcpServertool 헬퍼 함수를 사용하여 타입 안전한 커스텀 도구를 정의하세요:
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-code";
import { z } from "zod";

// 커스텀 도구로 SDK MCP 서버 생성
const customServer = createSdkMcpServer({
  name: "my-custom-tools",
  version: "1.0.0",
  tools: [
    tool(
      "get_weather",
      "위치의 현재 날씨 가져오기",
      {
        location: z.string().describe("도시 이름 또는 좌표"),
        units: z.enum(["celsius", "fahrenheit"]).default("celsius").describe("온도 단위")
      },
      async (args) => {
        // 날씨 API 호출
        const response = await fetch(
          `https://api.weather.com/v1/current?q=${args.location}&units=${args.units}`
        );
        const data = await response.json();
        
        return {
          content: [{
            type: "text",
            text: `온도: ${data.temp}°\n날씨: ${data.conditions}\n습도: ${data.humidity}%`
          }]
        };
      }
    )
  ]
});

커스텀 도구 사용

mcpServers 옵션을 통해 커스텀 서버를 query 함수에 딕셔너리/객체로 전달하세요.
중요: 커스텀 MCP 도구는 스트리밍 입력 모드가 필요합니다. prompt 매개변수에 비동기 제너레이터/이터러블을 사용해야 하며, 단순한 문자열은 MCP 서버와 함께 작동하지 않습니다.

도구 이름 형식

MCP 도구가 Claude에 노출될 때, 이름은 특정 형식을 따릅니다:
  • 패턴: mcp__{server_name}__{tool_name}
  • 예시: my-custom-tools 서버의 get_weather 도구는 mcp__my-custom-tools__get_weather가 됩니다

허용된 도구 구성

allowedTools 옵션을 통해 Claude가 사용할 수 있는 도구를 제어할 수 있습니다:
import { query } from "@anthropic-ai/claude-code";

// 스트리밍 입력으로 커스텀 도구 사용
async function* generateMessages() {
  yield {
    type: "user" as const,
    message: {
      role: "user" as const,
      content: "샌프란시스코의 날씨는 어때?"
    }
  };
}

for await (const message of query({
  prompt: generateMessages(),  // 스트리밍 입력을 위한 비동기 제너레이터 사용
  options: {
    mcpServers: {
      "my-custom-tools": customServer  // 배열이 아닌 객체/딕셔너리로 전달
    },
    // 선택적으로 Claude가 사용할 수 있는 도구 지정
    allowedTools: [
      "mcp__my-custom-tools__get_weather",  // 날씨 도구 허용
      // 필요에 따라 다른 도구 추가
    ],
    maxTurns: 3
  }
})) {
  if (message.type === "result" && message.subtype === "success") {
    console.log(message.result);
  }
}

다중 도구 예시

MCP 서버에 여러 도구가 있을 때, 선택적으로 허용할 수 있습니다:
const multiToolServer = createSdkMcpServer({
  name: "utilities",
  version: "1.0.0",
  tools: [
    tool("calculate", "계산 수행", { /* ... */ }, async (args) => { /* ... */ }),
    tool("translate", "텍스트 번역", { /* ... */ }, async (args) => { /* ... */ }),
    tool("search_web", "웹 검색", { /* ... */ }, async (args) => { /* ... */ })
  ]
});

// 스트리밍 입력으로 특정 도구만 허용
async function* generateMessages() {
  yield {
    type: "user" as const,
    message: {
      role: "user" as const,
      content: "5 + 3을 계산하고 'hello'를 스페인어로 번역해줘"
    }
  };
}

for await (const message of query({
  prompt: generateMessages(),  // 스트리밍 입력을 위한 비동기 제너레이터 사용
  options: {
    mcpServers: {
      utilities: multiToolServer
    },
    allowedTools: [
      "mcp__utilities__calculate",   // 계산기 허용
      "mcp__utilities__translate",   // 번역기 허용
      // "mcp__utilities__search_web"는 허용되지 않음
    ]
  }
})) {
  // 메시지 처리
}

Python에서의 타입 안전성

@tool 데코레이터는 타입 안전성을 위한 다양한 스키마 정의 방법을 지원합니다:
import { z } from "zod";

tool(
  "process_data",
  "타입 안전성을 갖춘 구조화된 데이터 처리",
  {
    // Zod 스키마는 런타임 검증과 TypeScript 타입을 모두 정의
    data: z.object({
      name: z.string(),
      age: z.number().min(0).max(150),
      email: z.string().email(),
      preferences: z.array(z.string()).optional()
    }),
    format: z.enum(["json", "csv", "xml"]).default("json")
  },
  async (args) => {
    // args는 스키마를 기반으로 완전히 타입이 지정됨
    // TypeScript는 args.data.name이 string, args.data.age가 number임을 알고 있음
    console.log(`${args.data.name}의 데이터를 ${args.format}으로 처리 중`);
    
    // 처리 로직
    return {
      content: [{
        type: "text",
        text: `${args.data.name}의 데이터 처리 완료`
      }]
    };
  }
)

오류 처리

의미 있는 피드백을 제공하기 위해 오류를 우아하게 처리하세요:
tool(
  "fetch_data",
  "API에서 데이터 가져오기",
  {
    endpoint: z.string().url().describe("API 엔드포인트 URL")
  },
  async (args) => {
    try {
      const response = await fetch(args.endpoint);
      
      if (!response.ok) {
        return {
          content: [{
            type: "text",
            text: `API 오류: ${response.status} ${response.statusText}`
          }]
        };
      }
      
      const data = await response.json();
      return {
        content: [{
          type: "text",
          text: JSON.stringify(data, null, 2)
        }]
      };
    } catch (error) {
      return {
        content: [{
          type: "text",
          text: `데이터 가져오기 실패: ${error.message}`
        }]
      };
    }
  }
)

예시 도구

데이터베이스 쿼리 도구

const databaseServer = createSdkMcpServer({
  name: "database-tools",
  version: "1.0.0",
  tools: [
    tool(
      "query_database",
      "데이터베이스 쿼리 실행",
      {
        query: z.string().describe("실행할 SQL 쿼리"),
        params: z.array(z.any()).optional().describe("쿼리 매개변수")
      },
      async (args) => {
        const results = await db.query(args.query, args.params || []);
        return {
          content: [{
            type: "text",
            text: `${results.length}개 행 발견:\n${JSON.stringify(results, null, 2)}`
          }]
        };
      }
    )
  ]
});

API 게이트웨이 도구

const apiGatewayServer = createSdkMcpServer({
  name: "api-gateway",
  version: "1.0.0",
  tools: [
    tool(
      "api_request",
      "외부 서비스에 인증된 API 요청 수행",
      {
        service: z.enum(["stripe", "github", "openai", "slack"]).describe("호출할 서비스"),
        endpoint: z.string().describe("API 엔드포인트 경로"),
        method: z.enum(["GET", "POST", "PUT", "DELETE"]).describe("HTTP 메서드"),
        body: z.record(z.any()).optional().describe("요청 본문"),
        query: z.record(z.string()).optional().describe("쿼리 매개변수")
      },
      async (args) => {
        const config = {
          stripe: { baseUrl: "https://api.stripe.com/v1", key: process.env.STRIPE_KEY },
          github: { baseUrl: "https://api.github.com", key: process.env.GITHUB_TOKEN },
          openai: { baseUrl: "https://api.openai.com/v1", key: process.env.OPENAI_KEY },
          slack: { baseUrl: "https://slack.com/api", key: process.env.SLACK_TOKEN }
        };
        
        const { baseUrl, key } = config[args.service];
        const url = new URL(`${baseUrl}${args.endpoint}`);
        
        if (args.query) {
          Object.entries(args.query).forEach(([k, v]) => url.searchParams.set(k, v));
        }
        
        const response = await fetch(url, {
          method: args.method,
          headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
          body: args.body ? JSON.stringify(args.body) : undefined
        });
        
        const data = await response.json();
        return {
          content: [{
            type: "text",
            text: JSON.stringify(data, null, 2)
          }]
        };
      }
    )
  ]
});

계산기 도구

const calculatorServer = createSdkMcpServer({
  name: "calculator",
  version: "1.0.0",
  tools: [
    tool(
      "calculate",
      "수학적 계산 수행",
      {
        expression: z.string().describe("평가할 수학 표현식"),
        precision: z.number().optional().default(2).describe("소수점 정밀도")
      },
      async (args) => {
        try {
          // 프로덕션에서는 안전한 수학 평가 라이브러리 사용
          const result = eval(args.expression); // 예시 전용!
          const formatted = Number(result).toFixed(args.precision);
          
          return {
            content: [{
              type: "text",
              text: `${args.expression} = ${formatted}`
            }]
          };
        } catch (error) {
          return {
            content: [{
              type: "text",
              text: `오류: 잘못된 표현식 - ${error.message}`
            }]
          };
        }
      }
    ),
    tool(
      "compound_interest",
      "투자의 복리 이자 계산",
      {
        principal: z.number().positive().describe("초기 투자 금액"),
        rate: z.number().describe("연간 이자율 (소수로, 예: 5%는 0.05)"),
        time: z.number().positive().describe("투자 기간(년)"),
        n: z.number().positive().default(12).describe("연간 복리 빈도")
      },
      async (args) => {
        const amount = args.principal * Math.pow(1 + args.rate / args.n, args.n * args.time);
        const interest = amount - args.principal;
        
        return {
          content: [{
            type: "text",
            text: `투자 분석:\n` +
                  `원금: $${args.principal.toFixed(2)}\n` +
                  `이율: ${(args.rate * 100).toFixed(2)}%\n` +
                  `기간: ${args.time}\n` +
                  `복리: 연 ${args.n}\n\n` +
                  `최종 금액: $${amount.toFixed(2)}\n` +
                  `이자 수익: $${interest.toFixed(2)}\n` +
                  `수익률: ${((interest / args.principal) * 100).toFixed(2)}%`
          }]
        };
      }
    )
  ]
});

관련 문서