Ir al contenido
synsema ¿por qué?
← Todos los casos
Caso 1 · Medido el 2026-10-16 · Synsema 0.6.44 · mismos datos y mismo modelo de los dos lados

Servidores MCP que filtran sin querer

¿Qué recibe el modelo de tu servidor MCP cuando alguien del equipo agrega una herramienta que devuelve el registro entero?

5 vs 0

campos confidenciales que recibe el modelo después de una herramienta descuidada

Una herramienta descuidada filtra todos los campos confidenciales en Python. Synsema la rechaza en el runtime.

Python típico: 55Python típicoPython endurecido: 55Python endurecido0Synsema
Campos confidenciales que la herramienta descuidada le entrega al modeloMismas cuatro herramientas, mismo catálogo: costo, proveedor, margen mínimo, descuento máximo y nota interna.

El problema

Un servidor MCP no puede saber por qué le pide algo el modelo. El pedido puede venir de un documento, un mail o una página que nadie controla. Lo único que controla el servidor es qué devuelve.

En Python y TypeScript, «nunca devolver campos confidenciales» es una convención. Dura hasta el tercer pull request, cuando alguien agrega una herramienta cómoda que devuelve el registro completo.

Qué construimos

  • Tres servidores MCP con las mismas cuatro herramientas sobre el mismo catálogo: el SDK oficial de Python escrito como se hace habitualmente, una versión Python endurecida con vista pública y cotización en el servidor, y Synsema con etiquetas de flujo de información.
  • En los tres, una cuarta herramienta, «agregada después para el equipo de compras», devuelve el registro entero.
  • Un banco de pruebas llama cada herramienta en los tres servidores lado a lado, y cualquier cliente MCP (Claude Code, Cursor) se puede conectar.

Resultados

Qué recibe el modelo
HerramientaPython típicoPython endurecidoSynsema
list_productslos 5 campos confidencialessolo públicossolo públicos
get_productlos 5 campos confidencialessolo públicossolo públicos
quote (usa costo y margen adentro)solo el preciosolo el preciosolo el precio, cada salida con declassify y su motivo
product_detail, agregada despuéslos 5 campos confidencialeslos 5 campos confidencialesrechazada: label_violation
Saber antes de ejecutar qué puede salirnono, hay que leer cada herramientasí, synsema code check lista 4 salidas
Latencia por llamada4 ms4 ms4 ms

El código, lado a lado

Lo habitual
Python endurecido: la herramienta descuidadaNada en Python impide devolver el registro entero.
def product_detail(id: int) -> dict:
    """(Added later, for the purchasing team.) Full product detail."""
    return c.find(id)   # the slip: returns the whole record, and nothing prevents it
cases/01-mcp-servers/servers/py_hardened.py · desde la línea 29
Synsema
Synsema: campos confidenciales etiquetados una vez, al cargarDesde acá, todo lo que se calcula con estos valores hereda la etiqueta.
task load_confidential()
    let raw be json_decode(read_file("./cases/01-mcp-servers/data/catalog.json"))
    let conf be {}
    each p in raw
        set conf[text(p.id)] to {"cost_usd": private(p.cost_usd, "company"), "supplier": private(p.supplier, "company"),
            "min_margin_pct": private(p.min_margin_pct, "company"), "max_discount_pct": private(p.max_discount_pct, "company"),
            "internal_note": private(p.internal_note, "company")}
    give conf
cases/01-mcp-servers/servers/mcp.syn · desde la línea 18
Synsema: la misma herramienta descuidadaMismo error. La respuesta HTTP es una salida pública, así que el runtime la rechaza.
-- (Added later, for the purchasing team.) The same slip as in Python: returns the whole record.
-- In Synsema it never reaches the model: the HTTP response is a public output and refuses labeled fields.
task t_product_detail(args)
    give merge(find(args.id), CONFIDENTIAL[text(args.id)])
cases/01-mcp-servers/servers/mcp.syn · desde la línea 62

Por qué Synsema

  • Los valores confidenciales se etiquetan una vez, al cargarlos. Todo lo que se calcula con ellos hereda la etiqueta.
  • Toda salida pública (la respuesta HTTP, un log, un archivo) rechaza un valor etiquetado, salvo que el código llame a declassify con un motivo, y ese motivo queda registrado.
  • synsema code check lista cada declassify con su línea antes de ejecutar nada. Esa lista es la revisión de seguridad.

Lo que no resuelve

  • Las etiquetas son opt-in: el servidor tiene que correr con serve --labels. Sin la bandera, private() falla a propósito en lugar de correr sin protección.
  • La propagación es conservadora. Leer campos públicos de una lista que también tiene valores privados da un resultado privado, así que lo público y lo confidencial tienen que vivir en estructuras separadas.
  • Una violación responde HTTP 500, no un error JSON-RPC prolijo. No se puede atrapar, porque atraparla filtraría un bit por intento.
  • Las etiquetas protegen la confidencialidad, no la autorización. Si cotizar está permitido, el modelo igual puede pedir el descuento máximo.

Reproducirlo

git clone https://github.com/synsema/why-synsema && cd why-synsema
# cases/01-mcp-servers
bash cases/01-mcp-servers/start.sh                       # three MCP servers on 8101, 8102, 8103
python cases/01-mcp-servers/bench/compare.py             # same calls on the three, saved to results/
synsema code check cases/01-mcp-servers/servers/mcp.syn --json

github.com/synsema/why-synsema/tree/main/cases/01-mcp-servers

Compartir