리소스
리소스 (Resources)
리소스(resource) 는 애플리케이션이 읽으라고 노출하는 데이터입니다.
그게 구분의 핵심이에요. 도구는 모델이 호출하기로 결정하는 것입니다. 리소스는 애플리케이션이 (설정 파일, 레코드, 문서 같은 것을) 불러와서 모델 앞에 컨텍스트로 놓기로 결정하는 것이지요.
평범한 파이썬 함수에 @mcp.resource(uri)를 붙이면 선언됩니다.
첫 번째 리소스
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.resource("config://app")
def get_config() -> str:
"""The active shop configuration."""
return "theme=dark\nlanguage=en"
도구와 같은 모양에, 한 가지가 더 있습니다. 바로 URI입니다. 리소스는 이름이 아니라 주소로 접근합니다. 클라이언트는 get_config를 요청하지 않고 config://app을 요청하지요.
SDK는 나머지를 여전히 함수에서 읽습니다.
- 이름은 함수 이름입니다:
get_config. - 클라이언트가 보는 설명은 docstring입니다.
- 콘텐츠는 여러분이 반환하는 그 무엇이든입니다.
resources/list 동안 클라이언트는 이걸 받습니다.
{
"name": "get_config",
"uri": "config://app",
"description": "The active shop configuration.",
"mimeType": "text/plain"
}
그리고 config://app을 읽으면 함수가 실행되고 반환값이 텍스트로 돌아옵니다.
result.contents # [TextResourceContents(uri="config://app", mime_type="text/plain", text="theme=dark\nlanguage=en")]
!!! tip
나열(listing)은 값이 쌉니다. 여러분의 함수는 resources/list 동안 호출되지 않고, resources/read에서만, 그것도 요청된 URI에 대해서만 호출됩니다. 리소스 천 개를 노출해도 누군가 여는 것에 대해서만 비용을 냅니다.
직접 해 보기
MCP Inspector로 서버를 실행합니다.
uv run mcp dev server.py
출력된 URL을 열고 Resources 탭으로 갑니다. config://app이 설명과 함께 목록에 있어요. 클릭하면 Inspector가 읽습니다. 설정 두 줄이 거기 있죠.
리소스 템플릿
레코드마다 URI 하나는 확장이 안 됩니다. URI에 플레이스홀더를, 함수에 그에 맞는 파라미터를 넣으세요.
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.resource("config://app")
def get_config() -> str:
"""The active shop configuration."""
return "theme=dark\nlanguage=en"
@mcp.resource("users://{user_id}/profile")
def get_user_profile(user_id: str) -> str:
"""A customer's profile."""
return f"User {user_id}: 12 orders since 2021."
URI에 {user_id}, 함수에 user_id: str. 그게 계약의 전부입니다.
이제 이건 리소스 템플릿이 되었고, 자리를 옮깁니다. resources/list를 떠나 resources/templates/list에 패턴(주소가 아니라)으로 나타나죠.
{
"name": "get_user_profile",
"uriTemplate": "users://{user_id}/profile",
"description": "A customer's profile.",
"mimeType": "text/plain"
}
클라이언트가 플레이스홀더를 채우고 구체 URI를 읽습니다. users://42/profile, users://ada/profile. 함수 하나가 그 전부에 답하고, 매칭된 값이 user_id로 전달됩니다.
result.contents # [TextResourceContents(uri="users://42/profile", text="User 42: 12 orders since 2021.")]
결과의 uri를 주목하세요. 그것은 클라이언트가 요청한 구체 URI이지 템플릿이 아닙니다.
!!! check
플레이스홀더와 파라미터는 일치해야 합니다. 함수 파라미터를 user로 바꾸는데 URI가 여전히 {user_id}라면, 데코레이터는 어떤 클라이언트도 가까이 오기 전에 import 시점에 거부합니다.
```text
ValueError: Mismatch between URI parameters {'user_id'} and function parameters {'user'}
```
불일치는 언제나 버그일 수밖에 없으므로, SDK는 그런 상태로 서버가 시작되는 것을 불가능하게 만듭니다.
플레이스홀더 문법은 RFC 6570입니다. {+path}는 다중 세그먼트 값, {?q,lang}은 선택적 쿼리 파라미터 등이죠. SDK는 추출된 값에 기본적으로 경로 안전 검사도 적용합니다. 전체 참조는 **URI templates and path safety**를 보세요.
get_user_profile은 Context로 어노테이트된 파라미터도 받을 수 있습니다. SDK가 그걸 URI 파라미터로 취급하지 않고 주입하며, The Context 페이지가 그게 주는 것을 다룹니다.
여러분이 반환하는 것
str에만 제한되지 않습니다. 각 리소스에 mime_type을 주고 맞는 무엇이든 반환하세요.
import base64
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.resource("docs://readme", mime_type="text/markdown")
def readme() -> str:
"""How to use this server."""
return "# Bookshop\n\nSearch the catalog with the `search_books` tool."
@mcp.resource("stats://catalog", mime_type="application/json")
def catalog_stats() -> dict[str, int]:
"""Live counts for the catalog."""
return {"books": 1204, "authors": 391}
@mcp.resource("covers://placeholder", mime_type="image/gif")
def placeholder_cover() -> bytes:
"""A 1x1 transparent GIF, shown when a book has no cover."""
return base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")
-
readme는str을 반환해서 그대로 보내집니다. 흔한 경우예요. -
catalog_stats는dict를 반환해서 SDK가 JSON 텍스트로 직렬화해 줍니다.{ "books": 1204, "authors": 391 } -
placeholder_cover는bytes를 반환해서TextResourceContents대신BlobResourceContents를 받습니다. 여러분의 bytes가blob필드에 base64로 인코딩되어 있죠.
그 밖에 JSON 직렬화 가능한 것은 모두 같은 규칙이 적용됩니다. 리스트, Pydantic 모델, dataclass. str도 bytes도 아니면 JSON이 됩니다.
mime_type은 여러분이 선언하는 것이고, 기본값은 text/plain입니다. SDK는 무엇을 반환할지 들여다보고 추측하지 않아서, 라벨을 안 붙인 dict 리소스도 여전히 plain text로 광고됩니다.
!!! tip
@mcp.resource()도 name=, title=, description=을 받아요. 함수에서 유도하고 싶지 않을 때죠. 그리고 쓸 함수 자체가 없을 때는 mcp.server.mcpserver.resources에 준비된 Resource 클래스(TextResource, BinaryResource, FileResource, HttpResource, DirectoryResource)가 있고, mcp.add_resource(...)로 등록합니다.
클라이언트는 리소스에 **구독(subscribe)**해서 바뀔 때 알림을 받을 수도 있습니다. 그건 클라이언트 쪽 이야기이고 **The Client**에 있습니다.
정리(Recap)
- 함수에
@mcp.resource(uri)를 붙이면 리소스가 됩니다. URI가 주소, 반환값이 콘텐츠, docstring이 설명이에요. - URI의
{placeholder}는 템플릿을 만듭니다.resources/templates/list에 나열되고, 함수 하나가 매칭되는 모든 URI를 서빙하죠. - 플레이스홀더 이름은 함수의 파라미터 이름과 같아야 합니다. 틀리면 운영이 아니라 import 시점에 알게 됩니다.
- 여러분의 함수는 리소스가 나열될 때가 아니라 읽힐 때 실행됩니다.
str은 텍스트,bytes는 base64 blob, 그 밖에는 JSON 텍스트가 됩니다.mime_type=으로 라벨을 붙이세요.- 도구는 모델이 행동하게 하는 것. 리소스는 애플리케이션이 읽는 것.
세 번째 프리미티브, 사람이 메뉴에서 고르는 것은 Prompts 입니다.