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로 도구 목록을 발견합니다. 서버는 도구 정의 배열로 응답합니다(도구가 수백 개면 커서 기반 페이지네이션 지원). 페이지네이션은 긴 목록을 한 번에 다 주지 않고 몇 개씩 나눠 주는 방식이고, 커서는 "다음에 이어서 줄 위치"를 가리키는 표식입니다 — 책갈피처럼 다음 페이지를 받을 때 그 표식을 제시합니다.
{
"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 | (선택) 도구 동작에 대한 힌트 (읽기 전용 여부 등) |
inputSchema·outputSchema에 쓰인 형식이 JSON Schema입니다. "이 JSON은 이런 모양이어야 한다"를 그 자체로 JSON으로 기술하는 표준 규격입니다. 예컨대 {"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}는 "객체여야 하고, 문자열 location 필드가 필수"라는 뜻입니다. 모델은 이 스키마를 보고 올바른 인자를 만들고, 서버는 들어온 인자가 스키마에 맞는지 검증합니다. 데이터의 "양식 서식"이라고 보면 됩니다.description은 모델이 읽는 라벨입니다. "Get weather"보다 "도시명이나 우편번호로 현재 기온·강수·바람을 조회한다. 예보가 아니라 현재값"처럼 구체적일수록 모델이 정확히 호출합니다. 5부에서 FastMCP는 함수의 독스트링을 description으로 가져갑니다.🔒 annotations는 신뢰 불가: 도구 annotations는 동작 힌트(예: "읽기 전용", "파괴적")일 뿐, 신뢰·안전 관점에서 클라이언트는 신뢰할 수 있는 서버에서 온 것이 아닌 한 annotations를 신뢰하지 말아야 합니다(스펙 MUST). 즉 "읽기 전용이라고 적혀 있으니 안전하다"고 가정하면 안 됩니다.
도구 호출 — tools/call과 결과
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "get_weather_data",
"arguments": { "location": "Seoul" }
}
}
성공 응답은 content 배열(텍스트·이미지 등)과 isError: false를 담습니다:
{
"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만 보기 때문입니다.
{
"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: true와content에 설명을 담음.
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{ "type": "text", "text": "출발일이 올바르지 않습니다: 미래 날짜여야 합니다." }
],
"isError": true
}
}
이 구분이 중요한 이유: 프로토콜 에러는 "통합이 깨졌다"는 신호라 개발자가 고쳐야 하고, 실행 에러는 모델이 보고 추론해 재시도하거나 다른 길을 찾을 수 있는 정보이기 때문입니다. isError: true로 돌려주면 모델이 에러를 보고 교정 행동을 하거나 사람 개입을 요청할 수 있습니다.
동적 도구 목록 — list_changed
도구 집합이 런타임에 바뀔 수 있습니다(예: 인증 후 추가 도구 노출). listChanged capability를 선언한 서버는 목록이 바뀌면 알림을 보내야 합니다(SHOULD):
{ "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 검증을 건너뛴다.
✍️ 확인 문제
- "DB에 행을 삽입하는 기능"과 "DB 스키마를 보여주는 기능"은 각각 Tool·Resource 중 무엇이 더 적합한가? 제어 모델로 설명해 보자.
- 도구가 호출한 결제 API가 "잔액 부족"을 반환했다. JSON-RPC
error로 줘야 할까,isError: true로 줘야 할까? 이유는? - 도구 정의에
outputSchema를 넣었다면 결과에서 무엇을 반드시 반환해야 하나? 빠뜨리면 어떤 일이 생기나?
다음 장: 11. Resources — 읽을 수 있는 데이터/컨텍스트