11. Resources — 읽을 수 있는 데이터/컨텍스트
- Resource가 "애플리케이션 제어" 읽기 전용 데이터라는 점을 이해한다
- URI 식별, 정적/동적 리소스, URI 템플릿, 구독을 구분해 쓸 수 있다
- "모델은 리소스를 스스로 못 가져온다"는 현실과 그 우회법(도구 래핑)을 안다
도서관 비유
Tool이 "모델이 누르는 버튼"이라면, Resource는 "도서관 서가에 꽂힌 자료"입니다. 자료에는 청구기호(URI)가 붙어 있고, 사서(호스트 애플리케이션)가 필요하다고 판단한 책을 꺼내 책상(컨텍스트) 위에 올려 둡니다. 책 자체는 읽기 전용이고, 꺼낼지 말지는 사서가 정합니다 — 책이 스스로 책상에 올라오지 않습니다.
Resources는 MCP 서버가 클라이언트에 노출해 LLM 상호작용의 컨텍스트로 읽히게 하는 데이터·콘텐츠입니다. 파일, 데이터베이스 스키마, 애플리케이션별 정보 등이 해당하며, 각각 유일한 URI로 식별되는 읽기 전용 컨텍스트 제공자입니다 — 서버 측에서 행동을 실행하지 않습니다.
핵심은 애플리케이션 제어(application-controlled) 입니다. 클라이언트 애플리케이션이 리소스를 언제 어떻게 쓸지 정합니다.
URI로 식별 — 정적과 동적
각 리소스는 유일한 URI를 가지며, 프로토콜·경로 구조는 서버 구현이 정의합니다. 서버는 자체 URI 스킴을 만들 수 있습니다(예: resource://, file://, db://, logs://).
file:///project/README.md db://customers/schema config://app/settings logs://recent?timeframe=1h
리소스는 두 종류입니다:
- 정적(static): 고정된 데이터. 예: 서버 설명, 설정 파일.
- 동적(dynamic): 입력 파라미터나 서버 상태에 따라 다른 데이터를 반환. 예: 특정 사용자 프로필, 특정 시간대 로그.
리소스 내용은 UTF-8 텍스트 또는 base64로 인코딩된 이진(blob) 데이터를 담습니다. 여기서 base64는 이미지·PDF 같은 이진 데이터(0과 1의 바이트 덩어리)를 텍스트로만 안전하게 실어 나르기 위한 인코딩 방식입니다. JSON은 텍스트 형식이라 바이트를 그대로 못 담으므로, 바이트를 A–Z a–z 0–9 + / 64개 문자로 바꿔 문자열로 표현합니다. blob(binary large object)은 그렇게 담기는 이진 데이터 덩어리를 가리키는 말입니다.
발견과 읽기 — resources/list, resources/read
서버는 구체적 리소스 목록을 resources/list로 노출하고, 클라이언트는 resources/read로 URI를 지정해 내용을 읽습니다. 읽기 응답은 contents 배열입니다:
{
"contents": [
{
"uri": "file:///project/README.md",
"mimeType": "text/markdown",
"text": "# 프로젝트 소개\n..."
}
]
}
각 항목은 uri, 선택적 mimeType, 그리고 text(텍스트) 또는 blob(base64 이진) 중 하나를 가집니다. 한 번의 resources/read에 여러 리소스를 반환할 수도 있습니다 — 예를 들어 디렉터리를 읽으면 그 안의 파일 목록을 한꺼번에 돌려줄 수 있습니다.
URI 템플릿 — 파라미터화된 리소스
리소스를 매번 일일이 나열하긴 어렵습니다. 예컨대 "모든 사용자 프로필"을 정적 목록으로 두면 끝이 없죠. 그래서 MCP는 URI 템플릿(RFC 6570 스타일)으로 동적 접근을 지원합니다. 서버는 resources/templates/list로 템플릿을 노출합니다.
users://{user_id}/profile
files://{path}
weather://{city}/current
클라이언트(또는 사용자)는 {user_id} 자리에 실제 값을 채워 구체적 URI를 만들어 resources/read합니다. IDE에서 # 뒤에 리소스 URI를 참조하고 인자를 채워 넣는 "리소스 템플릿" UX가 이 위에서 동작합니다.
@mcp.resource("greeting://{name}") → def get_greeting(name: str). 5부에서 직접 만듭니다.실시간 업데이트 — list_changed와 subscribe
리소스는 두 가지 실시간 갱신 메커니즘을 가집니다.
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: (1) 목록 변경 알림
S->>C: notifications/resources/list_changed
C->>S: resources/list (다시 가져옴)
Note over C,S: (2) 특정 리소스 구독
C->>S: resources/subscribe (uri)
S->>C: notifications/resources/updated (그 리소스가 바뀜)
C->>S: resources/read (최신 내용)
- 목록 변경: 사용 가능한 리소스 목록이 바뀌면 서버가
notifications/resources/list_changed를 보내고, 클라이언트가resources/list를 다시 호출합니다. - 개별 구독: 클라이언트가
resources/subscribe로 특정 URI를 구독하면, 그 리소스가 바뀔 때 서버가notifications/resources/updated를 보내고, 클라이언트가resources/read로 최신 내용을 가져옵니다.
구독은 서버가 resources.subscribe capability를 광고한 경우에만 가능합니다(05장).
현실 — "모델은 리소스를 스스로 못 가져온다"
여기서 중요한 실무적 함정을 짚어야 합니다. 리소스는 세 primitive 중 클라이언트 지원이 가장 덜 성숙합니다. 스펙은 annotations·구독·URI 템플릿·콘텐츠 타입까지 포괄적으로 정의하지만, 클라이언트 구현이 따라오지 못하는 경우가 많습니다.
결정적으로, 리소스 접근은 클라이언트 측 동작이라, LLM은 도구를 호출하듯이 리소스를 직접 요청할 방법이 없습니다. 클라이언트가 리소스를 능동적으로 컨텍스트에 주입하거나, 서버가 리소스 접근을 도구로 감싸지 않으면, 모델은 그 리소스를 아예 보지 못합니다.
클라이언트마다 처리가 다릅니다 — 어떤 데스크톱 앱은 사용자가 리소스를 명시적으로 골라야 하고, 다른 클라이언트는 휴리스틱으로 자동 선택하며, 일부는 모델이 직접 고르게 할 수도 있습니다. 서버 작성자는 이 모든 패턴에 대비해야 합니다.
read_resource류 도구로도 감싸기"입니다.🔒 보안 — 리소스는 권한 경계다: 리소스 URI는 파일·DB·내부 데이터를 가리킵니다. 클라이언트가 임의 URI(file:///etc/passwd,db://other_tenant/...)를 읽으려 할 수 있으므로, 서버는 접근 통제와 경로 정규화를 해야 합니다. URI 템플릿의{path}를 그대로 파일 경로에 쓰면 경로 탈출(path traversal)에 노출됩니다. 검증은 서버 책임입니다(15장, 21장).
Tool과 Resource, 무엇을 언제
| 상황 | 권장 | 이유 |
|---|---|---|
| 모델이 행동·상태 변경을 해야 함 | Tool | 모델 제어, 부작용 허용 |
| 호스트가 파일·스키마·레코드를 컨텍스트로 읽음(부작용 없음) | Resource | 애플리케이션 제어, 읽기 전용 |
| 모델이 데이터를 자동으로 끌어와야 함 | Tool(또는 리소스+도구 래핑) | 리소스는 모델이 직접 못 부름 |
이 장에서 배운 것
- Resource는 URI로 식별되는 읽기 전용 데이터이며 애플리케이션 제어다. 텍스트 또는 base64 blob.
resources/list·resources/read로 발견·읽기, URI 템플릿(resources/templates/list)으로 파라미터화, subscribe로 변경 통지.- 모델은 리소스를 스스로 못 가져온다 — 자동 사용이 필요하면 도구로 감싸는 패턴을 쓴다.
- 리소스 URI는 권한 경계이므로 접근 통제·경로 정규화가 필수.
✍️ 확인 문제
- "사용자별 주문 내역"을 리소스로 노출하려 한다. 사용자마다 URI를 일일이 나열하지 않으려면 무엇을 쓰나? 예시 URI를 하나 적어 보자.
- 모델이 자동으로 회사 위키 문서를 참고하게 하고 싶다. 리소스만으로 충분할까? 아니라면 어떤 보완책이 있나?
files://{path}템플릿에서 클라이언트가path=../../secret.env를 넣었다. 서버가 막아야 할 공격은 무엇이고, 어디서 막아야 하나?
다음 장: 12. Prompts — 재사용 프롬프트 템플릿