Módulo 7: Conectar con Claude Code y servidores de terceros
La frontera de confianza de un servidor
Descripción
La lección 05 mostró que .mcp.json no tiene ningún campo para distinguir un servidor de confianza de uno que no lo es — el mecanismo de registro es idéntico para los dos. Esta lección responde la pregunta que queda pendiente: si el archivo no te protege, ¿qué es exactamente lo que estás confiando cuando agregas una entrada nueva a mcpServers? La respuesta depende del transporte, y es más concreta de lo que parece a primera vista: un servidor stdio recibe ejecución de código en tu propia máquina, con tus propios permisos de sistema operativo; un servidor http recibe tus datos, viajando hacia un proceso que opera otra persona. Nombrar esto con precisión —sin construir todavía ninguna defensa— es el objetivo completo de esta lección.
Conexión con el módulo
Esta lección prepara el terreno para la 07, que se enfoca en un caso específico y más agudo de esta frontera: el texto que un servidor devuelve (la description de una tool, el contenido de un resource) como dato que cruza esa misma frontera, sin que el protocolo lo marque de ninguna forma especial.
Qué le das exactamente a un servidor stdio: ejecución de código local
Cuando registras una entrada type: "stdio", el host —Claude Code, o el subprocess.Popen que tú mismo escribiste en los Módulos 2 a 6— lanza un proceso nuevo del sistema operativo, con el command y los args exactos que especificaste. Ese proceso corre con los mismos permisos que tú tienes en tu sesión: puede leer cualquier archivo al que tú tengas acceso, escribir en cualquier directorio donde tú puedas escribir, iniciar conexiones de red si el código lo decide, y ejecutar cualquier otro programa que su código elija ejecutar. Nada en el protocolo MCP —ni en .mcp.json— restringe alguna de estas capacidades: MCP define cómo se habla con el proceso (JSON-RPC sobre stdin/stdout), no qué le está permitido hacer con los recursos del sistema mientras corre.
Esto no es una falla de diseño de MCP — es, simplemente, lo que significa lanzar un subproceso en cualquier sistema operativo, con o sin MCP de por medio. reservo_full_mcp_server.py nunca lee ningún archivo fuera de sí mismo ni hace ninguna conexión de red — lo sabes porque escribiste cada línea. unitconvert-mcp-server, de la lección anterior, tampoco lo hace en la versión que viste — pero eso lo sabes porque leíste su código completo, no porque el protocolo te lo garantice. Un servidor de terceros que no leíste, técnicamente, podría hacer cualquiera de esas cosas, y tools/list/tools/call seguirían funcionando exactamente igual desde afuera — el protocolo no tiene forma de detectarlo ni de impedirlo.
Qué le das exactamente a un servidor http: tus datos, a un operador remoto
Con type: "http", no hay ningún proceso que lanzas tú — el servidor ya está corriendo, en una máquina que no controlas, operada por quien sea que lo publicó. Lo que le das, en este caso, es distinto: cada request que tu host manda —incluidos los arguments de cada tools/call, y cualquier header que hayas configurado, típicamente un token de autenticación— viaja por la red hasta ese servidor remoto. El operador de ese servidor ve cada petición que le mandas, con todos los datos que contiene. Si el header incluye Authorization: Bearer <tu-token>, ese token —y cualquier cosa que ese token permita hacer del otro lado— ahora es algo que el operador remoto también posee, no solo tú.
La diferencia con stdio no es "uno es más peligroso que el otro en abstracto" —la lección 04 ya desarmó esa jerarquía falsa— es que exponen riesgos de naturaleza distinta: stdio pone en juego lo que ese proceso puede hacer en tu máquina; HTTP pone en juego lo que ese operador remoto puede ver de lo que le mandas.
Ejemplo trabajado: nombrar el riesgo de cada entrada de un .mcp.json real
# trust_grant.py
"""Explica, para cada entrada de .mcp.json, que le estas confiando exactamente
a ese servidor. NO implementa ninguna defensa -- eso es agent-security-and-
sandboxing-guide. Esta funcion solo NOMBRA el riesgo segun el transporte,
para que la decision de registrar (o no) un servidor se tome con informacion
completa."""
import json
with open("mcp_config_three.json", "r", encoding="utf-8") as file:
config = json.load(file)
def describe_trust_grant(name: str, entry: dict) -> str:
if entry["type"] == "stdio":
return (f"{name}: ejecucion de codigo LOCAL, con TUS permisos de sistema operativo "
f"(comando: {entry['command']} {' '.join(entry['args'])}). "
f"Puede leer/escribir cualquier archivo al que tú tengas acceso, "
f"hacer conexiones de red si quiere, sin que MCP se lo impida.")
if entry["type"] == "http":
header_keys = list(entry.get("headers", {}).keys())
return (f"{name}: tus REQUESTS y cualquier dato que mandes viajan a {entry['url']}, "
f"operado por un tercero. Encabezados enviados en cada llamada: {header_keys or 'ninguno'}.")
return f"{name}: type desconocido, no se puede describir el riesgo"
for server_name, entry in config["mcpServers"].items():
print(describe_trust_grant(server_name, entry))
Contra un .mcp.json con las tres entradas que ya conoces de este módulo —Reservo (stdio), unitconvert-mcp-server (stdio) y un hipotético reservo-cloud-mcp-server (http, de la lección 04)—:
Qué esperar:
reservo-mcp-server: ejecucion de codigo LOCAL, con TUS permisos de sistema operativo (comando: python3.14 reservo_full_mcp_server.py). Puede leer/escribir cualquier archivo al que tú tengas acceso, hacer conexiones de red si quiere, sin que MCP se lo impida.
unitconvert-mcp-server: ejecucion de codigo LOCAL, con TUS permisos de sistema operativo (comando: python3.14 -m unitconvert_mcp_server). Puede leer/escribir cualquier archivo al que tú tengas acceso, hacer conexiones de red si quiere, sin que MCP se lo impida.
reservo-cloud-mcp-server: tus REQUESTS y cualquier dato que mandes viajan a https://mcp.reservo.example.invalid/v1, operado por un tercero. Encabezados enviados en cada llamada: ['Authorization'].
Nota que describe_trust_grant le aplica el mismo texto de riesgo a reservo-mcp-server y a unitconvert-mcp-server — la función no sabe, ni tiene forma de saber, que una es tuya y la otra no. Esa es, otra vez, la lección central de este módulo: el riesgo estructural (qué podría hacer un proceso stdio) es el mismo para los dos; lo que cambia es cuánta certeza tienes tú de que, en la práctica, no lo hace — y esa certeza viene de haber leído el código (Reservo) o de no haberlo leído (unitconvert-mcp-server, si fuera un paquete real que instalaste sin auditar).
El límite de esta lección: nombrar, no defender
Esta lección, a propósito, no construye ninguna forma de limitar lo que un servidor stdio puede hacer, ni de verificar que un servidor remoto sea quien dice ser. Eso —sandboxing de la ejecución (correr el subproceso con permisos restringidos, en un contenedor o una jaula del sistema operativo), allowlists de servidores aprobados, permisos granulares por tool, defensas específicas contra inyección de instrucciones vía la salida de un tool o el texto de un resource, gestión segura de credenciales remotas— es el contenido completo de agent-security-and-sandboxing-guide. Esta guía, y este módulo en particular, se detienen exactamente en el borde: nombrar con precisión qué se le confía a un servidor MCP, para que sepas qué pregunta hacerle a esa guía cuando llegue el momento de construir la defensa real.
Errores comunes
-
Pensar que MCP "sandboxea" automáticamente los servidores stdio. No lo hace, y no fue diseñado para hacerlo — el protocolo especifica comunicación (JSON-RPC sobre stdio/HTTP), no aislamiento de ejecución. Cualquier restricción de lo que un proceso stdio puede hacer tiene que venir de una capa externa (el sistema operativo, un contenedor, una política del host) — nunca del protocolo MCP en sí mismo.
-
Confiar más en HTTP "porque tiene autenticación" sin pensar en qué protege esa autenticación. Un
headercon un token demuestra, ante el servidor remoto, que eres quien dices ser — no dice nada sobre si ese servidor remoto es honesto con lo que hace con tus datos una vez que los recibe. Autenticación e integridad del operador son dos preguntas distintas. -
Aplicar el riesgo de stdio a un servidor que en realidad corre remoto, o viceversa. El riesgo depende estrictamente de
type, como confirmadescribe_trust_grantde esta lección — mezclar los dos lleva a preocuparte por el problema equivocado (por ejemplo, revisar credenciales de un servidor stdio que nunca hace peticiones HTTP, en vez de revisar qué archivos locales podría tocar). -
Creer que "no leí el código, pero tampoco pasó nada raro" es evidencia de que un servidor es seguro. No lo es — la ausencia de un incidente observado no confirma la ausencia de riesgo, solo confirma que, hasta ahora, no lo notaste. La lección 07 muestra, ejecutado, un caso donde "todo funciona perfectamente, sin ningún error de protocolo" convive con una intención maliciosa completa.
Ejercicios
Ejercicio 1: Clasifica el riesgo de tres escenarios (Fácil)
Para cada uno, decide si el riesgo principal es "ejecución de código local" o "exposición de datos a un operador remoto":
A) Un servidor `stdio` de terceros que instalaste sin leer su código.
B) Un servidor `http` que manda tu historial completo de conversación como contexto en cada request.
C) Un servidor `stdio` que tú mismo escribiste, como reservo_full_mcp_server.py.
Ver solución
- A) Ejecución de código local — el riesgo estructural de cualquier
stdio, agravado aquí porque no auditaste el código: no sabes con certeza qué hace ese proceso con tus permisos de sistema operativo. - B) Exposición de datos a un operador remoto — el riesgo estructural de
http: cualquier dato que ese servidor reciba en el request (en este caso, todo un historial de conversación) queda visible para quien opera ese servidor remoto. - C) Ejecución de código local, con riesgo prácticamente nulo en la práctica — sigue siendo, estructuralmente, un proceso
stdiocon tus permisos, pero como escribiste cada línea tú mismo, tienes certeza completa de qué hace. El riesgo estructural existe igual; lo que cambia es tu nivel de certeza sobre si se materializa.
Ejercicio 2: Extiende describe_trust_grant con una advertencia de credenciales (Medio)
Modifica describe_trust_grant para que, cuando una entrada stdio tenga un env no vacío, agregue una frase adicional advirtiendo que esas variables de entorno —si contienen credenciales— también quedan expuestas al proceso que arranca. Pruébalo contra una entrada hipotética con env: {"API_KEY": "secret-value"}.
Ver solución
def describe_trust_grant(name: str, entry: dict) -> str:
if entry["type"] == "stdio":
base = (f"{name}: ejecucion de codigo LOCAL, con TUS permisos de sistema operativo "
f"(comando: {entry['command']} {' '.join(entry['args'])}). "
f"Puede leer/escribir cualquier archivo al que tú tengas acceso, "
f"hacer conexiones de red si quiere, sin que MCP se lo impida.")
env = entry.get("env", {})
if env:
base += f" ADEMAS recibe estas variables de entorno: {list(env.keys())} -- si alguna es una credencial, este proceso ahora la tiene."
return base
if entry["type"] == "http":
header_keys = list(entry.get("headers", {}).keys())
return (f"{name}: tus REQUESTS y cualquier dato que mandes viajan a {entry['url']}, "
f"operado por un tercero. Encabezados enviados en cada llamada: {header_keys or 'ninguno'}.")
return f"{name}: type desconocido"
print(describe_trust_grant("example-server", {
"type": "stdio", "command": "python3.14", "args": ["server.py"],
"env": {"API_KEY": "secret-value"},
}))
Salida esperada:
example-server: ejecucion de codigo LOCAL, con TUS permisos de sistema operativo (comando: python3.14 server.py). Puede leer/escribir cualquier archivo al que tú tengas acceso, hacer conexiones de red si quiere, sin que MCP se lo impida. ADEMAS recibe estas variables de entorno: ['API_KEY'] -- si alguna es una credencial, este proceso ahora la tiene.
Explicación: este ejercicio extiende el mismo principio de la lección a un caso más específico: env en una entrada stdio no solo configura al proceso, también le entrega cualquier valor que pongas ahí — si ese valor es una credencial de verdad (una API key, un token de base de datos), el proceso stdio la recibe con el mismo nivel de acceso que le diste a todo lo demás. La advertencia no evita el riesgo (esta lección no construye defensas) — solo lo hace explícito antes de que decidas agregar la entrada.
Ejercicio 3: Diseña una pregunta de evaluación por transporte (Difícil)
Sin escribir código todavía —solo razonamiento, en tus palabras— propón UNA pregunta concreta que te harías antes de registrar un servidor stdio nuevo, y UNA pregunta distinta que te harías antes de registrar un servidor http nuevo, cada una apuntando directamente al riesgo específico de ese transporte (no una pregunta genérica de "¿es seguro?"). Justifica por qué la pregunta de stdio no serviría para evaluar un servidor http, y viceversa.
Ver solución
Para stdio: "¿Leí el código de este servidor, o al menos confío en quien lo mantiene lo suficiente como para no leerlo yo mismo?" — apunta directo al riesgo de ejecución de código local: la única forma de tener certeza sobre qué hace un proceso con tus permisos de sistema operativo es haber visto ese código, o confiar en la reputación de quien lo escribió.
Para http: "¿En quién confío para operar el servidor detrás de esta URL, y qué puede hacer con los datos que le mando en cada request?" — apunta al riesgo de exposición de datos: no importa cuánto audites el protocolo o la documentación, el operador remoto ve cada petición, y ninguna cantidad de lectura de código de tu lado cambia eso, porque el código que corre del otro lado de la URL no es algo que puedes leer en absoluto.
Por qué no son intercambiables: la pregunta de stdio no tiene sentido para http, porque en http no hay ningún código que instales ni ejecutes localmente — no hay nada que "leer" de tu lado, el servidor entero vive en la infraestructura de otra persona. Y la pregunta de http no tiene sentido para stdio, porque un servidor stdio no tiene un "operador" recibiendo tus datos en tiempo real de la misma forma —el riesgo ahí es qué hace el proceso en tu propia máquina, no a quién le mandas información por red. Cada transporte crea una superficie de riesgo distinta, y la pregunta de evaluación tiene que apuntar a esa superficie específica, no a una versión genérica de "¿confío en esto?".
Resumen y siguiente paso
- Un servidor
stdiorecibe ejecución de código local, con tus permisos de sistema operativo — puede leer/escribir cualquier archivo al que tengas acceso, sin que MCP se lo impida. - Un servidor
httprecibe tus datos —los argumentos de cada request, cualquier header de autenticación— en cada petición, viajando hacia un proceso operado por otra persona. .mcp.jsonno distingue entre estos riesgos automáticamente —describe_trust_grantde esta lección los nombra con código, pero no los previene: eso es explícitamente fuera del alcance de esta guía.- El endurecimiento real —sandboxing, allowlists, permisos granulares, defensa contra inyección vía salida de tools— es contenido completo de
agent-security-and-sandboxing-guide. Esta lección deja el vocabulario exacto para llegar ahí con la pregunta correcta.
Siguiente lección: 07 — Cuándo un servidor no es confiable. El caso más específico y más peligroso de esta frontera: el texto que un servidor devuelve —description de una tool, contenido de un resource— como dato no confiable, ejecutado con una versión comprometida de unitconvert-mcp-server.
Recursos adicionales
- Model Context Protocol — Specification 2025-06-18 — La especificación base: define comunicación, no aislamiento de ejecución — la base de por qué MCP no "sandboxea" nada por sí mismo.
- Claude Code — Model Context Protocol (MCP) — Cómo Claude Code configura credenciales (
env,headers) para servidores registrados. - Python —
subprocess— El mecanismo real detrás de "ejecución de código local": un proceso hijo con los mismos permisos que el proceso padre. - Python —
json— La lectura de.mcp.jsonque alimentadescribe_trust_granten esta lección.