11. Resources — 읽을 수 있는 데이터/컨텍스트

🎯 이 장의 목표
  • Resource가 "애플리케이션 제어" 읽기 전용 데이터라는 점을 이해한다
  • URI 식별, 정적/동적 리소스, URI 템플릿, 구독을 구분해 쓸 수 있다
  • "모델은 리소스를 스스로 못 가져온다"는 현실과 그 우회법(도구 래핑)을 안다

도서관 비유

Tool이 "모델이 누르는 버튼"이라면, Resource는 "도서관 서가에 꽂힌 자료"입니다. 자료에는 청구기호(URI)가 붙어 있고, 사서(호스트 애플리케이션)가 필요하다고 판단한 책을 꺼내 책상(컨텍스트) 위에 올려 둡니다. 책 자체는 읽기 전용이고, 꺼낼지 말지는 사서가 정합니다 — 책이 스스로 책상에 올라오지 않습니다.

Resources는 MCP 서버가 클라이언트에 노출해 LLM 상호작용의 컨텍스트로 읽히게 하는 데이터·콘텐츠입니다. 파일, 데이터베이스 스키마, 애플리케이션별 정보 등이 해당하며, 각각 유일한 URI로 식별되는 읽기 전용 컨텍스트 제공자입니다 — 서버 측에서 행동을 실행하지 않습니다.

핵심은 애플리케이션 제어(application-controlled) 입니다. 클라이언트 애플리케이션이 리소스를 언제 어떻게 쓸지 정합니다.

URI로 식별 — 정적과 동적

각 리소스는 유일한 URI를 가지며, 프로토콜·경로 구조는 서버 구현이 정의합니다. 서버는 자체 URI 스킴을 만들 수 있습니다(예: resource://, file://, db://, logs://).

CODE
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 배열입니다:

JSON
{
  "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로 템플릿을 노출합니다.

CODE
users://{user_id}/profile
files://{path}
weather://{city}/current

클라이언트(또는 사용자)는 {user_id} 자리에 실제 값을 채워 구체적 URI를 만들어 resources/read합니다. IDE에서 # 뒤에 리소스 URI를 참조하고 인자를 채워 넣는 "리소스 템플릿" UX가 이 위에서 동작합니다.

💡 팁
Python(공식 SDK/FastMCP)에서는 데코레이터의 URI에 중괄호를 쓰면 함수 인자와 자동으로 매핑됩니다: @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은 도구를 호출하듯이 리소스를 직접 요청할 방법이 없습니다. 클라이언트가 리소스를 능동적으로 컨텍스트에 주입하거나, 서버가 리소스 접근을 도구로 감싸지 않으면, 모델은 그 리소스를 아예 보지 못합니다.

클라이언트마다 처리가 다릅니다 — 어떤 데스크톱 앱은 사용자가 리소스를 명시적으로 골라야 하고, 다른 클라이언트는 휴리스틱으로 자동 선택하며, 일부는 모델이 직접 고르게 할 수도 있습니다. 서버 작성자는 이 모든 패턴에 대비해야 합니다.

📌 핵심
핵심 설계 지침: 데이터를 모델이 자동으로 쓰게 하고 싶으면, 리소스만으로는 부족할 수 있습니다. 스펙도 권고합니다 — 모델이 자동으로 데이터를 쓰게 하려면 Tools 같은 모델 제어 primitive를 쓰라. 흔한 패턴은 "리소스로도 노출하되, 모델이 꺼내 쓸 수 있게 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는 권한 경계이므로 접근 통제·경로 정규화가 필수.

✍️ 확인 문제

  1. "사용자별 주문 내역"을 리소스로 노출하려 한다. 사용자마다 URI를 일일이 나열하지 않으려면 무엇을 쓰나? 예시 URI를 하나 적어 보자.
  2. 모델이 자동으로 회사 위키 문서를 참고하게 하고 싶다. 리소스만으로 충분할까? 아니라면 어떤 보완책이 있나?
  3. files://{path} 템플릿에서 클라이언트가 path=../../secret.env를 넣었다. 서버가 막아야 할 공격은 무엇이고, 어디서 막아야 하나?
다음 장: 12. Prompts — 재사용 프롬프트 템플릿