Table of contents
- 누가 호출 결정을 내리나
- Tools — 모델이 호출하는 함수
- Resources — 호스트가 끌어다 쓰는 데이터
- Prompts — 사용자가 부르는 슬래시 명령
- 한 서버에 셋이 같이 있을 때
누가 호출 결정을 내리나
세 가지 모두 “서버가 클라이언트에게 무언가를 노출한다”는 점은 같다. 결정적 차이는 노출된 것을 누가 호출하느냐다.
| Primitive | 호출 결정 | 호출 시점 |
|---|---|---|
| Tools | LLM이 자율적으로 | 대화 도중 모델이 필요하다고 판단할 때 |
| Resources | Host가 정책적으로 | 사용자 의도를 보고 컨텍스트로 끌어와 LLM에 주입할 때 |
| Prompts | 사용자가 명시적으로 | 슬래시 명령 같은 UI에서 직접 고를 때 |
쉽게 말해, Tools는 모델이 운전한다. Resources는 호스트가 운전한다. Prompts는 사용자가 운전한다. 같은 서버에 셋이 다 있어도 각각이 다른 사람의 손에 들려 있다.
이 분기는 단순한 분류가 아니라 무엇을 어디에 노출할지 결정할 때의 기준이 된다. DB 서버 하나를 만든다고 해보자. “임의 SQL 실행”은 LLM이 상황 보고 부르는 게 자연스러우니 Tool, “테이블 스키마”는 호스트가 컨텍스트로 한꺼번에 끼워주는 게 자연스러우니 Resource, “월간 매출 정리해줘 같은 정해진 분석”은 사용자가 직접 부르는 게 자연스러우니 Prompt다.
Tools — 모델이 호출하는 함수
Tools는 가장 익숙한 primitive다. LLM이 자기 도구 카탈로그를 보고, 사용자 질문에 맞춰 적절한 함수를 골라 호출한다.
서버가 Tool을 노출하려면 tools capability를 선언하고, 클라이언트는 연결 직후 tools/list로 카탈로그를 가져온다.
{
"jsonrpc": "2.0", "id": 1, "method": "tools/list"
}
응답에는 각 도구의 이름·설명·입력 스키마가 들어 있다.
{
"tools": [{
"name": "get_weather",
"title": "Weather Information Provider",
"description": "Get current weather information for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": { "type": "string", "description": "City name or zip code" }
},
"required": ["location"]
}
}]
}
inputSchema는 JSON Schema 2020-12 기본이다. LLM은 이 스키마를 보고 호출 인자를 만들어내고, 실제 호출은 tools/call로 나간다.
{
"method": "tools/call",
"params": { "name": "get_weather", "arguments": { "location": "Seoul" } }
}
응답은 content 배열이다. 텍스트, 이미지, 오디오, Resource 링크, 임베디드 Resource까지 섞을 수 있다. 가장 흔한 텍스트 응답은 이렇다.
{
"content": [{ "type": "text", "text": "Seoul: 15°C, partly cloudy" }],
"isError": false
}
두 종류의 에러
Tool 호출에는 프로토콜 에러와 실행 에러가 따로 있다.
- 프로토콜 에러 — 표준 JSON-RPC
error필드. “그런 도구 없음”, “잘못된 요청 형식” 같은 요청 자체의 문제. 모델이 자기 입력으로 고치기 어렵다. - 실행 에러 — 응답 결과에
isError: true로 표시. “잘못된 날짜 형식”, “API 호출 실패” 같은 실행 단계의 문제. 메시지에 어디가 잘못됐는지를 담아 LLM이 다음 호출에서 보정할 수 있게 한다.
자기 보정이 가능한 에러를 isError로 돌려주는 게 spec의 권장이다. 사용자에게 다시 묻지 않고 LLM이 알아서 한 번 더 시도한다.
구조화된 응답
텍스트만 돌려주면 LLM이 다시 파싱해야 한다. 구조가 있는 데이터는 outputSchema를 미리 선언하고 응답에 structuredContent를 같이 넣는다.
{
"content": [{ "type": "text", "text": "{\"temp\": 22.5, \"conditions\": \"Partly cloudy\"}" }],
"structuredContent": { "temp": 22.5, "conditions": "Partly cloudy" }
}
클라이언트는 outputSchema로 응답 검증을 하고, LLM에 자료형 정보를 같이 넘긴다. 일관된 자료형 체계를 유지할 수 있고, IDE급 통합에서 강력하다.
tools/list_changed
서버가 동적으로 도구를 추가·제거할 수 있으면 listChanged: true로 선언하고, 변할 때마다 알림을 푸시한다.
{ "method": "notifications/tools/list_changed" }
클라이언트는 알림을 받자마자 다시 tools/list를 호출해 LLM의 도구 카탈로그를 갱신한다.
Resources — 호스트가 끌어다 쓰는 데이터
Resources는 컨텍스트로 LLM에 흘려넣을 데이터를 노출한다. 파일 내용, DB 레코드, API 응답, 페이지 본문 같은 것들이다. 각 자원은 URI로 식별된다.
호출 결정은 호스트의 몫이다. 사용자가 명시적으로 고르거나(아래 예시처럼 picker UI를 띄운다), 호스트가 휴리스틱으로 자동 선택하거나, AI가 추천한 것을 호스트가 받아들이는 식이다.

출처: Model Context Protocol 공식 spec
목록은 resources/list로 가져오고, 내용은 resources/read로 읽는다.
{
"method": "resources/read",
"params": { "uri": "file:///project/src/main.rs" }
}
응답은 텍스트 또는 base64 인코딩된 바이너리(blob) 형태로 돌아온다.
{
"contents": [{
"uri": "file:///project/src/main.rs",
"mimeType": "text/x-rust",
"text": "fn main() {\n println!(\"Hello world!\");\n}"
}]
}
표준 URI 스킴
spec이 권장하는 스킴이 몇 가지 있다.
file://— 파일시스템처럼 동작하는 자원. 실제 디스크에 매핑될 필요는 없다.https://— 클라이언트가 직접 가져올 수 있는 웹 자원에 한해서.git://— Git 버전 관리 통합용.- 나머지는 자유. RFC 3986에 맞기만 하면 된다.
Resource Templates
URI가 매번 다르면(예: file:///{path}), resources/templates/list로 템플릿을 노출하고 클라이언트가 동적으로 채워 넣는다. URI Template 표준이다.
Subscribe
자원 내용이 자주 바뀌고, 클라이언트가 그 변화를 따라가야 하면 resources/subscribe로 구독한다. 서버는 변경 시점에 notifications/resources/updated를 푸시하고, 클라이언트는 다시 resources/read로 새 내용을 가져온다.
{ "method": "resources/subscribe", "params": { "uri": "file:///project/src/main.rs" } }
파일이 자주 변경되는 IDE 통합 같은 데서 자연스럽다. 모든 서버가 지원할 필요는 없다 — subscribe는 capability에서 옵션이다.
Prompts — 사용자가 부르는 슬래시 명령
Prompts는 사용자가 명시적으로 부르는 템플릿이다. 가장 흔한 형태는 슬래시 명령이다.

출처: Model Context Protocol 공식 spec
서버는 prompts/list로 카탈로그를, prompts/get으로 인자를 채운 메시지를 돌려준다.
{
"method": "prompts/get",
"params": {
"name": "code_review",
"arguments": { "code": "def hello():\n print('world')" }
}
}
응답은 LLM에 그대로 흘려보낼 수 있는 메시지 배열이다.
{
"messages": [{
"role": "user",
"content": {
"type": "text",
"text": "Please review this Python code:\ndef hello():\n print('world')"
}
}]
}
인자와 자동 완성
arguments는 사용자가 채워 넣는 슬롯이다. 슬래시 명령 UI에서 인자별로 입력 칸이 뜨거나, completion API로 후보를 자동 완성받는다.
Prompts와 Tools의 분기점
비슷해 보이지만 결이 다르다. Tools는 모델이 알아서 부른다. Prompts는 사용자가 직접 부른다. 같은 동작이라도 자동화하고 싶으면 Tool, 사용자가 흐름을 통제하길 원하면 Prompt에 둔다. “이슈 정리해줘”를 매번 자연어로 말하기보다 /summarize-issues로 부르고 싶을 때가 Prompt의 자리다.
한 서버에 셋이 같이 있을 때
DB 서버 하나를 예로 들면 셋이 자연스럽게 갈린다.
flowchart TB
classDef tool fill:#4a90d9,color:#fff,stroke:#2c5d8f
classDef res fill:#5ca45c,color:#fff,stroke:#3d7a3d
classDef prom fill:#d4943a,color:#fff,stroke:#a06a26
DB[DB MCP Server]
DB --> T1[Tools<br/>execute_query<br/>explain_plan]:::tool
DB --> R1[Resources<br/>schema://public/users<br/>schema://public/orders]:::res
DB --> P1[Prompts<br/>/monthly-revenue<br/>/slow-queries-report]:::prom
- Tools —
execute_query,explain_plan. 모델이 사용자 질문 보고 알아서 부른다. - Resources —
schema://public/users,schema://public/orders. 호스트가 사용자가 작업 중인 테이블의 스키마를 컨텍스트로 미리 넣어준다. - Prompts —
/monthly-revenue,/slow-queries-report. 사용자가 자주 돌리는 정해진 분석을 슬래시 명령으로 노출한다.
세 가지 모두 같은 데이터베이스를 향한 인터페이스지만, 호출 주체가 다르고 호출 시점이 다르다. 한 서버에서 셋을 같이 노출하면 LLM·호스트·사용자가 각자 자기 손에 맞는 도구를 가져간다.
다음 편에서는 클라이언트가 서버에게 노출하는 Sampling, Roots, Elicitation을 본다. 방향이 거꾸로다 — 서버가 호스트의 LLM을 빌리거나, 작업 범위를 묻거나, 사용자에게 추가 정보를 받아오는 흐름이다.




Loading comments...