Módulo 4: Resources — contexto y datos
URIs de resource y el esquema `reservo://`
Descripción
Cada resource de esta guía empieza igual: reservo://policies/.... Esta lección explica de dónde sale ese prefijo, qué significa cada parte, y por qué diseñar tu propio esquema de URI es una decisión legítima —y hasta recomendada— al construir un servidor MCP, en vez de reusar http:// o file:// por comodidad.
Conexión con el módulo
La lección 03 ya te mostró resources/list devolviendo estas URIs como si fueran obvias. Esta lección abre esa caja: qué es una URI, por qué MCP no impone un esquema fijo, y cómo se diseña uno propio con sentido. La lección 05 (resources/read) va a usar exactamente estas URIs como el único argumento que necesita para leer un documento — entender su estructura ahora hace que esa lección sea pura mecánica, sin sorpresas.
Qué es una URI (y por qué MCP la eligió)
URI significa Uniform Resource Identifier — un identificador con una sintaxis estándar (definida en el RFC 3986, el mismo estándar detrás de http://, mailto:, ftp://) para nombrar algo de forma única. La estructura general es:
esquema://autoridad/camino
| | |
reservo policies /cancellation-policy
MCP eligió URIs para identificar resources por una razón simple: es un estándar ya resuelto, con reglas claras de sintaxis, que cualquier cliente ya sabe parsear (todos los lenguajes modernos traen una librería de URIs en su stdlib — en Python, urllib.parse). No tuvo que inventar un formato de identificador propio, igual que no tuvo que inventar JSON-RPC para el formato de mensajes (Módulo 2).
Lo importante: la especificación de MCP no exige ningún esquema específico. http://, file://, git://, o un esquema completamente inventado como reservo:// son todos igualmente válidos — el servidor decide qué esquema usar, y el cliente no necesita "reconocer" el esquema de antemano para poder leerlo: solo necesita mandar la URI completa, tal cual la recibió de resources/list, de vuelta en resources/read.
Anatomía de reservo://policies/cancellation-policy
Vamos a parsear las URIs de Reservo con la librería estándar de Python, y comparar contra otros esquemas conocidos:
# parse_uris.py
from urllib.parse import urlparse
uris = [
"reservo://policies/cancellation-policy",
"reservo://policies/membership-tiers",
"https://example.com/docs/page",
"file:///project/README.md",
]
for uri in uris:
parsed = urlparse(uri)
print(uri)
print(f" scheme = {parsed.scheme!r}")
print(f" netloc = {parsed.netloc!r}")
print(f" path = {parsed.path!r}")
print()
Qué esperar:
reservo://policies/cancellation-policy
scheme = 'reservo'
netloc = 'policies'
path = '/cancellation-policy'
reservo://policies/membership-tiers
scheme = 'reservo'
netloc = 'policies'
path = '/membership-tiers'
https://example.com/docs/page
scheme = 'https'
netloc = 'example.com'
path = '/docs/page'
file:///project/README.md
scheme = 'file'
netloc = ''
path = '/project/README.md'
Fíjate en el paralelo exacto: en https://example.com/docs/page, example.com es el netloc (la "autoridad", típicamente un dominio) y /docs/page es el path. En reservo://policies/cancellation-policy, Reservo usa policies como netloc —una categoría en vez de un dominio— y /cancellation-policy como path —el identificador exacto dentro de esa categoría—. urllib.parse.urlparse no necesita saber nada especial sobre reservo:// para parsearlo correctamente: la sintaxis de URI es genérica, y cualquier esquema nuevo hereda gratis el mismo parseo.
Compáralo también con file:///project/README.md: nota las tres barras (file:// + /project/...) — eso ocurre porque file:// no usa netloc (no hay "autoridad" para un archivo local), así que el netloc queda vacío y el path arranca inmediatamente con /. reservo:// sí usa netloc (policies), por eso no necesita esa tercera barra.
Diseñando tu propio esquema: los tres segmentos que importan
El esquema de Reservo se diseñó con esta forma:
reservo://<categoría>/<identificador>
| | |
nombre grupo temático documento exacto
del
servidor
- El esquema (
reservo) identifica de qué servidor viene el resource — útil cuando un cliente conversa con varios servidores MCP a la vez (Módulo 6) y necesita distinguir de un vistazo si una URI le pertenece a Reservo o a otro servidor conectado. - La categoría (
policies) agrupa resources relacionados. Si Reservo creciera y agregara, por ejemplo, resources sobre disponibilidad de salas, podrías usarreservo://rooms/...como una segunda categoría, sin chocar conreservo://policies/.... - El identificador (
cancellation-policy,membership-tiers) es el nombre exacto del documento dentro de esa categoría — corto, legible, enkebab-case(el mismo estilo que ya usas para nombres de tools en Reservo).
Esta convención (esquema propio + categoría + identificador) no es un requisito de la especificación —MCP solo exige que la URI sea válida según el RFC 3986—, pero es una práctica sólida: hace que cualquiera que lea el catálogo de resources/list pueda adivinar la estructura del resto sin necesidad de leer la documentación completa del servidor.
Por qué no reusar http:// o file://
Podrías, técnicamente, exponer las políticas de Reservo con URIs como https://reservo.internal/policies/cancellation o file:///data/policies/cancellation.md. La razón para no hacerlo es la confusión que genera un esquema que ya tiene un significado establecido en el ecosistema:
- Un cliente que ve
https://...podría razonablemente asumir que puede hacer una petición HTTP real a esa URL — y en el caso de Reservo, no hay ningún servidor HTTP escuchando ahí. Sería una URI válida sintácticamente, pero engañosa semánticamente. - Un cliente que ve
file://...podría asumir que existe un archivo real en esa ruta del sistema de archivos local — y en Reservo, el texto de la política vive como un string en memoria del proceso servidor (CANCELLATION_POLICY_TEXTen el código de la lección 03), no como un archivo en disco.
Un esquema propio (reservo://) no hace ninguna promesa falsa: le dice a cualquiera que lo mire "esto es específico de Reservo, y la única forma correcta de leerlo es a través del protocolo MCP con resources/read — no lo trates como una URL real ni como una ruta de archivo".
Nota de producción: cómo registra un esquema el SDK oficial
En el SDK mcp (PyPI; pip install "mcp<2" para la línea legacy que enseña esta guía), declarar un resource con un esquema propio es tan simple como decorar una función con la URI exacta, algo parecido a:
# Codigo conceptual del SDK oficial -- NO se instala ni se ejecuta en esta guia.
@mcp.resource("reservo://policies/cancellation-policy")
def cancellation_policy() -> str:
return CANCELLATION_POLICY_TEXT
El SDK infiere mimeType a partir del tipo de retorno de la función (un str se mapea a text/plain por default, salvo que lo declares explícito) y arma automáticamente la entrada correspondiente en resources/list — el mismo trabajo que hace a mano el diccionario RESOURCES y la función handle_resources_list de la lección 03. El esquema (reservo://) sigue siendo una decisión tuya, no del SDK: la librería nunca valida ni restringe qué esquema eliges, exactamente como viste en el Ejercicio 3 de esta lección — solo te evita escribir el if method == "resources/list" a mano.
Errores comunes
-
Pensar que el
netlocde una URI de resource tiene que ser un dominio real. No. Enreservo://policies/cancellation-policy,policiesno resuelve a ninguna dirección de red — es solo un segmento de agrupación que el servidor definió.urlparselo coloca ennetlocporque sintácticamente ocupa esa posición (después de//, antes de la siguiente/), no porque tenga que comportarse como un dominio DNS. -
Modificar una URI antes de pasarla a
resources/read. La URI que recibiste deresources/listdebe pasarse exacta, byte a byte, aresources/read— ni recortada, ni con mayúsculas cambiadas, ni con un/de más o de menos. Es un identificador exacto, no un texto que se pueda "normalizar" por tu cuenta. -
Asumir que todos los servidores MCP usan el mismo esquema. Cada servidor elige el suyo — vas a ver esquemas completamente distintos entre servidores de terceros (algunos sí usan
file://legítimamente, para exponer archivos reales del sistema; otros inventan el propio, como Reservo). El Módulo 6 (varios servidores conectados a la vez) va a mostrar cómo un cliente maneja resources de esquemas distintos sin confundirlos. -
Confundir el esquema de una URI de resource con el
protocolVersionde MCP. Son conceptos completamente independientes — uno identifica un documento (reservo://...), el otro versiona el protocolo de mensajes ("2025-06-18"). No tienen ninguna relación entre sí.
Ejercicios
Ejercicio 1: Parsea tres URIs (Fácil)
Usando urllib.parse.urlparse, ¿cuáles son scheme, netloc y path de estas tres URIs? Resuélvelo mentalmente primero, después confírmalo ejecutando el código.
A) reservo://policies/support-hours
B) github://issues/1423
C) postgres://prod-db/orders
Ver solución
from urllib.parse import urlparse
for uri in ["reservo://policies/support-hours", "github://issues/1423", "postgres://prod-db/orders"]:
p = urlparse(uri)
print(uri, "->", p.scheme, "|", p.netloc, "|", p.path)
reservo://policies/support-hours -> reservo | policies | /support-hours
github://issues/1423 -> github | issues | /1423
postgres://prod-db/orders -> postgres | prod-db | /orders
- A)
scheme='reservo',netloc='policies',path='/support-hours'. - B)
scheme='github',netloc='issues',path='/1423'— un esquema hipotético que un servidor MCP de GitHub podría usar para exponer issues como resources. - C)
scheme='postgres',netloc='prod-db',path='/orders'— de nuevo hipotético: un servidor MCP sobre una base de datos podría exponer el esquema de una tabla como resource con un esquema así.
Ejercicio 2: Diseña el esquema de un nuevo servidor (Medio)
Estás diseñando un servidor MCP para una biblioteca interna de componentes de UI. Necesita exponer, como resources: la guía de estilo de colores (un documento fijo), y las especificaciones de tres componentes (Button, Modal, Table, cada uno un documento fijo separado). Diseña las URIs completas para las cuatro, siguiendo el patrón esquema://categoría/identificador de esta lección, y justifica tu elección de esquema y categorías.
Ver solución
uikit://guides/color-style-guide
uikit://components/button
uikit://components/modal
uikit://components/table
Justificación: uikit como esquema identifica que estos resources pertenecen a este servidor específico (igual que reservo identifica a Reservo) — corto, sin espacios, reconocible. Dos categorías distintas porque son dos tipos de contenido conceptualmente diferentes: guides para documentación general (la guía de estilo, que no es específica de ningún componente) y components para especificaciones puntuales de cada pieza de UI — así, si mañana se agrega una guía de accesibilidad, iría en uikit://guides/accessibility sin mezclarse con las especificaciones de componentes. El identificador de cada componente usa el mismo nombre que el componente en el código (button, modal, table, en minúsculas por convención de URI), para que sea trivial adivinar la URI de un componente nuevo sin tener que consultar resources/list primero.
Ejercicio 3: ¿Por qué la especificación no valida el esquema? (Difícil)
Un compañero de equipo propone que MCP debería exigir una lista cerrada de esquemas válidos (por ejemplo, solo permitir file://, https:// y git://), para evitar que cada servidor "invente" el suyo. Argumenta, desde el diseño de MCP como protocolo abierto (Módulo 1: el problema M×N), por qué esa restricción sería contraproducente.
Ver solución
Restringir los esquemas válidos reintroduciría, a otro nivel, el mismo problema M×N que MCP existe para resolver. Si la especificación exigiera una lista cerrada de esquemas, cada servidor tendría que forzar sus datos a encajar en uno de esos esquemas preexistentes aunque no le queden bien —por ejemplo, un servidor de base de datos tendría que fingir que sus tablas son "archivos" bajo file://, o un servidor de tickets de soporte tendría que fingir que sus tickets son URLs bajo https://—, generando exactamente la ambigüedad semántica que la sección "Por qué no reusar http:// o file://" de esta lección explica que hay que evitar.
El diseño real de MCP resuelve esto de otra forma: no restringe el conjunto de esquemas, sino que estandariza cómo se descubre y se lee cualquier esquema, sea cual sea. resources/list siempre devuelve el mismo formato de metadatos (uri/name/description/mimeType) sin importar qué esquema tenga la URI, y resources/read siempre recibe una URI completa y devuelve contents en el mismo formato (uri/mimeType/text o blob) sin importar el esquema. Un cliente MCP genérico nunca necesita "reconocer" reservo:// específicamente para poder leerlo — solo necesita hablar el protocolo. Eso es, en miniatura, la misma decisión de diseño M+N que motiva toda la guía: estandarizar el mecanismo de intercambio, no el contenido específico de cada servidor.
Resumen y siguiente paso
- Una URI de resource sigue la sintaxis estándar
esquema://autoridad/camino, parseable conurllib.parse.urlparsesin necesitar código especial por esquema. - MCP no exige un esquema fijo — cada servidor diseña el suyo. Reservo usa
reservo://<categoría>/<identificador>, ejecutado y parseado en esta lección. - Reusar
http://ofile://sin que exista de verdad un servidor HTTP o un archivo real es engañoso; un esquema propio evita esa promesa falsa. - La URI que devuelve
resources/listse pasa exacta aresources/read— no se modifica, no se normaliza, no se recorta.
Siguiente lección: 05 — resources/read. Con el esquema de URI ya entendido, este es el método que usa esa URI para traer el contenido completo del documento — ejecutado leyendo las dos políticas de Reservo, y el error -32002 provocado con una URI que no existe.
Recursos adicionales
- Model Context Protocol — Specification 2025-06-18: Resources — Confirma que la especificación no restringe el esquema de una URI de resource.
- RFC 3986 — Uniform Resource Identifier (URI): Generic Syntax — El estándar detrás de la sintaxis
esquema://autoridad/caminoque usa cualquier URI, incluidareservo://. - Python —
urllib.parse—urlparsey el resto de utilidades estándar para descomponer y construir URIs.