# Revisión de una herramienta MCP personalizada: lista práctica de comprobación de seguridad

Las herramientas MCP personalizadas merecen la misma revisión que dedicarías a una pequeña integración de producción con acceso a tu equipo, tu red y tus credenciales. Que un agente llame a la herramienta mediante MCP no reduce su autoridad. A menudo facilita que una autoridad mal delimitada se ejerza repetidamente.

He visto el mismo fallo una y otra vez: un desarrollador lee la descripción de una herramienta, ve un nombre útil como `deploy_preview` o `search_docs` y le concede acceso porque parece local. Después, la implementación convierte una cadena proporcionada por el modelo en una URL, un argumento de shell o una lectura recursiva de archivos. La herramienta útil nunca fue el límite de seguridad. Lo fueron la implementación y la ruta de la credencial.

La especificación del Model Context Protocol describe las herramientas como funciones que un servidor expone para que un cliente las descubra y las llame. También hace explícito un hecho incómodo: el modelo controla la ejecución de las herramientas. Un cliente puede incluir a una persona en el proceso de aprobación, pero quien escribe la herramienta debe asumir que los argumentos pueden ser inesperados, excesivos o dirigirse al destino equivocado. Revisa la herramienta antes de que el agente tenga ocasión de improvisar.

## Empieza por el mapa de autoridad, no por el README

Una herramienta MCP personalizada solo es aceptable cuando puedes dibujar un recorrido breve y concreto desde la solicitud del agente hasta el efecto que produce. Empieza por anotar qué puede leer la herramienta, adónde puede enviar datos, qué puede modificar y qué credencial o identidad del sistema operativo lo hace posible.

Hazlo antes de leer los detalles de la implementación. Así tendrás un criterio para juzgar el código, en lugar de dejar que una descripción agradable establezca el criterio. Una herramienta llamada `get_build_status` puede leer un archivo de configuración local, llamar a una API alojada, escribir una caché y ejecutar un ayudante de línea de comandos. Cada acción tiene un modo de fallo distinto.

Usa un mapa de autoridad pequeño como este:

| Parte | Registra | Por qué importa |
|---|---|---|
| Entrada del agente | Campos exactos de la herramienta y tamaños máximos | Muestra qué puede controlar el modelo |
| Lecturas locales | Rutas, variables de entorno y archivos de configuración | Revela la recopilación accidental de datos |
| Escrituras locales | Caché, espacio de trabajo, estado de git y archivos temporales | Detecta efectos secundarios persistentes |
| Red | Nombres de host, puertos, métodos y redirecciones | Define el riesgo de exfiltración y de las solicitudes |
| Procesos | Ruta del ejecutable, argumentos y procesos secundarios | Detecta inyección en el shell y acceso heredado |
| Credenciales | Nombre, alcance, almacenamiento y responsable de la revocación | Hace posible la retirada del acceso |
| Resultados | Datos devueltos al agente | Impide que los secretos vuelvan a través de la herramienta |

No escribas «internet» en la fila de red ni «credenciales del desarrollador» en la de credenciales. Esas etiquetas significan que la revisión todavía no ha comenzado. Nombra el host, la familia de rutas de la API, la cuenta o el token y la persona o sistema que puede revocarlo.

Este ejercicio también separa dos cosas que los equipos suelen confundir: la finalidad anunciada de una herramienta y su autoridad real. `create_issue` suena limitado. Una función que acepta una URL base arbitraria, encabezados arbitrarios y un cuerpo de solicitud arbitrario tiene la autoridad de un cliente HTTP genérico. Revisa esto último, no la etiqueta.

## El esquema de entrada debe reducir las opciones

Un esquema de entrada MCP debe limitar al agente a la operación que quieres permitir. No debe ser una definición de tipos decorativa alrededor de un canal de comandos libre.

La especificación MCP usa JSON Schema para los esquemas de entrada de las herramientas. Esto ayuda a los clientes a presentar argumentos y a las implementaciones a validarlos, pero JSON Schema no es una medida de cumplimiento a menos que el servidor rechace los valores no válidos antes de realizar el trabajo. Trata el esquema como la primera barrera y la validación del lado del servidor como la segunda.

Este es un esquema revisable para una herramienta que obtiene el estado de una compilación conocida:

```json
{
  "name": "get_build_status",
  "description": "Return the status for one build in the approved CI project.",
  "inputSchema": {
    "type": "object",
    "additionalProperties": false,
    "required": ["build_id"],
    "properties": {
      "build_id": {
        "type": "string",
        "pattern": "^[A-Z]{2,8}-[0-9]{1,10}$",
        "maxLength": 20
      },
      "include_logs": {
        "type": "boolean",
        "default": false
      }
    }
  }
}
```

Este esquema fija varias decisiones para el agente. No puede elegir un host. No puede adjuntar encabezados. No puede pasar un comando al shell. No puede enviar campos no declarados porque `additionalProperties` es `false`. El identificador de la compilación tiene un formato limitado, lo que hace menos peligroso su uso posterior y facilita el registro.

Ahora compáralo con la forma que suele causar problemas:

```json
{
  "name": "request",
  "inputSchema": {
    "type": "object",
    "properties": {
      "url": {"type": "string"},
      "method": {"type": "string"},
      "headers": {"type": "object"},
      "body": {}
    }
  }
}
```

Eso es un cliente HTTP con una interfaz amable. Si tiene un token bearer, puede enviar ese token o datos de un prompt controlado por el agente a un endpoint arbitrario. La gente conserva este diseño porque ahorra tiempo al crear prototipos. Sigue siendo un mal límite de producción, aunque el nombre de la herramienta parezca específico.

Prueba el manejo de entradas con valores que alteren el análisis, no solo con valores que parezcan defectuosos. Prueba un campo de identificador duplicado, un campo inesperado, una cadena grande, caracteres Unicode visualmente parecidos, un salto de línea y un valor sintácticamente válido que apunte fuera del dominio de negocio previsto. Si una entrada se convierte en una ruta, exige un identificador relativo y resuélvelo contra un directorio fijo. Si se convierte en un filtro de API, usa parámetros estructurados en lugar de concatenar cadenas de consulta.

Nunca entregues un argumento al shell solo porque hayas validado el esquema. Usa un array de argumentos con una ruta de ejecutable fija. Esto es más seguro:

```python
subprocess.run(
    ["/usr/local/bin/buildctl", "status", "--id", build_id],
    check=True,
    text=True,
    capture_output=True,
    env={"PATH": "/usr/bin:/bin"}
)
```

Esto es un fallo de revisión:

```python
subprocess.run(f"buildctl status --id {build_id}", shell=True)
```

La primera forma todavía necesita validación, gestión de errores y un ejecutable confiable. No le pide al shell que reinterprete la puntuación controlada por el modelo.

## Toda solicitud saliente necesita un destino fijo

Una herramienta MCP personalizada debe tener un conjunto pequeño de destinos, y su código debe rechazar cualquier otro antes de abrir una conexión. No sirve de nada permitir un nombre de host en la documentación si el código de solicitudes acepta una URL arbitraria.

Revisa el tráfico saliente en dos niveles. Primero, busca en el código bibliotecas HTTP, clientes WebSocket, consultas DNS, instaladores de paquetes, SDK de telemetría, bibliotecas de webhooks y cualquier proceso auxiliar que pueda conectarse a otro lugar. Después, observa una ejecución real. La inspección estática encuentra las rutas previstas. La observación en tiempo de ejecución detecta una dependencia que contacta con su origen o un valor de configuración que cambió el destino.

Un cliente seguro construye una URL a partir de piezas fijas y codifica solo el identificador:

```python
from urllib.parse import quote

BASE = "https://ci.example.internal/api/builds/"
url = BASE + quote(build_id, safe="")
response = client.get(url, timeout=10, follow_redirects=False)
```

El control importante no es `quote`. Es el origen fijo. Si tu cliente sigue redirecciones, un origen confiable puede devolver una redirección a un host no confiable. Desactiva las redirecciones, a menos que la herramienta compruebe cada destino de redirección contra la misma lista permitida.

Esto también importa en los servicios internos. Una herramienta que acepta `http://host/path` puede ser engañada para acceder a servicios administrativos locales o endpoints de metadatos a los que un agente no puede acceder directamente. Bloquear los hosts públicos no resuelve el problema. Necesitas una lista positiva de orígenes aprobados y una regla que rechace las direcciones IP literales o los destinos privados, salvo que la herramienta los necesite expresamente.

Captura el tráfico en un entorno de pruebas desechable. En macOS, `lsof` ofrece una primera vista rápida de los sockets de red actuales:

```sh
lsof -nP -iTCP -sTCP:ESTABLISHED -c python
```

La forma de la salida identifica un proceso, su usuario, el descriptor de archivo y el endpoint remoto:

```text
COMMAND   PID  USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
python   8421  alex   12u  IPv4 0x...             0t0  TCP 10.0.0.8:51244->203.0.113.20:443 (ESTABLISHED)
```

Sustituye `python` por el nombre real del proceso y repite la comprobación mientras invocas una sola operación de la herramienta. Este comando no es una auditoría completa del tráfico. Las conexiones breves pueden desaparecer antes de que las inspecciones. Aun así, detecta bien una conexión inesperada de larga duración o un ayudante cuya existencia desconocías.

Revisa las cargas de las solicitudes con el mismo cuidado. Una herramienta puede llamar correctamente a una API aprobada y, al mismo tiempo, volcar un diff completo del repositorio, una variable de entorno o la transcripción del agente en un parámetro de consulta. Limita los campos salientes en el código. Construye la carga a partir de los valores identificados que necesita la operación, en lugar de serializar un objeto completo recibido del agente.

## La identidad del proceso forma parte del permiso

No puedes aprobar de forma responsable el uso de una herramienta si no puedes saber qué ejecutable lo solicitó. El nombre del proceso por sí solo ofrece pocas garantías, porque cualquier proceso puede elegir un nombre que parezca conocido.

Registra el comando completo de inicio, la ruta del ejecutable, la versión, el directorio de trabajo, el proceso padre y la cuenta de usuario. Si se aplica la firma de código de macOS, revisa también la autoridad de la firma. `codesign` puede mostrar la información de identidad que macOS reconoce:

```sh
codesign -dv --verbose=4 /absolute/path/to/mcp-server 2>&1 | grep -E 'Identifier|TeamIdentifier|Authority'
```

La salida habitual contiene campos como estos:

```text
Identifier=com.example.mcpserver
Authority=Developer ID Application: Example Developer
TeamIdentifier=ABCDE12345
```

Esos campos son pruebas, no una decisión de permisos por sí mismos. Un binario firmado todavía puede ser el binario equivocado para este trabajo, y un script interno sin firma no es automáticamente malicioso. La revisión debe comprobar si la ruta, el propietario, el código fuente y la identidad coinciden con la herramienta que esperabas ejecutar.

Después, inspecciona el árbol de procesos durante una invocación:

```sh
ps -axo pid,ppid,user,command | grep -E 'mcp-server|sp-ssh|node|python'
```

Quieres obtener una respuesta aburrida: el cliente del agente inicia el servidor MCP y el servidor inicia únicamente los procesos auxiliares previstos. Sospecha cuando el servidor inicia un shell, un gestor de paquetes, un intérprete desde un directorio de proyecto modificable o un proceso en segundo plano que sobrevive a la sesión.

Un fallo común parece inofensivo en un archivo de configuración:

```json
{
  "command": "npx",
  "args": ["-y", "some-mcp-package"]
}
```

Esto puede descargar o cambiar código al iniciarse, según el estado de la caché local y la resolución de paquetes. Hace que una revisión del código fuente de ayer sea menos significativa de lo que los equipos suelen pensar. Fija el ejecutable o la versión del paquete, instálalo mediante un proceso controlado e inicia una ruta local conocida. Si la herramienta necesita actualizaciones, convierte la actualización en un evento de revisión explícito, no en un efecto secundario invisible de iniciar un agente.

La identidad del proceso también incluye el entorno heredado. Un servidor iniciado desde el shell de un desarrollador puede heredar tokens de servicios en la nube, tokens de control de código fuente, configuración de proxy y un `PATH` amplio. Imprime un inventario saneado en las ejecuciones de prueba o inicia el proceso con un entorno mínimo. No registres valores secretos. Registra los nombres de las variables que afectan al comportamiento y confirma que la herramienta no necesita credenciales ambientales que nunca debió utilizar.

## Los registros deben reconstruir las acciones sin repetir secretos

Un registro de auditoría útil responde quién ejecutó la herramienta, bajo qué proceso, con qué argumentos saneados, contra qué destino y con qué resultado. Una línea que diga `tool call succeeded` no responde a ninguna de las preguntas que tendrás cuando un agente envíe datos a un lugar inesperado.

Separa los registros operativos de la salida de depuración que contiene secretos. Los operadores necesitan detalles suficientes para investigar. No necesitan tokens bearer, encabezados de autorización, claves privadas, transcripciones completas del agente ni cuerpos de respuesta completos copiados en un archivo de texto.

Para cada invocación, registra campos parecidos a estos:

```json
{
  "time": "2025-03-08T14:22:11Z",
  "session_id": "run_7c2f",
  "process": "/opt/tools/build-mcp",
  "tool": "get_build_status",
  "argument_summary": {"build_id": "CI-4812", "include_logs": false},
  "destination": "ci.example.internal",
  "decision": "approved",
  "result": "success",
  "request_id": "c4e8..."
}
```

El ejemplo usa deliberadamente un resumen de argumentos. El resumen debe conservar identificadores y campos limitados que ayuden a investigar, y ocultar o aplicar un hash a los valores sensibles. Si una operación envía legítimamente un documento, registra su tamaño en bytes y un resumen del contenido cuando ayude a establecer correlaciones. No pongas el documento en el registro solo para facilitar la depuración.

El modelo de registros de OpenTelemetry resulta útil incluso si no adoptas OpenTelemetry. Distingue el cuerpo del evento de los atributos y da importancia a los campos estructurados para filtrar y establecer correlaciones. Aplica la misma idea localmente: haz que el destino, la operación, el resultado y la identidad del proceso sean legibles por máquinas. Un montón de líneas de texto se vuelve inútil la primera vez que necesitas saber si el agente envió la misma solicitud diez veces.

Revisa también las rutas de error. Muchas herramientas ocultan las solicitudes correctas, pero imprimen un objeto de solicitud completo cuando una API devuelve un error. Fuerza un 401, un tiempo de espera agotado, JSON malformado y una búsqueda DNS fallida. Lee cada línea emitida. La salida de depuración es el lugar por el que suelen escaparse las credenciales.

Sallyport mantiene un diario Sessions para las ejecuciones de los agentes y un diario Activity para las llamadas individuales, ambos proyectados desde un registro de auditoría cifrado y encadenado mediante hashes, ciego a la escritura. Este diseño resulta útil cuando necesitas un registro local que vaya más allá de los registros propios del autor de la herramienta, pero no hace segura una herramienta amplia. La herramienta sigue necesitando entradas limitadas y destinos conocidos.

## Los prompts de aprobación no pueden reparar una autoridad amplia

La aprobación humana solo sirve de freno cuando lo que apruebas tiene un alcance comprensible. Una tarjeta que indica que un proceso no reconocido quiere usar una credencial te comunica algo importante. No te dice si una herramienta de solicitudes genérica enviará un archivo del repositorio a un host elegido por un modelo cinco segundos después.

Mantén la unidad de aprobación cerca de la unidad de autoridad. Un token de solo lectura para consultar estados y un token de despliegue en producción no deberían compartir una aprobación, porque sus consecuencias son distintas. Una herramienta que lee un registro de versiones no debería obtener silenciosamente la capacidad de crear uno porque ambas operaciones usen la misma API.

La especificación del Model Context Protocol recomienda que los clientes obtengan el consentimiento del usuario antes de invocar herramientas. Es una recomendación sensata, pero el consentimiento tiene un punto débil: las personas aprueban prompts repetitivos y mal descritos hasta que el prompt deja de transmitir información. No resuelvas la fatiga de los prompts aprobando para siempre toda una categoría de acciones. Corrige el límite de la herramienta que produce prompts vagos o excesivos.

La escala de decisiones de Sallyport coloca una bóveda bloqueada por delante de todas las acciones, solicita autorización por sesión de forma predeterminada y puede exigir una decisión para cada uso de una credencial específica. Usa la aprobación por llamada para credenciales cuyo uso quieras inspeccionar cada vez, como una credencial de despliegue o una API con capacidad de escritura. No la uses como excusa para entregar esa credencial a una herramienta HTTP genérica.

Una buena prueba de aprobación puede expresarse en una frase: «Este proceso firmado, iniciado desde esta ruta, puede usar esta credencial para leer estados de este servicio durante esta ejecución». Si no puedes decirlo honestamente, deniega la solicitud y vuelve al mapa de autoridad.

## Prueba las rutas de fallo antes de confiar en el éxito

Una herramienta que funciona en el camino ideal no ha superado una revisión de seguridad. Necesitas ver cómo se comporta cuando las entradas son incorrectas, falta su credencial, la red apunta a un lugar inesperado o falla su proceso auxiliar.

Ejecuta la herramienta en una cuenta de prueba o un proyecto aislado, con una credencial cuyo alcance práctico sea el mínimo. Usa datos de prueba lo bastante parecidos a los reales para probar la serialización y los límites de tamaño, pero nunca introduzcas secretos de producción en una herramienta no revisada solo para ver qué sucede.

Usa esta secuencia de cinco pruebas:

1. Llama a la herramienta con una solicitud válida y captura su árbol de procesos, el destino saliente y el registro de auditoría.
2. Envía un campo no declarado, un valor de longitud máxima, un valor que contenga saltos de línea y un identificador aparentemente válido fuera del proyecto permitido. El servidor debería rechazar cada uno antes de realizar una solicitud.
3. Intenta forzar un host no aprobado mediante todas las rutas de entrada y configuración disponibles. Incluye las redirecciones si la herramienta usa HTTP. La herramienta debe rechazarlo y registrar el rechazo sin exponer la entrada sensible.
4. Elimina o revoca la credencial mientras el servidor sigue en ejecución y repite la solicitud válida. Confirma que la siguiente acción falla, en lugar de tener éxito gracias a una caché oculta o a un entorno heredado.
5. Termina el proceso del agente padre y comprueba si quedan procesos del servidor o auxiliares. Un trabajador en segundo plano que conserve el acceso después de que salga el agente necesita una razón clara y una revisión independiente.

Conserva las pruebas junto con la versión de la herramienta: el manifiesto, la revisión del código fuente o el resumen del paquete, los comandos usados, los endpoints observados, un registro de auditoría de muestra con los datos ocultos y la persona que aceptó los riesgos restantes. No es papeleo por hacer papeleo. Sin pruebas versionadas, una actualización posterior del paquete cambia la herramienta y todo el mundo supone que la revisión anterior sigue siendo válida.

Un fallo merece especial atención. Supón que una herramienta de búsqueda de documentos acepta `repository_path` y llama a un ayudante mediante una cadena de shell. Las solicitudes normales funcionan. Más tarde, un agente recibe en un ticket una instrucción para buscar una ruta que contiene puntuación del shell. El ayudante ejecuta un segundo comando con la cuenta del desarrollador, lee un archivo de credenciales y la herramienta envía el resultado a su endpoint de búsqueda aprobado. Cada componente hizo lo que esperaba su autor. La composición falló porque el esquema permitía una ruta, el shell la reinterpretó y la carga saliente aceptaba cualquier salida del ayudante. Prueba las cadenas completas, no solo las funciones aisladas.

## Retirar el acceso requiere algo más que borrar una entrada de configuración

Quitar un servidor MCP de la configuración de un agente detiene la ruta normal de inicio. No revoca un token copiado en una caché, no termina un servidor que todavía está en ejecución, no elimina una clave SSH de un proceso de agente ni invalida el acceso en el servicio remoto.

Planifica la retirada cuando concedas el acceso. El responsable de la credencial debe saber dónde revocarla, la herramienta debe usar una credencial distinta cuando sea posible y el operador debe saber qué proceso y qué archivos locales eliminar. Los tokens compartidos de desarrolladores convierten una retirada sencilla en un incidente, porque no puedes saber qué uso pertenece a la herramienta.

Para una herramienta que usa HTTP, revoca o desactiva primero el token remoto. Después detén el servidor MCP y todos los ayudantes secundarios, elimina la referencia local a la credencial y borra la configuración de inicio. Por último, vuelve a ejecutar la misma solicitud y conserva el resultado denegado. En SSH, elimina la clave pública correspondiente de la cuenta o el repositorio remoto, termina los procesos auxiliares locales y revisa la configuración del agente en busca de identidades alternativas.

No confundas el bloqueo de una bóveda local con la revocación remota. El bloqueo impide usos futuros a través de esa bóveda mientras permanezca bloqueada. No puede retirar datos ya enviados, invalidar un token almacenado en otro lugar ni detener un proceso independiente que heredó una credencial antes.

Haz que la retirada se pueda probar antes de un incidente. Añade una entrada breve al procedimiento operativo con la ubicación de la revocación remota, la respuesta de denegación esperada, el comando del servidor, la ubicación de la configuración y los campos del registro que confirman que el intento falló. Si el responsable de la herramienta no puede proporcionar esa entrada, no ha terminado la integración.

## Una herramienta limitada se gana una confianza repetible

Las herramientas que superan una revisión suelen ser aburridas, en el mejor sentido. Aceptan unos pocos campos tipados, se conectan a un solo servicio conocido, ejecutan un binario conocido cuando lo necesitan, devuelven únicamente el resultado que el agente necesita y dejan un registro que alguien puede inspeccionar después.

Las herramientas amplias parecen flexibles porque trasladan al agente las decisiones de diseño durante la ejecución. También convierten una sola aprobación en autoridad sobre destinos, datos y comandos que nadie revisó. Mantén la flexibilidad en el código que controlas y expón al agente una operación limitada.

Antes de aprobar una herramienta personalizada, intenta eliminar una entrada, un destino, parte del alcance de una credencial o un proceso secundario. Si nadie puede explicar por qué ese elemento debe permanecer, elimínalo. La revisión será más sencilla, y también lo será la respuesta ante un incidente.
