10. Tools — 모델이 호출하는 행동

🎯 이 장의 목표
  • Tool이 "모델 제어" primitive라는 의미를 이해한다
  • 도구 정의(name·description·inputSchema·outputSchema·annotations)와 호출 결과(content·isError·structuredContent)를 읽고 쓸 수 있다
  • 프로토콜 에러와 도구 실행 에러(isError)의 차이를 정확히 구분한다

세 primitive의 큰 그림

MCP 서버가 노출하는 일급 능력은 셋입니다. 셋을 가르는 기준은 단 하나 — 누가 호출을 통제하는가(제어 모델) 입니다.

flowchart TD
    SERVER["MCP Server가 선언"] --> T["Tools\n행동 / 실행"]
    SERVER --> R["Resources\n읽기 전용 데이터"]
    SERVER --> P["Prompts\n재사용 템플릿"]
    T --> TC["모델 제어\n(LLM이 알아서 호출)"]
    R --> RC["애플리케이션 제어\n(호스트가 컨텍스트로 주입)"]
    P --> PC["사용자 제어\n(사용자가 명시적으로 선택)"]
    classDef s fill:#5eead4,stroke:#0f766e,color:#000;
    classDef ctrl fill:#fde68a,stroke:#b45309,color:#000;
    class T,R,P s;
    class TC,RC,PC ctrl;
primitive제어 모델비유주요 메서드
Tools모델 제어모델이 누르는 버튼tools/list, tools/call
Resources애플리케이션 제어호스트가 펼쳐 보여주는 자료resources/list, resources/read
Prompts사용자 제어사용자가 고르는 메뉴prompts/list, prompts/get

이 장은 Tools, 다음 두 장은 Resources와 Prompts입니다.

Tool이란 — 모델이 누르는 버튼

MCP는 서버가 언어 모델이 호출할 수 있는 도구를 노출하게 합니다. 도구는 모델이 데이터베이스 질의, API 호출, 계산 같은 외부 시스템 상호작용을 하게 해줍니다. 각 도구는 이름으로 유일하게 식별되고, 스키마를 기술하는 메타데이터를 포함합니다.

핵심은 모델 제어(model-controlled) 입니다. 도구는 모델이 맥락과 사용자의 요청을 이해해 자동으로 발견하고 호출하도록 설계됐습니다. 그래서 도구는 "모델이 상황을 보고 스스로 누르는 버튼"에 가깝습니다.

🔒 보안 — 사람이 고리에 있어야: 신뢰·안전을 위해, 도구 호출을 거부할 수 있는 사람(human-in-the-loop) 이 항상 있어야 합니다(스펙의 SHOULD). 호스트 앱은 어떤 도구가 모델에 노출됐는지 분명히 보여주는 UI를 제공하고, 서버를 호출하기 전에 도구 입력을 사용자에게 보여줘 악의적·우발적 데이터 유출을 막아야 합니다.

도구 정의 — tools/list의 응답

클라이언트는 tools/list로 도구 목록을 발견합니다. 서버는 도구 정의 배열로 응답합니다(도구가 수백 개면 커서 기반 페이지네이션 지원). 페이지네이션은 긴 목록을 한 번에 다 주지 않고 몇 개씩 나눠 주는 방식이고, 커서는 "다음에 이어서 줄 위치"를 가리키는 표식입니다 — 책갈피처럼 다음 페이지를 받을 때 그 표식을 제시합니다.

JSON
{
  "name": "get_weather_data",
  "title": "Weather Data Retriever",
  "description": "지정한 위치의 현재 날씨 데이터를 가져온다",
  "inputSchema": {
    "type": "object",
    "properties": {
      "location": { "type": "string", "description": "도시명 또는 우편번호" }
    },
    "required": ["location"]
  },
  "outputSchema": {
    "type": "object",
    "properties": {
      "temperature": { "type": "number", "description": "섭씨 온도" },
      "conditions": { "type": "string", "description": "날씨 상태" }
    }
  }
}
필드의미
name호출에 쓰는 유일 식별자
title(선택) 사람이 읽는 표시 이름
description모델이 "언제 이 도구를 쓸지" 판단하는 근거. 품질이 곧 사용성
inputSchema입력 인자의 JSON Schema. 모델이 인자를 만들 때 따름
outputSchema(선택) 출력 구조의 JSON Schema
annotations(선택) 도구 동작에 대한 힌트 (읽기 전용 여부 등)
💡 팁
JSON Schema란?inputSchema·outputSchema에 쓰인 형식이 JSON Schema입니다. "이 JSON은 이런 모양이어야 한다"를 그 자체로 JSON으로 기술하는 표준 규격입니다. 예컨대 {"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}는 "객체여야 하고, 문자열 location 필드가 필수"라는 뜻입니다. 모델은 이 스키마를 보고 올바른 인자를 만들고, 서버는 들어온 인자가 스키마에 맞는지 검증합니다. 데이터의 "양식 서식"이라고 보면 됩니다.
💡 팁
description은 모델을 향한 UX다: 사람이 보는 버튼 라벨처럼, description모델이 읽는 라벨입니다. "Get weather"보다 "도시명이나 우편번호로 현재 기온·강수·바람을 조회한다. 예보가 아니라 현재값"처럼 구체적일수록 모델이 정확히 호출합니다. 5부에서 FastMCP는 함수의 독스트링을 description으로 가져갑니다.
🔒 annotations는 신뢰 불가: 도구 annotations는 동작 힌트(예: "읽기 전용", "파괴적")일 뿐, 신뢰·안전 관점에서 클라이언트는 신뢰할 수 있는 서버에서 온 것이 아닌 한 annotations를 신뢰하지 말아야 합니다(스펙 MUST). 즉 "읽기 전용이라고 적혀 있으니 안전하다"고 가정하면 안 됩니다.

도구 호출 — tools/call과 결과

JSON
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "get_weather_data",
    "arguments": { "location": "Seoul" }
  }
}

성공 응답은 content 배열(텍스트·이미지 등)과 isError: false를 담습니다:

JSON
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [
      { "type": "text", "text": "현재 서울 날씨:\n기온: 23°C\n상태: 구름 조금" }
    ],
    "isError": false
  }
}

content의 각 항목은 type(text·image·audio·resource 등)을 가집니다. 도구는 출력에 리소스 링크나 임베디드 리소스를 담을 수도 있어, 모델이 다음 상호작용에 쓸 추가 컨텍스트를 줄 수 있습니다.

구조화된 출력 — outputSchema + structuredContent

2025-06 스펙부터 구조화된 콘텐츠가 도입됐습니다. 결과에 텍스트 content와 나란히 structuredContent(JSON 객체)를 담아, 모델이 텍스트를 파싱하지 않고 프로그램적으로 소비하게 합니다.

규칙이 까다로우니 정확히 짚습니다:

  • 도구 정의에 outputSchema있으면, 결과는 그 스키마를 따르는 structuredContent를 반환해야 합니다(서버 MUST). 클라이언트는 스키마로 검증해야 합니다(SHOULD).
  • 하위 호환: 구조화된 콘텐츠를 주는 도구는 직렬화된 JSON을 content의 TextContent에도 함께 넣는 게 좋습니다. 많은 클라이언트(특히 LLM 기반)는 여전히 텍스트 content만 보기 때문입니다.
JSON
{
  "result": {
    "content": [
      { "type": "text", "text": "{\"temperature\":23,\"conditions\":\"구름 조금\"}" }
    ],
    "structuredContent": { "temperature": 23, "conditions": "구름 조금" },
    "isError": false
  }
}
⚠️ 흔한 실수
흔한 함정: outputSchema를 선언했는데 structuredContent를 안 주거나(또는 null로 주면), 일부 SDK·클라이언트가 "구조화된 콘텐츠가 없다"며 호출을 에러로 처리합니다. 실제로 한 도구가 outputSchema를 광고하면서 structuredContent: null을 반환해 매 호출이 실패한 버그 사례가 있습니다. outputSchema를 선언했으면 반드시 그에 맞는 structuredContent를 반환하세요. 확신이 없으면 outputSchema를 빼는 것도 방법입니다.

프로토콜 에러 vs 도구 실행 에러 (가장 중요한 구분)

04장에서 예고한 두 층위를 여기서 확정합니다.

flowchart TD
    CALL["tools/call 도착"] --> Q{"무엇이 잘못됐나?"}
    Q -->|"없는 도구명·잘못된 인자·서버 크래시"| PROTO["JSON-RPC error 객체\n(code: -32602 등)"]
    Q -->|"도구는 실행됐으나 결과 실패\n(API 404, 입력 부적합, 비즈니스 규칙)"| EXEC["정상 result +\nisError: true + content에 설명"]
    classDef q fill:#bfdbfe,stroke:#1d4ed8,color:#000;
    classDef proto fill:#fca5a5,stroke:#b91c1c,color:#000;
    classDef exec fill:#fde68a,stroke:#b45309,color:#000;
    class CALL,Q q;
    class PROTO proto;
    class EXEC exec;
  • 프로토콜 에러: 없는 도구 이름, 잘못된 인자, 서버 크래시 등 통합 자체의 문제 → 표준 JSON-RPC error 응답.
  • 도구 실행 에러: 도구는 실행됐는데 비즈니스 수준에서 실패(외부 API 한도 초과, 입력값 부적합, DB 연결 실패) → 정상 result 안에서 isError: truecontent에 설명을 담음.
JSON
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [
      { "type": "text", "text": "출발일이 올바르지 않습니다: 미래 날짜여야 합니다." }
    ],
    "isError": true
  }
}

이 구분이 중요한 이유: 프로토콜 에러는 "통합이 깨졌다"는 신호라 개발자가 고쳐야 하고, 실행 에러는 모델이 보고 추론해 재시도하거나 다른 길을 찾을 수 있는 정보이기 때문입니다. isError: true로 돌려주면 모델이 에러를 보고 교정 행동을 하거나 사람 개입을 요청할 수 있습니다.

⚠️ 흔한 실수
outputSchema와 isError의 상호작용: 스펙상 에러 응답(isError: true)은 outputSchema 검증을 건너뜁니다. 검증을 무조건 돌리면, 에러 메시지가 스키마에 안 맞는다며 원래 에러를 가려 버리는 버그가 생깁니다(실제 게이트웨이 구현에서 보고됨). isError가 참이면 구조 검증을 생략하세요.

동적 도구 목록 — list_changed

도구 집합이 런타임에 바뀔 수 있습니다(예: 인증 후 추가 도구 노출). listChanged capability를 선언한 서버는 목록이 바뀌면 알림을 보내야 합니다(SHOULD):

JSON
{ "jsonrpc": "2.0", "method": "notifications/tools/list_changed" }

이 알림을 받은 클라이언트는 tools/list를 다시 호출해 최신 목록을 받습니다. (알림 일반론은 13장)

이 장에서 배운 것

  • 세 primitive는 제어 모델로 갈린다: Tools=모델, Resources=애플리케이션, Prompts=사용자.
  • Tool은 모델이 호출하는 행동. 정의는 name·description·inputSchema(+선택 outputSchema·annotations), 결과는 content 배열·isError(+선택 structuredContent).
  • description은 모델을 향한 UX이고, annotations는 신뢰 불가. 사람이 호출을 거부할 수 있어야 한다.
  • 프로토콜 에러(JSON-RPC error)와 실행 에러(isError: true)는 다른 층위. 에러 응답은 outputSchema 검증을 건너뛴다.

✍️ 확인 문제

  1. "DB에 행을 삽입하는 기능"과 "DB 스키마를 보여주는 기능"은 각각 Tool·Resource 중 무엇이 더 적합한가? 제어 모델로 설명해 보자.
  2. 도구가 호출한 결제 API가 "잔액 부족"을 반환했다. JSON-RPC error로 줘야 할까, isError: true로 줘야 할까? 이유는?
  3. 도구 정의에 outputSchema를 넣었다면 결과에서 무엇을 반드시 반환해야 하나? 빠뜨리면 어떤 일이 생기나?
다음 장: 11. Resources — 읽을 수 있는 데이터/컨텍스트