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.
Busca en todas las páginas de la documentación
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.
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.
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:
# 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:
articles://{article_id}) que lee una sola fila de una conexión de base de datos real.docs://{filename}) que sirve archivos desde el disco, con una protección contra traversales de ruta antes de tocar el sistema de archivos.stats://article-count) sin parámetros, útil para resúmenes o datos agregados.json.dumps para que los datos estructurados vuelvan en una forma predecible y analizable.@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.list_resources() (para URIs concretas) o list_resource_templates() (para las con plantilla), luego lee una URI específica con read_resource(uri).config://, notes://, docs://) es arbitraria y elegida por usted; no es un protocolo de red real, solo un espacio de nombres.| 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 |
# 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")| 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 |
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.docs://../../etc/passwd puede 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.json.dumps(...) para cualquier cosa con más de un campo.try/finally (como se muestra arriba) o un gestor de contexto de conexión.| 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 |
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.
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.
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.
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.
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.
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.
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.
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é.
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.
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.
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.
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.
Revisado por Chris St. John·Última actualización: 16 jul 2026