리소스

리소스 (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_profileContext로 어노테이트된 파라미터도 받을 수 있습니다. 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")
  • readmestr을 반환해서 그대로 보내집니다. 흔한 경우예요.

  • catalog_statsdict를 반환해서 SDK가 JSON 텍스트로 직렬화해 줍니다.

    {
      "books": 1204,
      "authors": 391
    }
    
  • placeholder_coverbytes를 반환해서 TextResourceContents 대신 BlobResourceContents를 받습니다. 여러분의 bytes가 blob 필드에 base64로 인코딩되어 있죠.

그 밖에 JSON 직렬화 가능한 것은 모두 같은 규칙이 적용됩니다. 리스트, Pydantic 모델, dataclass. strbytes도 아니면 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 입니다.

출처: Python SDK — Resources

더 알아보기 (Learn more)