Plantillas de URI y seguridad de rutas
Traducción automática
Esta página se tradujo automáticamente a partir de la documentación en inglés, y la página en inglés es la versión de referencia. Si algo no se lee bien, Traducciones explica cómo avisarnos.
Esta es la referencia de la sintaxis de plantillas de URI que acepta
@mcp.resource y de la
política de seguridad de rutas que el SDK aplica a los valores extraídos. Para una
introducción a qué son los recursos y cuándo usarlos, empieza por
Recursos; esta página supone que ya te sientes cómodo declarando un
recurso y quieres el conjunto completo de operadores, los ajustes de seguridad o la
conexión con la capa de bajo nivel.
La sintaxis de plantillas es RFC 6570.
El SDK admite un subconjunto elegido para hacer coincidir las URI entrantes de
resources/read, más una capa de seguridad que rechaza los valores que se resolverían
fuera del directorio que pretendes servir. Para los detalles a nivel de protocolo
(formatos de mensaje, ciclo de vida, paginación) consulta la
especificación de recursos de MCP.
El conjunto completo de operadores
El marcador simple, {user_id}, es el que presenta Recursos. Hay cuatro
formas de operador más; aquí están en un solo servidor para que puedas verlas una junto a
otra:
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
BOOKS = {
"978-0441172719": {"title": "Dune", "author": "Frank Herbert"},
"978-0553293357": {"title": "Foundation", "author": "Isaac Asimov"},
}
MANUALS = {
"printing/setup.md": "# Printer setup\n\nLoad paper, then power on.",
"returns.md": "# Returns policy\n\nThirty days with a receipt.",
}
@mcp.resource("books://{isbn}")
def get_book(isbn: str) -> dict[str, str]:
"""A single book by ISBN."""
return BOOKS[isbn]
@mcp.resource("orders://{order_id}")
def get_order(order_id: int) -> dict[str, object]:
"""An order by its numeric id."""
return {"order_id": order_id, "next_order": order_id + 1, "status": "shipped"}
@mcp.resource("manuals://{+path}")
def read_manual(path: str) -> str:
"""A staff manual page. The path keeps its slashes."""
return MANUALS[path]
@mcp.resource("reviews://{isbn}{?limit,sort}")
def list_reviews(isbn: str, limit: int = 10, sort: str = "newest") -> str:
"""Reviews of a book, optionally limited and sorted."""
return f"{limit} {sort} reviews of {BOOKS[isbn]['title']}"
@mcp.resource("shelves://browse{/path*}")
def browse_shelf(path: list[str]) -> str:
"""A shelf in the category tree, addressed by segments."""
return " > ".join(["catalog", *path])
Cada decorador resaltado es una forma distinta de dividir la URI. Las secciones siguientes los recorren de arriba abajo.
Expansión simple: {name}
books://{isbn} es la forma simple, la de todos los días. El marcador se asigna al
parámetro isbn, así que un cliente que lee books://978-0441172719 llama a
get_book("978-0441172719").
Un {name} simple se detiene en la primera /. books://978/extra no
coincide porque la barra después de 978 termina la captura y /extra
sobra.
Conversión de tipos
Los valores extraídos llegan como cadenas, pero puedes declarar un tipo más específico
y el SDK los convierte. orders://{order_id} llega a una función
cuyo parámetro es order_id: int, así que leer orders://12345 llama a
get_order(12345), no a get_order("12345"). El handler hace
aritmética con él (order_id + 1) sin conversión explícita.
Rutas de varios segmentos: {+name}
Para capturar un valor que contiene barras, usa {+name}. Con
manuals://{+path}:
manuals://returns.mddapath = "returns.md"manuals://printing/setup.mddapath = "printing/setup.md"
Recurre a {+name} siempre que el valor sea jerárquico: rutas del sistema de
archivos, claves de objetos anidados, rutas de URL que estés redirigiendo como proxy.
Parámetros de consulta: {?a,b,c}
reviews://{isbn}{?limit,sort} pone limit y sort después del ?.
La ruta identifica qué libro; la consulta ajusta cómo lo lees.
Los parámetros de consulta se comparan con flexibilidad: el orden no importa, los
sobrantes se ignoran y los omitidos caen en los valores por defecto de tu función. Así que
reviews://978-0441172719 usa limit=10, sort="newest", y
reviews://978-0441172719?sort=top sobrescribe solo sort.
Segmentos de ruta como lista: {/name*}
Si quieres cada segmento de ruta como un elemento separado de una lista en lugar de una
sola cadena con barras, usa {/name*}. Con shelves://browse{/path*}, un
cliente que lee shelves://browse/fiction/sci-fi llama a
browse_shelf(["fiction", "sci-fi"]).
Referencia de plantillas
Los patrones más comunes:
| Patrón | Entrada de ejemplo | Obtienes |
|---|---|---|
{name} |
alice |
"alice" |
{name} |
docs/intro.md |
no coincide (se detiene en /) |
{+path} |
docs/intro.md |
"docs/intro.md" |
{.ext} |
.json |
"json" |
{/segment} |
/v2 |
"v2" |
{?key} |
?key=value |
"value" |
{?a,b} |
?a=1&b=2 |
"1", "2" |
{/path*} |
/a/b/c |
["a", "b", "c"] |
Lo que rechaza el analizador
Algunas formas de plantilla se detectan desde el principio en lugar de fallar en la
primera solicitud. @mcp.resource analiza la plantilla cuando se ejecuta el
decorador, así que ninguna de estas llega nunca a un servidor en ejecución.
UriTemplate.parse() lanza InvalidUriTemplate en estos casos:
- Dos variables sin nada entre ellas.
manuals://{+path}{ext}se rechaza: la comparación no puede saber dónde terminapathy dónde empiezaext. Pon un literal entre ellas (manuals://{+path}/{ext}) o usa un operador que aporte su propio delimitador.manuals://{+path}{.ext}se acepta porque{.ext}aporta el.por sí mismo. - Más de una variable de varios segmentos. Como máximo una entre
{+var},{#var}o una variable expandida ({/var*},{.var*},{;var*}) por plantilla. Dos son intrínsecamente ambiguas: no hay una forma fundamentada de decidir cuál de ellas absorbe un segmento adicional. - Los errores de sintaxis habituales: una llave sin cerrar, un nombre de variable usado
dos veces o una característica de RFC 6570 que el SDK no admite, como el
modificador de prefijo
{var:3}o la expansión de consulta{?vars*}.
Además de eso, @mcp.resource lanza ValueError cuando un parámetro del
handler está vinculado a una variable de consulta en el tramo final
{?...}/{&...} de la plantilla pero no tiene valor por defecto en Python. Esas variables se
comparan con flexibilidad (un cliente puede omitir cualquiera de ellas), así que un parámetro
sin valor por defecto solo aparecería como un error interno opaco en la
primera solicitud que lo omita. reviews://{isbn}{?limit,sort} en el
servidor de arriba es la versión bien formada: tanto limit como sort tienen
valores por defecto.
Seguridad
Los parámetros de plantilla vienen del cliente. Si llegan a operaciones del sistema de
archivos o de base de datos sin comprobar, valores como ../../etc/passwd pueden
resolverse fuera del directorio que pretendías servir.
Lo que el SDK comprueba por defecto
Antes de que se ejecute tu handler, el SDK rechaza cualquier parámetro que:
- escaparía de su directorio de partida mediante componentes
.. - parezca una ruta absoluta (
/etc/passwd,C:\Windows) o una ruta relativa a unidad de Windows (C:foo). Un valor relativo a unidad y un identificador con espacio de nombres comox:yson indistinguibles como cadenas, así que cualquier valor de una sola letra seguida de dos puntos se rechaza por defecto; exime el parámetro si recibe legítimamente ese tipo de valores - contenga un byte nulo (
\x00)
La comprobación de .. se basa en componentes, no en buscar subcadenas. Valores como
v1.0..v2.0 o HEAD~3..HEAD pasan porque ahí .. no es un segmento de ruta
independiente.
Estas comprobaciones se aplican al valor decodificado, así que detectan el recorrido de
directorios sin importar cómo se codificó en la URI (../etc, ..%2Fetc,
%2E%2E/etc, ..%5Cetc, %00: todos se detectan).
Check
Lee manuals://../etc/passwd en el servidor de arriba y la solicitud
se rechaza sin más: la comparación de plantillas se detiene en el primer fallo,
así que no se prueba ninguna plantilla posterior (potencialmente más permisiva) como
alternativa. El cliente ve el mismo error -32602 "Unknown resource"
que vería con una URI que no coincide con ninguna plantilla, y
read_manual nunca se ejecuta.
Handlers del sistema de archivos: usa safe_join
Las comprobaciones integradas detienen los casos comunes, pero no pueden conocer el límite
de tu entorno aislado. Para acceder al sistema de archivos, usa safe_join para resolver la
ruta y verificar que se mantiene dentro de tu directorio base:
from pathlib import Path
from mcp.server import MCPServer
from mcp.shared.path_security import safe_join
mcp = MCPServer("Bookshop")
DOCS_ROOT = Path("./manuals")
@mcp.resource("manuals://{+path}")
def read_manual(path: str) -> str:
"""A staff manual page, served from a directory on disk."""
return safe_join(DOCS_ROOT, path).read_text()
safe_join detecta escapes mediante enlaces simbólicos, secuencias .. y trucos con rutas
absolutas que una simple comprobación de cadenas pasaría por alto. Si la ruta resuelta
escapa de DOCS_ROOT, lanza PathEscapeError, que le llega al
cliente como un ResourceError.
Cuando los valores por defecto estorban
A veces las comprobaciones bloquean valores legítimos. Una herramienta de importación de
catálogos podría recibir intencionadamente una ruta absoluta, o un parámetro podría ser una
referencia relativa como ../sibling que tu handler interpreta con
seguridad sin tocar el sistema de archivos. Exime ese parámetro o relaja
la política para todo el servidor:
from mcp.server import MCPServer
from mcp.server.mcpserver import ResourceSecurity
mcp = MCPServer("Bookshop")
@mcp.resource(
"imports://preview/{+source}",
security=ResourceSecurity(exempt_params={"source"}),
)
def preview_import(source: str) -> str:
"""Preview a catalog import. `source` may be an absolute path."""
return f"Would import from {source}"
relaxed = MCPServer(
"Bookshop",
resource_security=ResourceSecurity(reject_path_traversal=False),
)
@relaxed.resource("imports://preview/{+source}")
def preview_import_relaxed(source: str) -> str:
"""The server-wide flag exempts every resource on `relaxed`."""
return f"Would import from {source}"
security=ResourceSecurity(exempt_params={"source"})en el decorador omite las comprobaciones para ese único parámetro en ese único recurso. El resto del servidor mantiene la política por defecto.resource_security=en el constructor deMCPServerfija el valor por defecto para todos los recursos. Aquírelaxeddesactiva por completo la comprobación de...
Las comprobaciones configurables:
| Ajuste | Por defecto | Qué hace |
|---|---|---|
reject_path_traversal |
True |
Rechaza secuencias .. que escapan del directorio de partida |
reject_absolute_paths |
True |
Rechaza /foo, C:\foo, rutas UNC y la ruta relativa a unidad C:foo (también detecta x:y) |
reject_null_bytes |
True |
Rechaza valores que contienen \x00 |
exempt_params |
vacío | Nombres de parámetros para los que se omiten las comprobaciones |
Estas comprobaciones son un prefiltro heurístico; para el acceso al sistema de archivos,
safe_join sigue siendo el límite de contención.
Tip
Si tu handler no puede satisfacer la solicitud (el archivo no existe, el id es desconocido), lanza una excepción. El SDK la convierte en una respuesta de error. Consulta Manejo de errores para ver la diferencia entre un error de protocolo y un error de herramienta.
Recursos en el Server de bajo nivel
Si construyes sobre el Server de bajo nivel (consulta El Server de bajo
nivel), registras directamente los handlers para los métodos de protocolo
resources/list y resources/read. No hay decorador; devuelves
tú mismo los tipos del protocolo.
Recursos estáticos
Para URI fijas, mantén un registro y despacha por coincidencia exacta:
from mcp.server import Server, ServerRequestContext
from mcp.types import (
ListResourcesResult,
PaginatedRequestParams,
ReadResourceRequestParams,
ReadResourceResult,
Resource,
TextResourceContents,
)
RESOURCES = {
"config://shop": '{"currency": "USD", "tax_rate": 0.08}',
"status://health": "ok",
}
async def list_resources(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListResourcesResult:
return ListResourcesResult(resources=[Resource(name=uri, uri=uri) for uri in RESOURCES])
async def read_resource(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:
if (text := RESOURCES.get(params.uri)) is not None:
return ReadResourceResult(contents=[TextResourceContents(uri=params.uri, text=text)])
raise ValueError(f"Unknown resource: {params.uri}")
server = Server("Bookshop", on_list_resources=list_resources, on_read_resource=read_resource)
El handler de listado les dice a los clientes qué hay disponible; el handler de lectura sirve el contenido. Comprueba primero tu registro, pasa a las plantillas (más abajo) si tienes alguna y luego lanza una excepción para cualquier otra cosa.
Plantillas
El motor de plantillas que usa MCPServer vive en mcp.shared.uri_template
y funciona por sí solo. Obtienes el mismo análisis y la misma comparación; el
enrutamiento y la política de seguridad los conectas tú mismo.
from mcp.server import Server, ServerRequestContext
from mcp.shared.path_security import contains_path_traversal, is_absolute_path
from mcp.shared.uri_template import UriTemplate
from mcp.types import (
ListResourceTemplatesResult,
PaginatedRequestParams,
ReadResourceRequestParams,
ReadResourceResult,
ResourceTemplate,
TextResourceContents,
)
TEMPLATES = {
"manuals": UriTemplate.parse("manuals://{+path}"),
"books": UriTemplate.parse("books://{isbn}"),
}
MANUALS = {"printing/setup.md": "# Printer setup", "returns.md": "# Returns policy"}
BOOKS = {"978-0441172719": "Dune by Frank Herbert"}
def read_manual_safely(path: str) -> str:
if contains_path_traversal(path) or is_absolute_path(path):
raise ValueError("rejected")
return MANUALS[path]
async def read_resource(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:
if (matched := TEMPLATES["manuals"].match(params.uri)) is not None:
text = read_manual_safely(str(matched["path"]))
return ReadResourceResult(contents=[TextResourceContents(uri=params.uri, text=text)])
if (matched := TEMPLATES["books"].match(params.uri)) is not None:
text = BOOKS[str(matched["isbn"])]
return ReadResourceResult(contents=[TextResourceContents(uri=params.uri, text=text)])
raise ValueError(f"Unknown resource: {params.uri}")
async def list_resource_templates(
ctx: ServerRequestContext, params: PaginatedRequestParams | None
) -> ListResourceTemplatesResult:
return ListResourceTemplatesResult(
resource_templates=[
ResourceTemplate(name=name, uri_template=str(template)) for name, template in TEMPLATES.items()
]
)
server = Server(
"Bookshop",
on_read_resource=read_resource,
on_list_resource_templates=list_resource_templates,
)
En las líneas resaltadas ocurren tres cosas:
- Analiza una vez, compara en cada solicitud.
UriTemplate.parse()construye la plantilla;template.match(uri)devuelve las variables extraídas como undict, oNonesi la URI no encaja. La decodificación de URL ocurre dentro dematch(); los valores decodificados se devuelven tal cual, sin validación de seguridad de rutas. Los valores salen como cadenas: conviértelos tú mismo (int(matched["id"]),Path(matched["path"])). - Aplica tú mismo las comprobaciones de seguridad. Las comprobaciones de
..y de rutas absolutas queMCPServerejecuta por defecto viven enmcp.shared.path_security.read_manual_safelylas llama antes de tocarMANUALS. Si un parámetro no es una ruta del sistema de archivos (un ISBN, una consulta de búsqueda), omite las comprobaciones para ese valor: controlas la política por handler en lugar de hacerlo mediante un objeto de configuración. - Lista las plantillas desde la misma fuente. Los clientes descubren
las plantillas mediante
resources/templates/list.str(template)devuelve la cadena original de la plantilla, así que el listado y el comparador comparten una única fuente de verdad.
Resumen
{name}coincide con un segmento;{+name}conserva las barras;{?a,b}toma de la cadena de consulta;{/name*}divide los segmentos en una lista.- Dos variables sin nada entre ellas, o una segunda variable de varios
segmentos, se rechazan al analizar. Un parámetro vinculado a una variable de consulta
final
{?...}/{&...}debe declarar un valor por defecto en Python. - Anota el parámetro (
order_id: int) y el SDK convierte. - La política de seguridad por defecto rechaza
.., rutas absolutas y bytes nulos antes de que se ejecute tu handler; sobrescríbela por recurso consecurity=ResourceSecurity(...)o para todo el servidor conresource_security=. - Para el acceso al sistema de archivos,
safe_joines el límite de contención. - En el
Serverde bajo nivel, analiza conUriTemplate.parse(), compara con.match()y aplicamcp.shared.path_securitytú mismo.