도구

도구(tools)는 Model Context Protocol(MCP)에서 매우 중요한 기본 요소(primitive)로, 서버가 실행 가능한 기능을 클라이언트에 노출할 수 있게 합니다. 도구를 통해 LLM은 외부 시스템과 상호작용하고, 계산을 수행하며, 실제 세계에서 작업을 실행할 수 있습니다.

도구는 **모델 주도(model-controlled)** 사용을 전제로 설계되었습니다. 즉, 서버가 클라이언트에 도구를 노출할 때 AI 모델이 이를 자동으로 호출할 수 있도록(단, 사람의 승인(human-in-the-loop)으로 제어) 의도합니다.

개요

MCP의 도구는 서버가 실행 가능한 함수를 노출하도록 하며, 클라이언트가 이를 호출하고 LLM이 작업을 수행하는 데 사용할 수 있습니다. 주요 특징은 다음과 같습니다:

  • 탐색(Discovery): 클라이언트는 tools/list 엔드포인트로 사용 가능한 도구를 나열할 수 있습니다.
  • 호출(Invocation): tools/call 엔드포인트로 도구를 호출하면, 서버가 요청된 작업을 수행하고 결과를 반환합니다.
  • 유연성(Flexibility): 간단한 계산부터 복잡한 외부 API 호출까지 폭넓게 구현할 수 있습니다.

리소스(resources)처럼 도구도 고유한 이름으로 식별되며, 사용을 돕는 설명을 포함할 수 있습니다. 다만 리소스와 달리 도구는 상태를 변경하거나 외부 시스템과 상호작용할 수 있는 동적 작업을 나타냅니다.

도구 정의 구조

각 도구는 다음 구조로 정의됩니다:

{
  name: string;          // Unique identifier for the tool
  description?: string;  // Human-readable description
  inputSchema: {         // JSON Schema for the tool's parameters
    type: "object",
    properties: { ... }  // Tool-specific parameters
  }
}

도구 구현

MCP 서버에서 기본 도구를 구현하는 예시는 다음과 같습니다:

```typescript const server = new Server({ name: "example-server", version: "1.0.0" }, { capabilities: { tools: {} } });
// Define available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [{
      name: "calculate_sum",
      description: "Add two numbers together",
      inputSchema: {
        type: "object",
        properties: {
          a: { type: "number" },
          b: { type: "number" }
        },
        required: ["a", "b"]
      }
    }]
  };
});

// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "calculate_sum") {
    const { a, b } = request.params.arguments;
    return {
      toolResult: a + b
    };
  }
  throw new Error("Tool not found");
});
```
```python app = Server("example-server")
@app.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="calculate_sum",
            description="Add two numbers together",
            inputSchema={
                "type": "object",
                "properties": {
                    "a": {"type": "number"},
                    "b": {"type": "number"}
                },
                "required": ["a", "b"]
            }
        )
    ]

@app.call_tool()
async def call_tool(
    name: str,
    arguments: dict
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
    if name == "calculate_sum":
        a = arguments["a"]
        b = arguments["b"]
        result = a + b
        return [types.TextContent(type="text", text=str(result))]
    raise ValueError(f"Tool not found: {name}")
```

도구 패턴 예시

서버가 제공할 수 있는 도구 유형 예시는 다음과 같습니다:

시스템 작업

로컬 시스템과 상호작용하는 도구:

{
  name: "execute_command",
  description: "Run a shell command",
  inputSchema: {
    type: "object",
    properties: {
      command: { type: "string" },
      args: { type: "array", items: { type: "string" } }
    }
  }
}

API 통합

외부 API를 래핑하는 도구:

{
  name: "github_create_issue",
  description: "Create a GitHub issue",
  inputSchema: {
    type: "object",
    properties: {
      title: { type: "string" },
      body: { type: "string" },
      labels: { type: "array", items: { type: "string" } }
    }
  }
}

데이터 처리

데이터를 변환하거나 분석하는 도구:

{
  name: "analyze_csv",
  description: "Analyze a CSV file",
  inputSchema: {
    type: "object",
    properties: {
      filepath: { type: "string" },
      operations: {
        type: "array",
        items: {
          enum: ["sum", "average", "count"]
        }
      }
    }
  }
}

모범 사례

도구를 구현할 때는:

  1. 명확하고 설명적인 이름/설명을 제공합니다.
  2. 파라미터는 상세한 JSON Schema로 정의합니다.
  3. 모델이 어떻게 사용해야 하는지 알 수 있도록 설명에 예시를 포함합니다.
  4. 적절한 오류 처리와 입력 검증을 구현합니다.
  5. 시간이 오래 걸리는 작업은 progress 리포팅을 사용합니다.
  6. 도구 작업은 작고 원자적(atomic)으로 유지합니다.
  7. 기대하는 반환값 구조를 문서화합니다.
  8. 적절한 타임아웃을 구현합니다.
  9. 리소스 집약 작업은 레이트 리밋을 고려합니다.
  10. 디버깅/모니터링을 위해 도구 사용을 로깅합니다.

보안 고려사항

도구를 노출할 때는:

입력 검증

  • 모든 파라미터를 스키마에 따라 검증합니다.
  • 파일 경로와 시스템 커맨드를 정규화/무해화(sanitize)합니다.
  • URL과 외부 식별자를 검증합니다.
  • 파라미터 크기와 범위를 검사합니다.
  • 커맨드 인젝션을 방지합니다.

접근 제어

  • 필요 시 인증(authentication)을 구현합니다.
  • 적절한 인가(authorization) 검사를 적용합니다.
  • 도구 사용을 감사(audit)합니다.
  • 요청에 레이트 리밋을 적용합니다.
  • 남용 여부를 모니터링합니다.

오류 처리

  • 내부 오류를 클라이언트에 그대로 노출하지 않습니다.
  • 보안 관련 오류는 로깅합니다.
  • 타임아웃을 적절히 처리합니다.
  • 오류 발생 시 리소스를 정리(cleanup)합니다.
  • 반환값을 검증합니다.

도구 탐색 및 업데이트

MCP는 동적인 도구 탐색을 지원합니다:

  1. 클라이언트는 언제든지 사용 가능한 도구 목록을 조회할 수 있습니다.
  2. 서버는 notifications/tools/list_changed로 도구 변경을 클라이언트에 알릴 수 있습니다.
  3. 런타임 중에도 도구를 추가/제거할 수 있습니다.
  4. 도구 정의를 업데이트할 수 있지만(주의 필요), 호환성과 모델 동작에 영향을 줄 수 있습니다.

오류 처리

도구 오류는 MCP 프로토콜 레벨 오류로 올리는 대신, 결과(result) 객체 안에서 보고해야 합니다. 이렇게 하면 LLM이 오류를 인지하고, 수정 조치(또는 사람에게 도움 요청)를 할 수 있습니다. 도구에서 오류가 발생하면:

  1. 결과에 isError: true를 설정합니다.
  2. content 배열에 오류 세부 정보를 포함합니다.

도구의 올바른 오류 처리 예시는 다음과 같습니다:

```typescript try { // Tool operation const result = performOperation(); return { content: [ { type: "text", text: `Operation successful: ${result}` } ] }; } catch (error) { return { isError: true, content: [ { type: "text", text: `Error: ${error.message}` } ] }; } ``` ```python try: # Tool operation result = perform_operation() return types.CallToolResult( content=[ types.TextContent( type="text", text=f"Operation successful: {result}" ) ] ) except Exception as error: return types.CallToolResult( isError=True, content=[ types.TextContent( type="text", text=f"Error: {str(error)}" ) ] ) ```

이 접근은 LLM이 오류 발생 사실을 확인하고, 문제를 바로잡거나 사람의 개입을 요청할 수 있게 합니다.

도구 테스트

MCP 도구를 위한 종합 테스트 전략은 다음을 포함해야 합니다:

  • 기능 테스트: 유효 입력에서 올바르게 실행되는지, 잘못된 입력을 적절히 처리하는지 검증
  • 통합 테스트: 실제/목(mock) 의존성을 사용해 외부 시스템과의 상호작용을 검증
  • 보안 테스트: 인증/인가, 입력 sanitize, 레이트 리밋 검증
  • 성능 테스트: 부하 상황에서의 동작, 타임아웃 처리, 리소스 정리 확인
  • 오류 처리: MCP 프로토콜을 통해 오류를 올바르게 보고하고 리소스를 정리하는지 확인