Módulo 4: Resources — contexto y datos
Mini-proyecto: las políticas de Reservo como resources
Descripción
Las siete lecciones anteriores construyeron el módulo pieza por pieza: qué es un resource, cómo se descubre (resources/list), cómo se diseña su URI, cómo se lee (resources/read), sus dos formatos de contenido (text/blob), y cuándo elegirlo sobre una tool. Este mini-proyecto te pide juntar todo en un solo servidor y un solo cliente, con tus propias manos, y verificarlo con un checklist programático — no solo leyendo el código, confirmándolo con assert.
Es la base literal sobre la que se para el Módulo 5: cuando ahí agregues prompts/list/prompts/get, lo vas a hacer extendiendo un servidor que ya sabe hacer el handshake (M2) y resolver resources (M4) — exactamente como este mini-proyecto lo deja.
Conexión con el módulo
Cierra el Módulo 4 poniendo en tus manos, de punta a punta, las tres piezas que ya viste ejecutadas por separado: el catálogo (resources/list, lección 03), la lectura por URI (resources/read, lección 05), y el manejo del error cuando la URI no existe (-32002, también lección 05). Si algo de este mini-proyecto te cuesta, es la señal más clara de qué lección de las siete anteriores conviene repasar antes de seguir al Módulo 5.
El objetivo
Construir (o completar, si partes del servidor del Módulo 2) reservo_mcp_server.py y reservo_mcp_client.py que, entre los dos, completen: el handshake (initialize/notifications/initialized, del Módulo 2), resources/list (devolviendo los dos resources ancla de Reservo), y resources/read (leyendo cualquiera de los dos por su URI, y devolviendo el error correcto si se pide una URI que no existe). Nombres y versión fijos de siempre: servidor reservo-mcp-server, cliente reservo-mcp-client, ambos version: "1.0.0", protocolVersion: "2025-06-18".
Restricciones del mini-proyecto:
- El servidor debe declarar
resourcesen suscapabilitiesdeinitialize(ya lo hacía desde el Módulo 2, junto contoolsyprompts— no cambia). - Los dos resources ancla, exactos:
reservo://policies/cancellation-policyyreservo://policies/membership-tiers, ambosmimeType: "text/markdown". resources/readcon unaurique no está en el catálogo debe responder-32002 Resource not found, condata.uriindicando cuál falló — no una excepción de Python sin controlar, no un mensaje silencioso.stdoutsigue siendo territorio exclusivo del servidor para mensajes JSON-RPC — la regla dura del Módulo 2 no se negocia.- IDs con
itertools.count(1), nuncarandom/uuid.
Solución de referencia: el servidor completo
# reservo_mcp_server.py
"""Servidor MCP de Reservo: handshake (M2) + resources (M4).
Los 2 resources de politicas de Reservo se sirven por URI fija, sin busqueda.
Tools (M3) y prompts (M5) no se implementan en este servidor -- el foco de M4 es resources."""
import sys
import json
PROTOCOL_VERSION = "2025-06-18"
SERVER_INFO = {"name": "reservo-mcp-server", "version": "1.0.0"}
CANCELLATION_POLICY_TEXT = """# Cancellation Policy
Reservo bookings can be cancelled free of charge up to 24 hours before the reserved start time. Cancellations made within this window are refunded in full, with no penalty applied to the booking's account.
Cancellations made less than 24 hours before the start time are considered late cancellations. A late cancellation forfeits 50% of the quoted price; the remaining balance is refunded to the original payment method within 3-5 business days.
No-shows -- bookings that are never cancelled and never checked in -- forfeit the full quoted price. Reservo does not distinguish between a no-show and a same-day cancellation made after the reserved start time has already passed.
Pro members receive one exception per calendar month: a single late cancellation (within the 24-hour window) can be waived on request, refunded in full instead of the standard 50% penalty. Basic members do not have this exception available.
Cancellations are processed through the same channel used to make the original booking. There is no cancellation fee beyond the percentage forfeited under this policy -- Reservo does not charge a separate administrative fee.
"""
MEMBERSHIP_TIERS_TEXT = """# Membership Tiers
Reservo offers two membership tiers: `basic` and `pro`. Both tiers can book any room type (Focus, Studio, Boardroom) at the standard hourly rate quoted by `get_quote`.
`basic` membership has no monthly fee and no discount. Every quote is charged at the room's full hourly rate, with no adjustment applied.
`pro` membership includes a 20% discount on every quote, applied automatically -- the discount does not need to be requested and is never combined with any other promotion. A Focus room quoted at 2500 cents/hour for a basic member is quoted at 2000 cents/hour for a pro member (`2500 * 80 // 100`, always rounded down to the nearest whole cent).
`pro` membership also includes the late-cancellation exception described in the cancellation policy: one late cancellation per calendar month can be waived and refunded in full instead of forfeiting 50% of the quoted price.
Upgrading from `basic` to `pro` (or downgrading back) takes effect on the next booking made after the change -- it does not retroactively adjust bookings already confirmed under the previous tier.
"""
RESOURCES = {
"reservo://policies/cancellation-policy": {
"uri": "reservo://policies/cancellation-policy",
"name": "cancellation-policy",
"description": "Reservo's cancellation window and refund rules",
"mimeType": "text/markdown",
"text": CANCELLATION_POLICY_TEXT,
},
"reservo://policies/membership-tiers": {
"uri": "reservo://policies/membership-tiers",
"name": "membership-tiers",
"description": "Basic vs Pro membership tiers and the Pro discount",
"mimeType": "text/markdown",
"text": MEMBERSHIP_TIERS_TEXT,
},
}
def send(message: dict) -> None:
"""UN mensaje JSON-RPC por linea en stdout. Nunca un print() suelto aqui."""
sys.stdout.write(json.dumps(message) + "\n")
sys.stdout.flush()
def log(text: str) -> None:
"""Logs SIEMPRE a stderr -- stdout es exclusivo para mensajes MCP validos."""
print(text, file=sys.stderr, flush=True)
def handle_initialize(msg_id, params: dict) -> dict:
client_version = params.get("protocolVersion")
if client_version is None:
return {
"jsonrpc": "2.0",
"id": msg_id,
"error": {"code": -32602, "message": "Invalid params: falta protocolVersion"},
}
log(f"[server] initialize <- cliente {params.get('clientInfo')}, protocolVersion={client_version}")
return {
"jsonrpc": "2.0",
"id": msg_id,
"result": {
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {"tools": {}, "resources": {}, "prompts": {}},
"serverInfo": SERVER_INFO,
},
}
def handle_resources_list(msg_id) -> dict:
log(f"[server] resources/list -> {len(RESOURCES)} resources")
return {
"jsonrpc": "2.0",
"id": msg_id,
"result": {
"resources": [
{
"uri": resource["uri"],
"name": resource["name"],
"description": resource["description"],
"mimeType": resource["mimeType"],
}
for resource in RESOURCES.values()
]
},
}
def handle_resources_read(msg_id, params: dict) -> dict:
uri = params.get("uri")
resource = RESOURCES.get(uri)
if resource is None:
log(f"[server] resources/read <- uri={uri!r} NO ENCONTRADO")
return {
"jsonrpc": "2.0",
"id": msg_id,
"error": {"code": -32002, "message": "Resource not found", "data": {"uri": uri}},
}
log(f"[server] resources/read <- uri={uri}")
return {
"jsonrpc": "2.0",
"id": msg_id,
"result": {
"contents": [
{"uri": resource["uri"], "mimeType": resource["mimeType"], "text": resource["text"]}
]
},
}
def main() -> None:
log("[server] arrancando, esperando mensajes por stdin...")
for raw_line in sys.stdin:
line = raw_line.strip()
if not line:
continue
message = json.loads(line)
method = message.get("method")
msg_id = message.get("id")
params = message.get("params", {})
if method == "initialize":
send(handle_initialize(msg_id, params))
elif method == "notifications/initialized":
log("[server] notifications/initialized <- el cliente confirma que ya puede operar")
elif method == "resources/list":
send(handle_resources_list(msg_id))
elif method == "resources/read":
send(handle_resources_read(msg_id, params))
elif msg_id is not None:
send({
"jsonrpc": "2.0",
"id": msg_id,
"error": {"code": -32601, "message": f"Method not found: {method}"},
})
else:
log(f"[server] notificacion desconocida ignorada: {method}")
if __name__ == "__main__":
main()
Solución de referencia: el cliente completo
# reservo_mcp_client.py
"""Cliente MCP de Reservo: handshake (M2) + resources/list + resources/read (M4).
Ejecuta el ciclo completo por stdio real (subprocess.Popen + pipes)."""
import subprocess
import sys
import json
import itertools
PROTOCOL_VERSION = "2025-06-18"
CLIENT_INFO = {"name": "reservo-mcp-client", "version": "1.0.0"}
request_ids = itertools.count(1)
def send(proc: subprocess.Popen, message: dict) -> None:
line = json.dumps(message)
print(f"[client -> server] {line}")
proc.stdin.write(line + "\n")
proc.stdin.flush()
def recv(proc: subprocess.Popen) -> dict:
line = proc.stdout.readline().strip()
print(f"[server -> client] {line}")
return json.loads(line)
def main() -> None:
proc = subprocess.Popen(
[sys.executable, "reservo_mcp_server.py"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
# 1) initialize -> notifications/initialized (handshake de M2)
send(proc, {
"jsonrpc": "2.0",
"id": next(request_ids),
"method": "initialize",
"params": {
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": CLIENT_INFO,
},
})
init_response = recv(proc)
assert "resources" in init_response["result"]["capabilities"]
send(proc, {"jsonrpc": "2.0", "method": "notifications/initialized"})
# 2) resources/list -- descubrir que resources ofrece el servidor
send(proc, {"jsonrpc": "2.0", "id": next(request_ids), "method": "resources/list"})
list_response = recv(proc)
resources = list_response["result"]["resources"]
print(f"[client] descubri {len(resources)} resources:")
for resource in resources:
print(f" - {resource['uri']} ({resource['mimeType']})")
# 3) resources/read -- leer cada resource descubierto, uno por uno
for resource in resources:
send(proc, {
"jsonrpc": "2.0",
"id": next(request_ids),
"method": "resources/read",
"params": {"uri": resource["uri"]},
})
read_response = recv(proc)
content = read_response["result"]["contents"][0]
print(f"[client] {content['uri']} -> {len(content['text'])} caracteres de {content['mimeType']}")
proc.stdin.close()
proc.wait(timeout=5)
print("---- stderr del server (logs, nunca mensajes MCP) ----")
print(proc.stderr.read().rstrip())
if __name__ == "__main__":
main()
Qué esperar (ejecutando python3.14 reservo_mcp_client.py):
[client -> server] {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "reservo-mcp-client", "version": "1.0.0"}}}
[server -> client] {"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "2025-06-18", "capabilities": {"tools": {}, "resources": {}, "prompts": {}}, "serverInfo": {"name": "reservo-mcp-server", "version": "1.0.0"}}}
[client -> server] {"jsonrpc": "2.0", "method": "notifications/initialized"}
[client -> server] {"jsonrpc": "2.0", "id": 2, "method": "resources/list"}
[server -> client] {"jsonrpc": "2.0", "id": 2, "result": {"resources": [{"uri": "reservo://policies/cancellation-policy", "name": "cancellation-policy", "description": "Reservo's cancellation window and refund rules", "mimeType": "text/markdown"}, {"uri": "reservo://policies/membership-tiers", "name": "membership-tiers", "description": "Basic vs Pro membership tiers and the Pro discount", "mimeType": "text/markdown"}]}}
[client] descubri 2 resources:
- reservo://policies/cancellation-policy (text/markdown)
- reservo://policies/membership-tiers (text/markdown)
[client -> server] {"jsonrpc": "2.0", "id": 3, "method": "resources/read", "params": {"uri": "reservo://policies/cancellation-policy"}}
[server -> client] {"jsonrpc": "2.0", "id": 3, "result": {"contents": [{"uri": "reservo://policies/cancellation-policy", "mimeType": "text/markdown", "text": "# Cancellation Policy\n\nReservo bookings can be cancelled free of charge up to 24 hours before the reserved start time. Cancellations made within this window are refunded in full, with no penalty applied to the booking's account.\n\nCancellations made less than 24 hours before the start time are considered late cancellations. A late cancellation forfeits 50% of the quoted price; the remaining balance is refunded to the original payment method within 3-5 business days.\n\nNo-shows -- bookings that are never cancelled and never checked in -- forfeit the full quoted price. Reservo does not distinguish between a no-show and a same-day cancellation made after the reserved start time has already passed.\n\nPro members receive one exception per calendar month: a single late cancellation (within the 24-hour window) can be waived on request, refunded in full instead of the standard 50% penalty. Basic members do not have this exception available.\n\nCancellations are processed through the same channel used to make the original booking. There is no cancellation fee beyond the percentage forfeited under this policy -- Reservo does not charge a separate administrative fee.\n"}]}}
[client] reservo://policies/cancellation-policy -> 1170 caracteres de text/markdown
[client -> server] {"jsonrpc": "2.0", "id": 4, "method": "resources/read", "params": {"uri": "reservo://policies/membership-tiers"}}
[server -> client] {"jsonrpc": "2.0", "id": 4, "result": {"contents": [{"uri": "reservo://policies/membership-tiers", "mimeType": "text/markdown", "text": "# Membership Tiers\n\nReservo offers two membership tiers: `basic` and `pro`. Both tiers can book any room type (Focus, Studio, Boardroom) at the standard hourly rate quoted by `get_quote`.\n\n`basic` membership has no monthly fee and no discount. Every quote is charged at the room's full hourly rate, with no adjustment applied.\n\n`pro` membership includes a 20% discount on every quote, applied automatically -- the discount does not need to be requested and is never combined with any other promotion. A Focus room quoted at 2500 cents/hour for a basic member is quoted at 2000 cents/hour for a pro member (`2500 * 80 // 100`, always rounded down to the nearest whole cent).\n\n`pro` membership also includes the late-cancellation exception described in the cancellation policy: one late cancellation per calendar month can be waived and refunded in full instead of forfeiting 50% of the quoted price.\n\nUpgrading from `basic` to `pro` (or downgrading back) takes effect on the next booking made after the change -- it does not retroactively adjust bookings already confirmed under the previous tier.\n"}]}}
[client] reservo://policies/membership-tiers -> 1097 caracteres de text/markdown
---- stderr del server (logs, nunca mensajes MCP) ----
[server] arrancando, esperando mensajes por stdin...
[server] initialize <- cliente {'name': 'reservo-mcp-client', 'version': '1.0.0'}, protocolVersion=2025-06-18
[server] notifications/initialized <- el cliente confirma que ya puede operar
[server] resources/list -> 2 resources
[server] resources/read <- uri=reservo://policies/cancellation-policy
[server] resources/read <- uri=reservo://policies/membership-tiers
Nueve mensajes JSON-RPC en total (cinco requests/notifications salientes del cliente, cuatro responses del servidor — notifications/initialized no genera response), y el bloque final de stderr confirma que el servidor procesó cada uno en el orden correcto, sin que ningún log se filtrara a stdout en el medio.
Cómo verificar tu propia versión
Si escribiste tu propia versión antes de mirar la solución de referencia (recomendado), este checklist te confirma, con código, que tu servidor es correcto — no solo "se ve bien":
# checklist.py -- corre esto contra TU version de reservo_mcp_server.py
import subprocess, sys, json, itertools
ids = itertools.count(1)
proc = subprocess.Popen(
[sys.executable, "reservo_mcp_server.py"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, bufsize=1,
)
def send(message):
proc.stdin.write(json.dumps(message) + "\n")
proc.stdin.flush()
def recv():
return json.loads(proc.stdout.readline())
send({"jsonrpc": "2.0", "id": next(ids), "method": "initialize",
"params": {"protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": {"name": "reservo-mcp-client", "version": "1.0.0"}}})
init_response = recv()
send({"jsonrpc": "2.0", "method": "notifications/initialized"})
send({"jsonrpc": "2.0", "id": next(ids), "method": "resources/list"})
list_response = recv()
resources = list_response["result"]["resources"]
send({"jsonrpc": "2.0", "id": next(ids), "method": "resources/read",
"params": {"uri": resources[0]["uri"]}})
read_response = recv()
send({"jsonrpc": "2.0", "id": next(ids), "method": "resources/read",
"params": {"uri": "reservo://policies/does-not-exist"}})
error_response = recv()
checks = {
"capabilities.resources presente en initialize": "resources" in init_response["result"]["capabilities"],
"resources/list devuelve exactamente 2 resources": len(resources) == 2,
"las 2 uri empiezan con reservo://policies/": all(r["uri"].startswith("reservo://policies/") for r in resources),
"las 2 tienen mimeType text/markdown": all(r["mimeType"] == "text/markdown" for r in resources),
"resources/read devuelve contents con 1 elemento": len(read_response["result"]["contents"]) == 1,
"el contents leido trae mismo uri pedido": read_response["result"]["contents"][0]["uri"] == resources[0]["uri"],
"uri inexistente devuelve error -32002": error_response["error"]["code"] == -32002,
}
for description, passed in checks.items():
print(f"{'OK ' if passed else 'FALLA'} -- {description}")
proc.stdin.close()
proc.wait(timeout=5)
Qué esperar (corriendo el checklist contra la solución de referencia):
OK -- capabilities.resources presente en initialize
OK -- resources/list devuelve exactamente 2 resources
OK -- las 2 uri empiezan con reservo://policies/
OK -- las 2 tienen mimeType text/markdown
OK -- resources/read devuelve contents con 1 elemento
OK -- el contents leido trae mismo uri pedido
OK -- uri inexistente devuelve error -32002
Si tu propia versión produce algún FALLA, ya sabes exactamente qué método revisar: si falla la primera, revisa handle_initialize; si fallan la segunda o tercera, revisa RESOURCES o handle_resources_list; si fallan las últimas tres, revisa handle_resources_read.
Errores comunes
-
Olvidar declarar
"resources": {}encapabilitiesdeinitialize. Aunque tu servidor implementeresources/list/resources/readcorrectamente, sicapabilitiesno incluye la claveresources, un cliente que respeta la negociación de capacidades del Módulo 2 podría, legítimamente, nunca intentar llamarlos — la declaración eninitializees la que habilita, conceptualmente, todo lo que viene después. -
Devolver una excepción de Python sin controlar cuando la
urino existe. UnRESOURCES[uri]directo (con corchetes, en vez de.get(uri)) lanzaKeyErrorsi la clave no está — y como ese código corre dentro del bucle principal del servidor, una excepción sin capturar puede tumbar el proceso completo. La solución de referencia usa.get(uri)y verificais Noneexplícitamente, exactamente para evitar este caso. -
Reordenar
RESOURCESesperando queresources/listmantenga siempre el mismo orden entre corridas distintas del servidor. Dentro de una misma corrida sí es estable (Python preserva el orden de inserción de un diccionario), pero no está garantizado por la especificación de MCP en general — un checklist robusto no debería depender de queresources[0]sea siemprecancellation-policyespecíficamente, solo de que sea alguno de los dos esperados. -
Escribir el checklist con
print()de "parece que funciona" en vez de comparaciones booleanas explícitas. El patrónchecks = {"descripción": condición_booleana, ...}de esta lección es preferible porque cada verificación es independiente y su resultado (True/False) queda explícito — más fácil de depurar que leer una transcripción larga buscando algo que "se vea mal".
Ejercicios
Ejercicio 1: Cuenta los mensajes (Fácil)
De los nueve mensajes JSON-RPC de la transcripción del "Qué esperar" de esta lección (contando tanto los que salen del cliente como los que salen del servidor), ¿cuántos son requests, cuántos son responses, y cuántos son notifications? Usa el vocabulario del Módulo 2 (lección 02).
Ver solución
Requests (4): initialize, resources/list, y los dos resources/read — cada uno con id y esperando response.
Responses (4): la respuesta a cada uno de los cuatro requests anteriores, cada una con el mismo id de su request correspondiente.
Notifications (1): notifications/initialized — sin id, sin response.
Total: 4 + 4 + 1 = 9 mensajes, que coincide con el conteo del "Qué esperar". Nota que cada request generó exactamente una response — ninguno quedó sin contestar, y la única notification de la transcripción no generó ninguna.
Ejercicio 2: Agrega una tercera política y extiende el checklist (Medio)
Agrega un tercer resource al catálogo, reservo://policies/support-hours (mimeType: "text/plain", un text corto de tu invención), y extiende el checklist.py de esta lección con una verificación nueva: que resources/list ahora devuelva exactamente 3 resources, no 2.
Ver solución
En el servidor:
RESOURCES["reservo://policies/support-hours"] = {
"uri": "reservo://policies/support-hours",
"name": "support-hours",
"description": "Reservo customer support hours",
"mimeType": "text/plain",
"text": "Reservo support is available Monday to Friday, 9am to 6pm (local time).",
}
En el checklist, cambiar la verificación existente:
checks["resources/list devuelve exactamente 2 resources"] = len(resources) == 2
por:
checks["resources/list devuelve exactamente 3 resources"] = len(resources) == 3
Salida esperada, con el resto de las verificaciones sin cambios (la verificación de mimeType text/markdown para "las 2" seguiría pasando si se ajusta a resources[:2], ya que la nueva no es markdown):
OK -- capabilities.resources presente en initialize
OK -- resources/list devuelve exactamente 3 resources
...
Explicación: este ejercicio confirma en código lo que la lección 03 ya mostró — agregar un resource al catálogo es tan simple como agregar una entrada al diccionario RESOURCES, sin tocar handle_resources_list ni handle_resources_read. El checklist, al depender de len(resources) en vez de un número fijo hardcodeado en varios lugares, se ajusta con un solo cambio.
Ejercicio 3: Un cliente que solo lee resources con mimeType conocido (Difícil)
Escribe una función read_if_supported(proc, ids, uri, supported_mime_types) que llame resources/list primero, encuentre el resource con esa uri, y solo llame resources/read si su mimeType está en supported_mime_types; si no está, devuelva None sin siquiera intentar leerlo. Si la uri no aparece en el catálogo, también debería devolver None. Pruébala con supported_mime_types = {"text/markdown"} contra las dos políticas de Reservo (deberían leerse) y contra una uri inventada (no debería leerse).
Ver solución
def read_if_supported(proc, ids, uri: str, supported_mime_types: set[str]) -> str | None:
proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": next(ids), "method": "resources/list"}) + "\n")
proc.stdin.flush()
resources = json.loads(proc.stdout.readline())["result"]["resources"]
matching = next((r for r in resources if r["uri"] == uri), None)
if matching is None:
print(f"[client] {uri} no esta en el catalogo -- no se intenta leer")
return None
if matching["mimeType"] not in supported_mime_types:
print(f"[client] {uri} tiene mimeType {matching['mimeType']!r}, no soportado -- no se lee")
return None
proc.stdin.write(json.dumps({
"jsonrpc": "2.0", "id": next(ids), "method": "resources/read", "params": {"uri": uri},
}) + "\n")
proc.stdin.flush()
response = json.loads(proc.stdout.readline())
return response["result"]["contents"][0]["text"]
# Uso:
ids = itertools.count(1)
# ... proc con handshake ya hecho ...
text = read_if_supported(proc, ids, "reservo://policies/cancellation-policy", {"text/markdown"})
print("leido:", text[:30] if text else None, "...")
nothing = read_if_supported(proc, ids, "reservo://policies/does-not-exist", {"text/markdown"})
print("resultado con uri inexistente:", nothing)
Salida esperada:
leido: # Cancellation Policy
Reser ...
[client] reservo://policies/does-not-exist no esta en el catalogo -- no se intenta leer
resultado con uri inexistente: None
Explicación: este patrón —filtrar por mimeType antes de llamar resources/read, usando la información que ya trajo resources/list— evita hacer una llamada de más cuando de antemano sabes que no vas a poder procesar el resultado (por ejemplo, un cliente que solo sabe renderizar Markdown y quiere ignorar silenciosamente cualquier resource image/png sin intentar leerlo). Es la misma idea de "usa los metadatos del catálogo para decidir antes de pagar el costo de la operación completa" que sostiene por qué resources/list devuelve mimeType en primer lugar, en vez de forzarte a leer cada resource para descubrir su tipo.
Resumen y siguiente paso
- Construiste, con tus propias manos (o verificaste la solución de referencia), un servidor MCP que extiende el handshake del Módulo 2 con
resources/listyresources/readcompletos, sirviendo las dos políticas ancla de Reservo. - Un checklist programático —siete verificaciones booleanas, no solo lectura visual de la transcripción— confirma que tu implementación cumple la especificación: capabilities, catálogo, lectura, y el error
-32002ante una URI desconocida. - El servidor de este mini-proyecto es la base literal sobre la que el Módulo 5 va a agregar
prompts/list/prompts/get— mismo patrón exacto: una estructura de datos nueva, una función de manejo nueva, una rama más en el dispatch demain().
Con esto cierra el Módulo 4. Tienes, ejecutado y verificado por tus propias manos, el segundo primitivo completo de MCP: cómo se descubre un catálogo de datos (resources/list), cómo se diseña su identidad (URIs con esquema propio), cómo se lee su contenido (resources/read, en text o blob), y cuándo elegirlo sobre una tool de búsqueda. Todo esto corriendo sobre exactamente el mismo transporte stdio y el mismo handshake que construiste en el Módulo 2 — sin cambiar una sola de sus reglas.
Siguiente módulo: Módulo 5 — Prompts: plantillas reutilizables. El servidor de Reservo suma su tercer y último primitivo: plan_booking, una plantilla que el usuario elige activar para que el asistente revise la política de cancelación y pida una cotización antes de confirmar una reserva — prompts/list y prompts/get, los primeros mensajes de ese primitivo, ejecutados sobre este mismo servidor.
Recursos adicionales
- Model Context Protocol — Specification 2025-06-18: Resources — La especificación completa que este mini-proyecto implementó de punta a punta:
resources/list,resources/read, y el error-32002. - Model Context Protocol — Specification 2025-06-18: Base Protocol — El handshake y el transporte stdio del Módulo 2, reutilizados sin cambios en este servidor.
- Python —
subprocess— La base de todo el cliente ejecutado en este mini-proyecto. - Python —
itertools.count— El generador determinista de IDs, consistente con el resto de la guía.