Módulo 4: Resources — contexto y datos
`mimeType`, `text` y `blob`
Descripción
Las dos políticas de Reservo son texto plano, así que hasta ahora solo viste el campo text de contents. Pero MCP contempla que un resource sea binario — una imagen, un PDF, un archivo de audio — y necesita una forma de transportar esos bytes dentro de un mensaje JSON, que solo sabe representar texto. Esta lección muestra el mecanismo completo: mimeType declara qué tipo de contenido es, y según ese tipo, contents trae text (para contenido legible) o blob (para contenido binario, codificado en base64).
Conexión con el módulo
Esta lección cierra el ciclo completo de resources/read que abrieron las lecciones 03 y 05: ya viste la mecánica del catálogo y la lectura por URI; esta lección profundiza en la forma exacta del contenido, que es el último detalle que faltaba antes de pasar al criterio de diseño de la lección 07 (cuándo un resource, cuándo una tool).
mimeType: qué tipo de contenido estás recibiendo
mimeType es un string estándar (definido por IANA, el mismo sistema de tipos que usa el header Content-Type de HTTP) que le dice al que recibe un resource cómo interpretarlo, sin tener que adivinarlo por la extensión del nombre. Algunos ejemplos:
text/plain -> texto sin formato
text/markdown -> texto con sintaxis Markdown (las 2 políticas de Reservo)
application/json -> un documento JSON
image/png -> una imagen PNG (binario)
application/pdf -> un documento PDF (binario)
audio/mpeg -> un archivo de audio MP3 (binario)
La regla que decide text vs blob no es "cuál mimeType prefieras" — es si el contenido es texto legible como string o datos binarios. text/markdown, text/plain, application/json (casi siempre) van en text. image/*, audio/*, application/pdf y en general cualquier formato binario van en blob.
Por qué el binario no puede ir directo en el JSON
JSON es, por diseño, un formato de texto. Un string en JSON solo puede contener caracteres válidos de texto (con los escapes que ya viste en el Módulo 2, como \n) — no puede contener bytes binarios arbitrarios, que podrían incluir secuencias que rompan la sintaxis del propio JSON o que ni siquiera representen texto válido en ningún encoding. La solución estándar —la misma que usan innumerables protocolos basados en JSON, no algo específico de MCP— es codificar los bytes binarios como un string de texto, usando Base64: un esquema que representa cualquier secuencia de bytes usando únicamente 64 caracteres imprimibles (letras, dígitos, +, /, y = para relleno). El resultado ya es texto legítimo, seguro de meter dentro de un string JSON.
import base64
raw_bytes = b"\x89PNG\r\n\x1a\n..." # bytes binarios reales
encoded = base64.b64encode(raw_bytes).decode("ascii") # -> string seguro para JSON
base64.b64encode recibe bytes y devuelve bytes (por eso el .decode("ascii"): Base64 solo produce caracteres ASCII, así que decodificar es seguro sin riesgo de un error de encoding). El resultado es un string más largo que los bytes originales —Base64 tiene un costo de tamaño de aproximadamente un 33%—, pero a cambio es transportable dentro de cualquier campo de texto JSON, sin excepciones.
Ejemplo trabajado: un resource de texto y uno binario, ejecutados
Este ejemplo usa un servidor de demostración aparte —no el reservo_mcp_server.py canónico de este módulo, que solo tiene las dos políticas en texto— con un segundo resource inventado para el caso: reservo://rooms/focus-floorplan.png, el plano de la sala Focus, como una imagen PNG real (un PNG transparente de 1×1 píxel, 67 bytes fijos — lo suficientemente chico para citarlo entero, pero un PNG válido de verdad, no datos inventados).
# blob_demo_server.py (fragmento relevante)
import base64
# 1x1 pixel PNG transparente -- bytes fijos y deterministicos (constante conocida, NUNCA random).
FLOORPLAN_PNG_BYTES = bytes.fromhex(
"89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
"0000000a49444154789c6360000002000100ff9f0d9e0000000049454e44ae426082"
)
def handle_resources_read(msg_id, params):
uri = params.get("uri")
resource = RESOURCES.get(uri)
if resource is None:
return {"jsonrpc": "2.0", "id": msg_id,
"error": {"code": -32002, "message": "Resource not found", "data": {"uri": uri}}}
if resource["kind"] == "text":
content = {"uri": resource["uri"], "mimeType": resource["mimeType"], "text": resource["text"]}
else:
encoded = base64.b64encode(resource["bytes"]).decode("ascii")
content = {"uri": resource["uri"], "mimeType": resource["mimeType"], "blob": encoded}
return {"jsonrpc": "2.0", "id": msg_id, "result": {"contents": [content]}}
Nota la rama: kind == "text" arma contents con el campo text; kind == "blob" codifica los bytes con base64.b64encode y arma contents con el campo blob en su lugar. Nunca los dos campos a la vez — es la misma regla de exclusión mutua que ya viste con result/error en el Módulo 2, aplicada aquí a text/blob.
El cliente lee ambos tipos de resource en la misma corrida:
# blob_demo_client.py (fragmento relevante)
send({"jsonrpc": "2.0", "id": next(ids), "method": "resources/read",
"params": {"uri": "reservo://policies/cancellation-policy"}})
text_response = recv()
text_content = text_response["result"]["contents"][0]
print(f"[client] contenido de texto -> mimeType={text_content['mimeType']!r}, campo presente={'text' in text_content}, campo 'blob' presente={'blob' in text_content}")
send({"jsonrpc": "2.0", "id": next(ids), "method": "resources/read",
"params": {"uri": "reservo://rooms/focus-floorplan.png"}})
blob_response = recv()
blob_content = blob_response["result"]["contents"][0]
print(f"[client] contenido blob -> mimeType={blob_content['mimeType']!r}, campo 'text' presente={'text' in blob_content}, campo 'blob' presente={'blob' in blob_content}")
print(f"[client] blob (base64, {len(blob_content['blob'])} caracteres): {blob_content['blob']}")
raw_bytes = base64.b64decode(blob_content["blob"])
print(f"[client] bytes decodificados: {len(raw_bytes)} bytes, primeros 8 en hex: {raw_bytes[:8].hex()}")
Qué esperar:
[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/read", "params": {"uri": "reservo://policies/cancellation-policy"}}
[server -> client] {"jsonrpc": "2.0", "id": 2, "result": {"contents": [{"uri": "reservo://policies/cancellation-policy", "mimeType": "text/markdown", "text": "# Cancellation Policy\n\n(texto completo en la leccion 05)\n"}]}}
[client] contenido de texto -> mimeType='text/markdown', campo presente=True, campo 'blob' presente=False
[client -> server] {"jsonrpc": "2.0", "id": 3, "method": "resources/read", "params": {"uri": "reservo://rooms/focus-floorplan.png"}}
[server -> client] {"jsonrpc": "2.0", "id": 3, "result": {"contents": [{"uri": "reservo://rooms/focus-floorplan.png", "mimeType": "image/png", "blob": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGNgAAACAAEA/58NngAAAABJRU5ErkJggg=="}]}}
[client] contenido blob -> mimeType='image/png', campo 'text' presente=False, campo 'blob' presente=True
[client] blob (base64, 92 caracteres): iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGNgAAACAAEA/58NngAAAABJRU5ErkJggg==
[client] bytes decodificados: 67 bytes, primeros 8 en hex: 89504e470d0a1a0a
La última línea es la que cierra el círculo: el cliente toma el string blob (92 caracteres de Base64), lo decodifica con base64.b64decode, y recupera exactamente los 67 bytes binarios originales del PNG — confirmado con el mismo prefijo hexadecimal (89504e470d0a1a0a, la firma estándar de cualquier archivo PNG válido) que tenía FLOORPLAN_PNG_BYTES del lado del servidor. El roundtrip completo —bytes → Base64 → JSON → Base64 → bytes— no pierde ni un byte.
text y blob en una tabla
CAMPO TIPO DE CONTENIDO EJEMPLO DE mimeType CÓMO SE TRANSPORTA
------------------------------------------------------------------------------------
text Texto legible como string text/plain, text/markdown, String JSON normal,
application/json sin codificar
blob Datos binarios image/png, application/pdf, String Base64
audio/mpeg (`base64.b64encode`)
Un elemento de contents trae siempre exactamente uno de los dos, nunca ambos, nunca ninguno. El mimeType es la pista de cuál esperar, pero la fuente de verdad es cuál campo está realmente presente en el JSON —tal como hizo el cliente de este ejemplo, comprobando con in en vez de asumir.
Errores comunes
-
Olvidar el
.decode("ascii")después debase64.b64encode.b64encodedevuelvebytes, nostr— intentar meter ese valor directo en un diccionario que después pasa porjson.dumpsproduce unTypeError: Object of type bytes is not JSON serializable. El.decode("ascii")es obligatorio antes de que el valor pueda viajar en un mensaje JSON-RPC. -
Mandar bytes binarios crudos dentro de
text, sin codificar. Esto rompería la serialización JSON de inmediato (json.dumpsfallaría al intentar serializarbytes, que no es un tipo JSON válido) — o peor, si se intenta forzar con.decode("utf-8", errors="replace"), corrompería silenciosamente los datos binarios, que dejarían de poder reconstruirse.blobcon Base64 existe exactamente para evitar este problema. -
Poner
textyblobjuntos "por si acaso". La especificación es explícita: son mutuamente excluyentes, la misma regla que ya viste conresult/error. Un cliente bien escrito no debería tener que manejar el caso de que ambos estén presentes — no es un estado válido. -
Asumir que
mimeTypeempieza context/implica siempre el campotext. Es la convención más común, pero no una regla absoluta de la especificación — lo que determina si algo va entextoblobes si el contenido es representable como texto legible, no el prefijo literal delmimeType. En la práctica, sin embargo, vas a encontrar que casi todos los servidores siguen esta convención (texto legible conmimeType text/*oapplication/jsonva entext; todo lo demás, enblob).
Ejercicios
Ejercicio 1: Clasifica cinco mimeType (Fácil)
Para cada uno, di si el contenido correspondiente iría en text o en blob:
A) text/csv
B) image/jpeg
C) application/json
D) application/octet-stream
E) text/html
Ver solución
- A)
text— un CSV es texto plano legible, con comas y saltos de línea. - B)
blob— una imagen JPEG es binaria. - C)
text— JSON es texto (aunque estructurado), representable directamente como string. - D)
blob—application/octet-streames, literalmente, el mimeType genérico para "datos binarios sin un tipo más específico" — siempre va enblob. - E)
text— HTML es texto con marcado, legible como string.
Ejercicio 2: Codifica y decodifica tus propios bytes (Medio)
Escribe un script que tome el string "Reservo" (codificado a bytes con .encode("utf-8")), lo codifique en Base64, imprima el resultado, y después lo decodifique de vuelta, confirmando con un assert que el resultado final coincide exactamente con el string original.
Ver solución
import base64
original_text = "Reservo"
original_bytes = original_text.encode("utf-8")
encoded = base64.b64encode(original_bytes).decode("ascii")
print("base64:", encoded)
decoded_bytes = base64.b64decode(encoded)
decoded_text = decoded_bytes.decode("utf-8")
print("decodificado:", decoded_text)
assert decoded_text == original_text
print("OK: el roundtrip no perdio ningun caracter")
Salida esperada:
base64: UmVzZXJ2bw==
decodificado: Reservo
OK: el roundtrip no perdio ningun caracter
Explicación: aunque este ejemplo usa texto (no binario de verdad), demuestra que Base64 es un mecanismo de codificación general para cualquier secuencia de bytes, sea texto o no. La razón por la que Reservo no codifica sus políticas de esta forma es simplemente que no hace falta — ya son texto legible, y json.dumps las serializa directamente sin necesitar el paso extra de Base64, que solo agrega tamaño sin ningún beneficio para contenido que ya es texto.
Ejercicio 3: ¿Por qué Base64 y no una codificación más eficiente? (Difícil)
Un compañero de equipo propone: "Base64 agrega un 33% de overhead de tamaño — ¿por qué no transportar el binario directo como bytes crudos por el pipe de stdio, ya que de todas formas estamos usando subprocess?". Explica por qué esa propuesta rompería el framing de la lección 03 del Módulo 2 (un mensaje por línea), y por qué Base64 —a pesar del overhead— sigue siendo la elección correcta para este transporte.
Ver solución
El framing de stdio (Módulo 2, lección 03) depende de una regla estricta: cada mensaje ocupa exactamente una línea, delimitada por \n, y el mensaje completo tiene que ser JSON válido, parseable con json.loads. Bytes binarios crudos podrían contener, en cualquier posición, la secuencia de byte que representa un salto de línea (0x0A) — y si eso pasara dentro de un campo que se intentara meter "tal cual" en medio de un mensaje JSON, el readline() del lado que lee cortaría el mensaje ahí mismo, en medio de datos binarios, exactamente el mismo problema de framing que la lección 03 del Módulo 2 mostró con saltos de línea reales dentro de texto sin escapar. Peor aún: bytes binarios arbitrarios ni siquiera son, en general, una secuencia UTF-8 válida — y el transporte stdio de esta guía usa text=True en subprocess.Popen, que asume que todo lo que viaja por los pipes se puede decodificar como texto.
Base64 resuelve ambos problemas de una vez: al codificar cualquier secuencia de bytes usando solo un conjunto fijo de 64 caracteres imprimibles (sin \n, sin bytes de control, sin nada fuera de ASCII), garantiza que el resultado sea (a) texto UTF-8 válido, compatible con text=True, y (b) libre de cualquier carácter que pudiera confundirse con el delimitador de línea del framing. El costo del 33% de overhead es el precio de mantener la simplicidad del transporte: un mensaje, una línea, siempre texto — la misma simplicidad que hizo que el framing de stdio fuera tan fácil de implementar a mano en el Módulo 2. Un protocolo binario "más eficiente" existiría (y de hecho, Streamable HTTP, el otro transporte de MCP fuera de alcance de esta guía, maneja algunos casos de forma distinta), pero cambiaría por completo el modelo de framing que este módulo y el anterior construyeron con cuidado.
Resumen y siguiente paso
mimeTypedeclara el tipo de contenido de un resource (text/markdown,image/png, etc.), siguiendo el estándar de tipos MIME de IANA.- Contenido de texto legible va en el campo
textdecontents; contenido binario va enblob, codificado en Base64 conbase64.b64encode(...).decode("ascii")— nunca ambos campos a la vez. - Base64 existe porque JSON solo transporta texto; codificar bytes binarios como texto ASCII seguro es la forma estándar de resolverlo, con un costo de tamaño de ~33%.
- Ejecutado de punta a punta: un resource de texto (
text/markdown) y uno binario (image/png, un PNG real de 67 bytes), con el roundtrip de Base64 confirmado byte a byte.
Siguiente lección: 07 — Cuándo un resource y cuándo una tool. Con el mecanismo completo de resources ya dominado (list, URIs, read, text/blob), esta lección cierra el módulo con el criterio de diseño: cuándo modelar algo como resource, y cuándo como tool de búsqueda — con la frontera exacta hacia production-rag-and-document-ingestion-guide.
Recursos adicionales
- Model Context Protocol — Specification 2025-06-18: Resources — La forma exacta de
contents, contextyblobcomo campos mutuamente excluyentes. - IANA Media Types — El registro oficial de tipos MIME, la fuente del valor exacto de
mimeType. - Python —
base64—b64encode/b64decode, usados en todo el ejemplo trabajado de esta lección. - Python —
bytes.fromhex/bytes.hex— Cómo se construyeron y verificaron los bytes fijos del PNG de demostración.