Exponer Datos Legibles como Recursos MCP
Un recurso es la forma en que un servidor MCP entrega datos para que un cliente los lea, direccionado por una URI en lugar de invocado como una función.
Archivos, filas de bases de datos, valores de configuración y respuestas de API son todos encajes naturales para recursos una vez que los piensas como cosas que un cliente puede obtener, no como acciones que un modelo realiza.
Resumen
Los recursos en el SDK de Python MCP se definen con el decorador @mcp.resource(), al que se le pasa una URI o una plantilla de URI como argumento.
Un recurso estático tiene una URI fija, como config://settings, mientras que un recurso con plantilla utiliza marcadores de posición entre llaves, como notes://{name}, que se convierten en parámetros de función.
Dado que un recurso representa una lectura, nunca debe mutar el estado ni desencadenar un efecto secundario; esa distinción es lo que separa un recurso de una herramienta.
Devolver un tipo MIME sensato junto con el contenido ayuda al cliente a decidir cómo renderizar o analizar lo que recibe, ya sea texto plano, JSON o una imagen.
Los recursos se combinan de forma natural con las herramientas: una herramienta puede crear o actualizar algo, y un recurso puede exponer el resultado para su lectura posterior.
Receta
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("resource-recipes")
@mcp.resource("config://settings")
def get_settings() -> str:
"""Devuelve la configuración actual del servidor como JSON."""
return '{"theme": "dark", "max_results": 20}'
@mcp.resource("notes://{name}")
def get_note(name: str) -> str:
"""Devuelve el contenido de una nota con nombre."""
return f"Contenido de la nota '{name}'"Cuándo recurrir a esto:
- Los datos están destinados a ser leídos, no a ser actuados, como un archivo, un valor de configuración o una fila de base de datos.
- Desea que el cliente pueda listar y obtener los datos sin que el modelo tenga que construir una llamada a una herramienta.
- El mismo dato podría adjuntarse como contexto directamente por la aplicación, no solo obtenerse a petición del modelo.
- Desea un identificador estable y direccionable (una URI) para una pieza específica de datos que pueda referenciarse de manera consistente.
Ejemplo de Trabajo
# server.py
import json
import sqlite3
from pathlib import Path
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("docs-resources")
DB_PATH = Path(__file__).parent / "articles.db"
DOCS_DIR = Path(__file__).parent / "docs"
def _get_db() -> sqlite3.Connection:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
@mcp.resource("articles://{article_id}")
def get_article(article_id: str) -> str:
"""Devuelve una sola fila de artículo como JSON, direccionada por su id."""
conn = _get_db()
try:
row = conn.execute(
"SELECT id, title, body FROM articles WHERE id = ?", (article_id,)
).fetchone()
if row is None:
raise ValueError(f"No se encontró ningún artículo con el id {article_id}")
return json.dumps(dict(row))
finally:
conn.close()
@mcp.resource("docs://{filename}")
def get_doc_file(filename: str) -> str:
"""Devuelve el texto sin procesar de un archivo markdown del directorio docs."""
path = (DOCS_DIR / filename).resolve()
if DOCS_DIR.resolve() not in path.parents:
raise ValueError("el nombre del archivo debe permanecer dentro del directorio docs")
if not path.exists():
raise FileNotFoundError(f"No existe tal archivo: {filename}")
return path.read_text(encoding="utf-8")
@mcp.resource("stats://article-count")
def get_article_count() -> str:
"""Devuelve el número total de artículos como un recurso de resumen estático."""
conn = _get_db()
try:
count = conn.execute("SELECT COUNT(*) FROM articles").fetchone()[0]
return json.dumps({"article_count": count})
finally:
conn.close()
if __name__ == "__main__":
mcp.run(transport="stdio")Lo que esto demuestra:
- Un recurso con plantilla (
articles://{article_id}) que lee una sola fila de una conexión de base de datos real. - Un recurso con plantilla (
docs://{filename}) que sirve archivos desde el disco, con una protección contra traversales de ruta antes de tocar el sistema de archivos. - Un recurso estático (
stats://article-count) sin parámetros, útil para resúmenes o datos agregados. - Uso consistente de
json.dumpspara que los datos estructurados vuelvan en una forma predecible y analizable. - Lanzamiento de excepciones para una fila faltante o un archivo faltante, exactamente como lo haría una herramienta, ya que los recursos aún pueden fallar.
Análisis Profundo
Cómo Funciona
@mcp.resource("scheme://path/{param}")registra una plantilla de URI; el SDK extrae los segmentos{param}y los pasa como argumentos de función cuando se lee una URI coincidente.- Un cliente descubre recursos con
list_resources()(para URIs concretas) olist_resource_templates()(para las con plantilla), luego lee una URI específica conread_resource(uri). - A diferencia de una llamada a herramienta, una lectura de recurso no tiene un objeto de "argumentos" separado; cada parámetro proviene de la URI misma, lo que mantiene los recursos direccionables y cacheados.
- La porción del esquema de la URI (
config://,notes://,docs://) es arbitraria y elegida por usted; no es un protocolo de red real, solo un espacio de nombres. - El SDK infiere un tipo MIME del valor de retorno cuando puede, pero puede ser explícito para cualquier cosa que no sea texto plano.
Elección de un Esquema de URI
| Estilo de Esquema | Ejemplo | Mejor Para |
|---|---|---|
| Espacio de nombres personalizado | config://settings | Conceptos específicos del servidor sin equivalente en el sistema de archivos o red |
Estilo file:// | file:///reports/q1.csv | Datos que se mapean genuinamente a una ruta de archivo real |
| Estilo de dominio | articles://{id}, users://{id} | Filas o registros de una base de datos, un esquema por tipo de entidad |
| Agregado/estático | stats://summary | Resúmenes o recuentos fijos, sin parámetros |
Notas de Python
# Protege contra traversales de ruta siempre que un recurso lea desde el disco usando un
# segmento de ruta proporcionado por el usuario.
path = (DOCS_DIR / filename).resolve()
if DOCS_DIR.resolve() not in path.parents:
raise ValueError("el nombre del archivo debe permanecer dentro del directorio docs")Parámetros y Valores de Retorno
| Elemento | Tipo | Descripción |
|---|---|---|
| Plantilla de URI | str | Pasada a @mcp.resource(); los segmentos {param} se convierten en parámetros de función |
| Parámetros de función | str (típicamente) | Extraídos de los segmentos de URI coincidentes en el momento de la lectura |
| Valor de retorno | str (o bytes para contenido binario) | El contenido del recurso, devuelto al cliente en read_resource |
Trampas
- Tratar un recurso como una acción. Un recurso llamado
send_email://{to}implica un efecto secundario y engaña a los clientes que asumen que los recursos son seguros de leer repetidamente. Solución: mover cualquier cosa con un efecto secundario a una herramienta. - Sin protección contra traversales de ruta en recursos basados en el sistema de archivos. Una URI como
docs://../../etc/passwdpuede escapar del directorio previsto si construyes una ruta de forma ingenua. Solución: resuelve la ruta y confirma que permanece dentro de la raíz permitida antes de leer. - Devolver texto no estructurado para lo que en realidad son datos estructurados. Los llamadores tienen que analizar formatos ad hoc en lugar de JSON. Solución: devuelve
json.dumps(...)para cualquier cosa con más de un campo. - Abrir una conexión de base de datos por recurso sin cerrarla. Bajo carga, esto filtra conexiones y eventualmente agota el pool. Solución: usa un
try/finally(como se muestra arriba) o un gestor de contexto de conexión. - Sobrecargar una plantilla de recurso para que también acepte parámetros opcionales de estilo de consulta. Las plantillas de URI en MCP se basan en la ruta, no en la cadena de consulta, por lo que esto se vuelve incómodo e inconsistente. Solución: usa recursos separados o una herramienta si la lógica de selección de datos se vuelve lo suficientemente compleja como para necesitar filtros opcionales.
- Devolver silenciosamente una cadena vacía cuando una búsqueda falla. Un llamador no puede distinguir entre "sin datos" y "ocurrió un error". Solución: lanza una excepción con un mensaje claro cuando el recurso solicitado realmente no existe.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| Herramienta MCP | La operación realiza una acción o cálculo con un resultado | Los datos son una lectura simple sin efectos secundarios |
| Prompt MCP | Estás empaquetando instrucciones, no datos | El cliente necesita obtener o referenciar contenido real |
| Una herramienta que devuelve los mismos datos | Quieres que el modelo solicite explícitamente los datos como parte de un paso de razonamiento | Quieres que el cliente adjunte o cachee los datos como contexto ambiental |
| Acceso directo a base de datos/API fuera de MCP | Solo una aplicación interna necesitará estos datos | Quieres que los mismos datos sean direccionables desde Claude Code, Claude Desktop y otros clientes |
Preguntas Frecuentes
¿Cuál es la diferencia entre un recurso y una herramienta que simplemente devuelve datos?
- Un recurso se aborda mediante una URI estable y está destinado a ser leído, listado y potencialmente cacheado.
- Una llamada a herramienta es iniciada por el modelo como parte del razonamiento a través de una tarea y no tiene la misma garantía de direccionabilidad.
¿Puede una URI de recurso tener más de un parámetro?
Sí, una plantilla como orgs://{org_id}/users/{user_id} extrae ambos segmentos como parámetros de función separados, siempre que cada uno esté delimitado por su propio segmento de ruta.
¿Deben los recursos tener efectos secundarios alguna vez?
No. Los recursos están destinados a ser leídos de forma segura, potencialmente más de una vez, sin cambiar nada. Cualquier cosa que cree, actualice o elimine datos pertenece a una herramienta en su lugar.
¿Cómo devuelvo contenido binario, como una imagen, desde un recurso?
Devuelve bytes del manejador en lugar de str, y el SDK lo codificará apropiadamente para el cliente, típicamente junto con un tipo MIME explícito que describa el formato binario.
¿Qué sucede si un cliente solicita una URI de recurso sin un manejador coincidente?
El servidor devuelve un error que indica que el recurso no existe. Es por eso que el diseño exacto del esquema y la plantilla de URI es importante, ya que un error tipográfico en el esquema significa que no hay ningún manejador coincidente.
¿Puedo listar todos los recursos disponibles sin conocer sus URIs exactas?
Sí. list_resources() y list_resource_templates() permiten a un cliente descubrir lo que ofrece un servidor, incluidos los recursos con plantilla con sus marcadores de posición {param} mostrados, antes de leer cualquier URI específica.
¿Es el esquema de URI (como config:// o articles://) un protocolo real?
No, es solo un espacio de nombres que usted elige. MCP no requiere que estas sean URIs de red resolubles; solo necesitan ser únicas y significativas dentro de su servidor.
¿Cómo debo manejar un recurso que no encuentra sus datos?
Lanza una excepción con un mensaje específico, exactamente como lo harías en un manejador de herramientas, en lugar de devolver una cadena vacía o None que oculte el fallo.
¿Puede un recurso depender del estado de la base de datos que cambia con frecuencia?
Sí, los recursos pueden ser tan dinámicos como cualquier función manejadora; cada lectura se ejecuta de forma fresca, por lo que un recurso basado en base de datos siempre refleja los datos actuales a menos que agregues tu propia capa de caché.
¿Deben los recursos basados en archivos confiar directamente en el parámetro del nombre de archivo?
No. Siempre resuelve la ruta resultante y verifica que permanezca dentro del directorio raíz previsto antes de leer, para evitar traversales de ruta a archivos fuera del directorio servido.
¿Qué tipo MIME debo usar para un recurso JSON?
application/json es la opción convencional, y devolver contenido a través de json.dumps(...) junto con ese tipo MIME proporciona a los clientes una carga útil predecible y analizable.
¿Puede un servidor mezclar recursos estáticos y con plantilla?
Sí, un solo servidor FastMCP puede registrar cualquier número de recursos estáticos (config://settings) y con plantilla (articles://{id}) uno al lado del otro.
Relacionado
- Comprendiendo MCP: Herramientas, Recursos y Prompts - la distinción conceptual entre recursos, herramientas y prompts.
- Conceptos Básicos de los Conceptos Centrales de MCP - un servidor y cliente stdio mínimo sobre el cual construir recursos.
- Definición de Herramientas Invocables en un Servidor MCP - la primitiva hermana para acciones y cálculos.
- Comparación de Herramientas, Recursos y Prompts de MCP - decidir qué primitiva se ajusta a una capacidad dada.
- Mejores Prácticas de Conceptos Centrales de MCP - una lista de verificación para el diseño de URI de recursos.
Versiones de Stack: Escrito contra la línea de modelos Claude actual a partir de ~junio de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (el predeterminado), y Claude Haiku 4.5 - y la especificación actual del Protocolo de Contexto del Modelo. Los nombres de los modelos, las versiones del SDK y la especificación MCP cambian rápidamente; verifica los detalles actuales en platform.claude.com/docs y modelcontextprotocol.io antes de confiar en ellos.