Skip to main content
Glama
Asaad-Suliman

safe-mcp-suite

safe-mcp-suite

Dos servidores MCP — un terminal y un organizador de archivos — que comparten un núcleo de seguridad que a ninguno de los dos se le permite eludir.

Python 3.12+ Licencia: MIT CI MCP


Qué es esto

Busca en GitHub un servidor MCP que ejecute comandos de shell y encontrarás una y otra vez el mismo archivo: una herramienta decorada con @mcp.tool(), una llamada a subprocess.run(command, shell=True), y el resultado devuelto directamente al modelo. Los de sistema de archivos tienen la misma forma: os.rename en un bucle, quizás con un try/except alrededor.

Funcionan. Ese es el problema. Funcionan hasta que el modelo produce algo que nadie anticipó, y para entonces la eliminación ya ha ocurrido, y no hay registro de qué se ejecutó ni por qué se permitió.

Este repositorio es esos dos servidores reconstruidos para que la seguridad sea el diseño, no un envoltorio añadido encima. Un único paquete safety/ es dueño de cada decisión que podría hacerte daño: qué está permitido, dónde está el límite, qué se registra, qué se oculta. Los dos servidores debajo son cableado. Ninguno puede llegar más allá del núcleo, porque ninguno implementa una regla propia.

La apuesta detrás de esto: la política determinista es más confiable que el juicio del modelo. Un prompt puede convencer a un modelo de dejar de ser cuidadoso. No puede convencer a una comprobación de contención de rutas de devolver True.


Related MCP server: Safe Terminal MCP Server

Inicio rápido

Necesitas Python 3.12+ y uv.

git clone https://github.com/Asaad-Suliman/safe-mcp-suite.git safe-mcp-suite
cd safe-mcp-suite
uv sync
./scripts/make_demo_sandbox.sh

Ese último script siembra sandbox/terminal, sandbox/files y state/ para que ambos servidores tengan un lugar legal donde operar. Luego inicia el que quieras:

uv run safe-mcp terminal --config policy.example.toml
uv run safe-mcp files --config policy.example.toml

Sobre la bandera --config

No es opcional, y no hay respaldo. No hay búsqueda automática de ./policy.toml, ni carga de .env, ni una raíz predeterminada que apunte silenciosamente a tu directorio personal. Si ni --config ni SAFE_MCP_POLICY_FILE están configurados, el servidor imprime el motivo y sale.

Esto es deliberado y es la decisión más dogmática de toda la configuración. Una zona de pruebas que silenciosamente usa un valor predeterminado conveniente es una zona de pruebas que algún día usará un valor predeterminado costoso. Negarse a iniciar es el fallo más barato posible.

Dos archivos de política se incluyen con el repositorio, y no son intercambiables:

Archivo

Qué es

¿Arrancará?

policy.example.toml

Ejemplo funcional, con raíz en ./sandbox

Sí — ejecútalo ya

policy.toml

Plantilla anotada con las raíces comentadas

No, a propósito

policy.toml se niega a iniciar hasta que rellenes jail_root y workspace_root tú mismo. Esa negativa es una característica, y hay una prueba de regresión que la mantiene. Cópialo, edítalo, apúntalo a directorios reales cuando estés listo.

Las raíces también pueden venir del entorno si lo prefieres:

SAFE_MCP_JAIL_ROOT=/path/to/jail
SAFE_MCP_WORKSPACE_ROOT=/path/to/workspace

Registro con un cliente MCP

{
  "mcpServers": {
    "safe-mcp terminal": {
      "command": "uv",
      "args": ["run", "safe-mcp", "terminal"],
      "env": {
        "SAFE_MCP_POLICY_FILE": "/srv/safe-mcp/policy.example.toml",
        "SAFE_MCP_JAIL_ROOT": "/srv/safe-mcp/sandbox"
      }
    },
    "safe-mcp files": {
      "command": "uv",
      "args": ["run", "safe-mcp", "files"],
      "env": {
        "SAFE_MCP_POLICY_FILE": "/srv/safe-mcp/policy.example.toml",
        "SAFE_MCP_WORKSPACE_ROOT": "/srv/safe-mcp/inbox"
      }
    }
  }
}

Demostración

Todo lo siguiente es salida real capturada. Nada aquí está escrito a mano, recortado o embellecido después del hecho — estos son los sobres OperationResult reales devueltos por un cliente MCP en vivo hablando con ambos servidores, ejecutados contra la zona de pruebas sembrada desde ./scripts/make_demo_sandbox.sh con policy.example.toml.

Lee el campo code en cada uno. Esa taxonomía es el punto central: cada resultado, éxito o rechazo, vuelve con la misma forma.

Servidor de terminal

Un comando permitido:

>>> tool: run_command  args: {"command": "cat notes.txt"}
{
  "action": "run_command",
  "code": "OK",
  "detail": {
    "exit_code": 0,
    "stderr": "",
    "stdout": "demo file\n"
  },
  "duration_ms": 1,
  "ok": true,
  "reason": "'cat' is allowed"
}

Un comando denegado:

>>> tool: run_command  args: {"command": "rm -rf /"}
{
  "action": "run_command",
  "code": "POLICY_DENIED",
  "detail": {},
  "duration_ms": 0,
  "ok": false,
  "reason": "'rm' is on the denylist"
}

Observa lo que la denegación no es: un traceback, una excepción lanzada, o una cadena que el modelo tenga que adivinar. Es un código tipado con una razón adjunta.

Servidor de archivos

Esta secuencia se ejecuta contra un espacio de trabajo sembrado que contiene archivos ordinarios, un instalador, un archivo de punto y un enlace simbólico — uno de cada cosa que la política trata de manera diferente.

plan_organize proponiendo movimientos y listando omisiones:

>>> tool: plan_organize  args: {}
{
  "action": "plan_organize",
  "code": "OK",
  "detail": {
    "created": "2026-08-07T16:49:00.048899+00:00",
    "move_count": 2,
    "moves": [
      {
        "category": "Images",
        "dest": "Images/photo.png",
        "size": 41,
        "src": "photo.png"
      },
      {
        "category": "Documents",
        "dest": "Documents/report.pdf",
        "size": 16,
        "src": "report.pdf"
      }
    ],
    "plan_id": "7dcf2cff-399d-4af5-bb9a-a4131e1d5288",
    "skip_count": 3,
    "skips": [
      {
        "code": "NEEDS_EXPLICIT_REQUEST",
        "name": ".bashrc",
        "reason": "dotfiles are configuration, not clutter to be filed",
        "rule": "organize"
      },
      {
        "code": "POLICY_DENIED",
        "name": "link.pdf",
        "reason": "'organize' is not permitted by any rule (deny by default)",
        "rule": null
      },
      {
        "code": "NEEDS_EXPLICIT_REQUEST",
        "name": "setup.exe",
        "reason": "installers, executables and application folders are left where the user put them",
        "rule": "organize"
      }
    ],
    "truncated": false
  },
  "duration_ms": 0,
  "ok": true,
  "reason": "proposed 2 move(s), skipped 3"
}

apply_plan ejecutando ese mismo plan:

>>> tool: apply_plan  args: {"plan_id": "7dcf2cff-399d-4af5-bb9a-a4131e1d5288"}
{
  "action": "apply_plan",
  "code": "OK",
  "detail": {
    "moved": 2,
    "moves": [
      {
        "dest": "Images/photo.png",
        "src": "photo.png"
      },
      {
        "dest": "Documents/report.pdf",
        "src": "report.pdf"
      }
    ],
    "plan_id": "7dcf2cff-399d-4af5-bb9a-a4131e1d5288",
    "planned": 2
  },
  "duration_ms": 2,
  "ok": true,
  "reason": "moved 2 file(s)"
}

Ahora el par interesante. Misma herramienta, dos objetivos nombrados, dos respuestas diferentes.

move_file rechazado por PROTECTION — el mismo enlace simbólico que plan_organize omitió arriba, nombrado explícitamente:

>>> tool: move_file  args: {"src": "link.pdf", "dest": "Documents/link.pdf"}
{
  "action": "move_file",
  "code": "POLICY_DENIED",
  "detail": {},
  "duration_ms": 0,
  "ok": false,
  "reason": "'organize' is not permitted by any rule (deny by default)"
}

move_file teniendo éxito con el instalador que plan_organize difirió con NEEDS_EXPLICIT_REQUEST, ahora nombrado explícitamente:

>>> tool: move_file  args: {"src": "setup.exe", "dest": "Documents/setup.exe"}
{
  "action": "move_file",
  "code": "OK",
  "detail": {
    "dest": "Documents/setup.exe",
    "moved": 1,
    "src": "setup.exe"
  },
  "duration_ms": 0,
  "ok": true,
  "reason": "moved setup.exe"
}

El planificador rechazó ambos. Preguntado directamente, el movedor rechazó uno e hizo el otro. Esa diferencia no es una inconsistencia — es el modelo de dos capas, y tiene su propia sección más abajo.


Diseño del sistema

Los servidores son cableado, no política

Ningún archivo de servidor contiene una regla de seguridad. Todas viven en safety/, y ambos servidores alcanzan las mismas funciones para obtener las mismas respuestas.

flowchart TD
    A["MCP client"] --> B["terminal server"]
    A --> C["files server"]
    B --> D["safety/policy.py<br/>allow or deny"]
    C --> D
    D --> E["safety/paths.py<br/>PathJail containment"]
    E --> F["execute or move"]
    F --> G["safety/redact.py<br/>secrets out, then truncate"]
    G --> H["safety/audit.py<br/>append-only JSONL"]
    H --> I["OperationResult"]
    I --> A

Esto no es orden por el orden mismo. Significa que una corrección de seguridad aterriza en exactamente un lugar, y significa que un revisor que audite este repositorio lea safety/ y haya terminado. No hay una segunda implementación escondida en un módulo de servidor, desviándose silenciosamente de la primera.

Cómo fluye realmente una solicitud

  1. Analizar. La cadena de comando se divide en una lista argv. Los metacaracteres de shell se rechazan aquí, antes de que se interprete nada — incluso dentro de comillas. Esa última parte es deliberadamente conservadora y es un techo documentado, no un descuido.

  2. Evaluar. El motor de política responde permitir o denegar. Denegar siempre gana. Cualquier cosa no permitida explícitamente es rechazada, así que una política vacía es un servidor inútil, no uno abierto.

  3. Contener. Cada ruta se resuelve y se comprueba contra la raíz de la jaula. Los enlaces simbólicos se inspeccionan como enlaces y nunca se atraviesan.

  4. Actuar. subprocess.run con shell=False, un entorno depurado de exactamente PATH, HOME, LANG, un tiempo de espera y un límite de salida. O, en el lado de archivos, un solo movimiento protegido registrado en un diario.

  5. Redactar, luego truncar. En ese orden, siempre.

  6. Registrar. Añadir al registro de auditoría. Si esa escritura falla, la operación falla con ella.

Seis decisiones que vale la pena robar

Los errores son datos, no excepciones. Cada resultado es un OperationResult con un ResultCode tipado. Ningún traceback llega al cliente. Un modelo que recibe un stack trace intentará sortearlo; un modelo que recibe POLICY_DENIED ha recibido algo útil e inequívoco.

Denegar gana, siempre. Las reglas de permitir y denegar no se ponderan ni se ordenan en un desempate. Si una denegación coincide, la respuesta es no. Las reglas de argumentos coinciden con tokens sin importar el orden, así que reorganizar banderas no es una vía de escape.

Nada arranca sin una raíz explícita. Cubierto en el Inicio rápido, pero también pertenece a la lista de diseño, porque "valor predeterminado sensato" es como la mayoría de las zonas de pruebas adquieren su primera fuga.

El registro de auditoría tiene permitido detenerte. Cierre por fallo por defecto: si el registro no se puede escribir, la operación no ocurre. Puedes cambiarlo a apertura por fallo con SAFE_MCP_AUDIT_FAIL_MODE, y esa es una decisión que tomas a propósito, en un archivo de configuración, donde alguien pueda verla.

Redactar antes de truncar. Invierte esos dos y un límite de salida de 64KB puede cortar un secreto por la mitad y emitir el primer fragmento, sin haber coincidido con ningún patrón. Esta es una elección de orden de dos líneas que cierra toda una categoría de fuga.

La detección de secretos basada en entropía está desactivada por defecto. Se dispara constantemente con hashes, UUIDs y cargas útiles base64. Un redactor que grita "lobo" termina siendo desactivado por sus propios usuarios, lo cual es peor que uno que admite sus límites desde el principio.

PROTECTION vs RESTRAINT

Las reglas de omisión se dividen en dos capas, y cada regla declara a cuál pertenece.

PROTECTION es aplicada por todas las herramientas, sin excepción. Solo parte de ella es una regla declarada — unsafe-name cubre caracteres de control en un nombre de archivo. El resto es estructural: una fuga de ruta falla la contención en safety/paths.py, un destino ocupado falla la comprobación lstat en servers/files/apply.py, y un enlace simbólico no coincide con ninguna regla de permitir, cayendo en denegar por defecto — que evaluate_layered clasifica deliberadamente como PROTECTION. Ver la nota más abajo. De cualquier manera, no hay bandera, ni anulación, ni argumento de "sé lo que hago". Estos son invariantes.

RESTRAINT es aplicada solo por plan_organize. Instaladores, carpetas de aplicaciones, archivos del sistema, archivos ocultos, directorios. No son peligrosos — son cosas sobre las que un clasificador automático no debería adivinar en tu nombre. El planificador los reporta como NEEDS_EXPLICIT_REQUEST y sigue adelante.

La consecuencia es el par de la demostración anterior. move_file moverá un instalador que tú mismo nombraste, porque rechazarlo sería paternalismo, no seguridad. No moverá un enlace simbólico por muy explícitamente que lo pidas, porque eso es contención.

Un matiz que vale la pena hacer explícito: la razón del rechazo del enlace simbólico dice "'organize' no está permitido por ninguna regla (denegar por defecto)" en lugar de cualquier cosa que mencione "enlace simbólico". Un enlace simbólico no coincide con ninguna regla de permitir en ninguna capa, así que cae en denegar por defecto — y safety.policy.evaluate_layered clasifica un fallo de denegar por defecto no coincidente como PROTECTION a propósito, como la lectura más estricta de algo para lo que la política no tiene vocabulario. La redacción genérica no es una garantía más débil; move_file nombrando el enlace simbólico directamente, arriba, es la prueba.

Un detalle de implementación que repetiría en cualquier lugar: el conjunto PROTECTION se deriva filtrando el conjunto completo de reglas, nunca se ensambla como su propia lista. Dos listas mantenidas a mano se desvían, y el modo de fallo de la desviación aquí es una regla de protección que desaparece silenciosamente. Un filtro no puede olvidar.

Sobre las pruebas

661 pruebas, todas herméticas. Ninguna prueba escribe estado real: cualquier cosa que se ejecute lo hace contra tmp_path, y las dos suites que leen los policy.toml y policy.example.toml incluidos los copian a tmp_path primero. Nunca se toca un registro de auditoría o espacio de trabajo real. CI ejecuta la suite más un escaneo de gitleaks en cada push y pull request.

El número quedará obsoleto en cuanto añada una prueba. La propiedad que importa es el aislamiento, no el recuento.


Garantías

Cada fila nombra el módulo y la función que la aplica. Si una afirmación aquí no está respaldada por código que puedas abrir, no debería estar en la tabla.

Garantía

Aplicada por

Denegar por defecto, en ambos servidores

safety/policy.py evaluate() — ninguna regla coincidente es una denegación

Una denegación coincidente siempre gana a una autorización coincidente

safety/policy.py evaluate() escanea cada coincidencia; la denegación cortocircuita

Sin shell en el servidor de terminal

servers/terminal/execute.pysubprocess.run(argv, shell=False)

Metacaracteres de shell rechazados antes de cualquier comprobación de política

servers/terminal/parse.py + safety/patterns.py (; | & < > `` $()

Los comandos están confinados a un directorio

safety/paths.py PathJail.resolve, comprobado en cwd

Los movimientos de archivos están confinados a un espacio de trabajo, seguros frente a enlaces simbólicos en el componente final

safety/paths.py PathJail.resolve_for_write — resuelve solo el padre, nunca sigue un enlace en el nombre que se escribe

Política de archivos de dos capas: invariante de seguridad frente a evitación de conjeturas

safety/policy.py PROTECTION / RESTRAINT, evaluate_layered()

Ningún destino se sobrescribe silenciosamente jamás

servers/files/apply.py move_one() — comprobado con lstat antes de cada renombrado

No existe ninguna herramienta de borrado, protegida o no

servers/files/server.py — seis herramientas, ninguna de ellas borra; ningún argumento produce una

Cada movimiento es deshacible, incluido un plan aplicado completo

servers/files/journal.py + undo_last_action / redo_last_action

Un plan que ha quedado obsoleto no mueve nada en absoluto

servers/files/plan.py verify() — se rechaza el plan completo, no un subconjunto parcial

Un comando colgado se mata en un plazo

servers/terminal/execute.pysubprocess.run(timeout=...)

La salida está limitada, después de la redacción, nunca antes

safety/redact.py redact_and_truncate()

El proceso hijo recibe un entorno depurado

servers/terminal/execute.pyENV_ALLOWLIST = (PATH, HOME, LANG)

Los secretos se redactan tanto de la salida como de los campos de auditoría

safety/redact.py BUILTIN_PATTERNS, aplicados mediante un redactor compartido

El registro de auditoría es de cierre por defecto

safety/audit.py AuditLogger.record() — un registro no escribible lanza una excepción, y la operación nunca ocurre

Cada rechazo se registra, con un motivo

servers/*/server.py _serve() — el único punto de escritura de resultado por herramienta


Modelo de amenazas — explícitamente fuera de alcance

  • Un comando peligroso que hayas incluido en la lista blanca. Si permites un intérprete o una herramienta similar a un shell (bash, python, sh, find -exec, awk, env, …), el modelo puede hacer cualquier cosa que esa herramienta pueda. La fortaleza de la política depende enteramente de la lista blanca del operador.

  • Escapes del kernel / sandbox. El confinamiento es una comprobación de contención de rutas, no un sandbox del kernel — sin namespaces, cgroups ni seccomp. Ver "Advertencias honestas".

  • Acceso del host a los archivos de estado. El registro de auditoría y el diario de deshacer son a prueba de manipulaciones para el servidor, no a prueba de manipulaciones contra cualquiera con acceso al sistema de archivos del host.

  • Completitud de la redacción. Basada en patrones y de mejor esfuerzo; una forma de secreto que los patrones no reconocen pasa.

  • Identidad multiinquilino o limitación de velocidad. No hay autenticación por llamante dentro de ninguno de los servidores — el límite de confianza es "quien pueda iniciar este proceso", que un cliente MCP hace cumplir al lanzarlo, no este código.

  • Una condición de carrera entre validar una ruta y actuar sobre ella (TOCTOU). Ver "Advertencias honestas" más abajo.


Comparado con un servidor MCP ingenuo

Muchos servidores MCP rápidos envuelven subprocess.run(cmd, shell=True) para el acceso a la terminal y os.rename para los movimientos de archivos. Ambos son convenientes e inseguros. Esta tabla es factual, no una afirmación de seguridad perfecta.

Preocupación

Servidor MCP ingenuo

safe-mcp-suite

Ejecución de comandos

subprocess.run(cmd, shell=True) — cualquier cosa que el shell pueda interpretar

Solo argv, shell=False, lista blanca de denegación por defecto, la lista negra gana

Metacaracteres de shell

Interpretados (;, |, $(), redirección)

Rechazados antes de cualquier comprobación de política

Movimientos de archivos

os.rename en cualquier lugar al que el proceso pueda llegar

Confinado a un espacio de trabajo; un enlace simbólico en cualquiera de los extremos se rechaza, nunca se sigue

Sobrescribir un archivo

Generalmente silencioso — rename de POSIX reemplaza el destino

Siempre se rechaza; nunca se inventa una variante numerada (report(1).pdf)

Deshacer

Ninguno

Cada movimiento se registra en el diario; undo_last_action / redo_last_action

Borrar archivos

A menudo presente, a menudo sin protección

No existe ninguna herramienta de borrado en este servidor, punto

Entorno dado a un proceso hijo

Entorno completo del padre, incluidos los secretos

Depurado a PATH / HOME / LANG

Secretos en la salida o los registros

Se dejan pasar

Redactados, antes de la truncación, tanto en la respuesta como en el registro de auditoría

Auditabilidad

Ninguna por defecto

JSONL de solo añadir, cierre por defecto

Acción automática frente a acción solicitada explícitamente

Un solo camino de código trata ambos igual

plan_organize (no solicitado) está sujeto a ambas capas de reglas; move_file (una solicitud nombrada) está sujeto solo a los invariantes de seguridad

Ambas herramientas devuelven un OperationResult:

OperationResult {
  ok: bool
  code: ResultCode
  action: str
  reason: str
  detail: dict            # stdout, stderr, exit_code — empty when nothing ran
  duration_ms: int
}
  • run_command(command: str, cwd: str | None = None) — evalúa command contra la política y, si está permitido, lo ejecuta en un entorno aislado. cwd es opcional y debe resolverse dentro de la raíz del confinamiento; una travesía, un enlace simbólico o una ruta absoluta que escape devuelve PATH_ESCAPE sin ejecutar nada. Cada invocación se audita; bajo auditoría de cierre, un registro no escribible devuelve AUDIT_UNAVAILABLE en lugar de ejecutar sin registrar. Un código de salida distinto de cero sigue siendo ok: true — el comando se ejecutó; si tuvo éxito es asunto suyo.

  • explain_command(command: str) — la ejecución en seco. Alcanza el mismo análisis y evaluación que run_command y regresa antes del ejecutor, por lo que detail nunca lleva stdout, stderr ni un código de salida — no se ejecutó nada.

Códigos de resultado que este servidor puede devolver: OK, POLICY_DENIED, INVALID_REQUEST, PATH_ESCAPE, TIMEOUT, OUTPUT_TRUNCATED, OPERATION_FAILED, AUDIT_UNAVAILABLE, INTERNAL_ERROR.

Seis herramientas, y la lista es el diseño — no hay una séptima, y ninguna de ellas borra.

  • list_files(subdir: str | None = None) — solo lectura. Informa del nombre, tamaño, categoría de cada entrada, si el organizador la movería y por qué no cuando no lo haría.

  • plan_organize() — propone movimientos y omisiones. No cambia nada, ni siquiera las carpetas de destino. Devuelve un plan_id para pasar a apply_plan.

  • apply_plan(plan_id: str) — lleva a cabo un plan. Cada archivo se vuelve a comprobar primero; si alguno cambió, se movió o desapareció desde la planificación, se rechaza el plan completo. De un solo uso — un id no se puede reproducir.

  • move_file(src: str, dest: str) — mueve un archivo nombrado. dest es la ruta de destino completa, no una carpeta. Un destino que ya existe se rechaza, nunca se sobrescribe y nunca se renombra alrededor. Obedece solo las reglas de PROTECTION — ver "El modelo de dos capas" más arriba.

  • undo_last_action() — revierte el movimiento o plan aplicado más reciente, como una sola acción. Nada se sobrescribe para hacer espacio para un archivo restaurado.

  • redo_last_action() — vuelve a aplicar la acción deshecha más reciente. La pila de rehacer se limpia cada vez que se registra un nuevo trabajo.

Códigos de resultado que este servidor puede devolver adicionalmente: NEEDS_EXPLICIT_REQUEST (cumplió todos los invariantes de seguridad, rechazado solo porque actuar sin que se lo pidan sería una conjetura — nombra el objetivo directamente y pídelo).

Un archivo, policy.toml, leído por ambos servidores:

audit_log = "audit.jsonl"          # shared
audit_fail_mode = "closed"         # shared: "closed" or "open"

[redaction]                        # shared
enabled = true
entropy_fallback = false
extra_patterns = []                # [{ name = "...", regex = "..." }]

[terminal]
# jail_root = "/srv/safe-mcp/sandbox"   # REQUIRED — here or via env

[terminal.limits]
timeout_seconds = 30
max_output_bytes = 65536

[terminal.allowlist]
commands = ["ls", "cat", "echo", "pwd", "git"]

[terminal.denylist]
commands = ["rm", "shutdown", "reboot", "curl", "wget", "chmod", "sudo"]

[[terminal.rules]]
command = "git"
deny_args = ["push --force", "push -f"]
reason = "force-push rewrites shared history"

[files]
# workspace_root = "/srv/safe-mcp/inbox"   # REQUIRED — here or via env
journal = "organizer-journal.json"
max_plan_moves = 500

[files.categories]
Documents = [".pdf", ".doc", ".docx", "..."]
# ...

[[files.skip]]
layer = "protection"   # or "restraint" — required, no default
when = ["unsafe-name"]
reason = "..."

El archivo de política en sí no tiene ubicación predeterminada. Apúntalo con --config:

safe-mcp terminal --config /path/to/policy.toml
safe-mcp files --config /path/to/policy.toml

Las variables de entorno siguen siendo compatibles y cada una anula la clave correspondiente de policy.toml. SAFE_MCP_POLICY_FILE es la única excepción que vale la pena mencionar: es una alternativa a --config, no una anulación de este — --config gana si se dan ambos, y el inicio se niega si no se da ninguno.

Variable

Significado

Valor por defecto

SAFE_MCP_POLICY_FILE

Ruta a policy.toml (obligatorio, aquí o --config)

ninguna — se niega a iniciar

SAFE_MCP_JAIL_ROOT

Directorio de la jaula del terminal (obligatorio, aquí o jail_root)

ninguna — se niega a iniciar

SAFE_MCP_WORKSPACE_ROOT

Directorio del espacio de trabajo de archivos (obligatorio, aquí o files.workspace_root)

ninguna — se niega a iniciar

SAFE_MCP_FILES_JOURNAL

Ruta del diario de deshacer/rehacer (debe estar fuera del espacio de trabajo)

organizer-journal.json

SAFE_MCP_AUDIT_LOG

Ruta del registro de auditoría compartido (debe estar fuera de ambas jaulas)

audit.jsonl

SAFE_MCP_AUDIT_FAIL_MODE

closed o open

closed

El inicio falla de forma evidente — con un mensaje fatal: impreso y una salida distinta de cero — si no se proporciona ninguna ruta de archivo de política (ni --config ni SAFE_MCP_POLICY_FILE), si policy.toml falta o no es válido, si la raíz de la jaula/espacio de trabajo no está definida o no es un directorio, si la expresión regular de redacción del operador no es válida, si hay una entrada [[files.skip]] sin etiquetar, o si el registro de auditoría / diario se encuentra dentro de una jaula que el servidor podría entonces mover o falsificar.


Advertencias honestas

Esta es una capa de endurecimiento, no una caja fuerte. Léelas antes de desplegar cualquiera de los dos servidores.

  • La jaula es una comprobación de contención de rutas, no un sandbox del kernel. No hay namespaces, cgroups ni seccomp. Una explotación del kernel o una vía de escape accesible desde un binario en la lista de permitidos no está contenida.

  • El registro de auditoría evidencia manipulaciones, no es a prueba de manipulaciones, y no tiene rotación. Solo añadido con vaciado por registro + fsync significa que no perderá registros por un fallo, pero cualquiera con acceso al sistema de archivos del host a audit.jsonl puede leerlo, modificarlo o eliminarlo — y el archivo crece sin límite; no hay rotación ni política de retención integrada.

  • La redacción se basa en patrones y es de mejor esfuerzo. Detecta formas comunes de secretos; un formato novedoso o inusual pasa sin redactar. El respaldo opcional de entropía está desactivado por defecto porque es ruidoso con los SHA de git, los UUID y los datos base64, no porque sea débil.

  • Los metacaracteres del terminal se rechazan incluso entre comillas — un límite conocido. echo "a;b" se rechaza aunque el ; sea inerte dentro de las comillas, porque el escaneo es una comprobación bruta de subcadenas sin conocimiento de las comillas. Esa es la dirección segura en la que equivocarse — no hay truco de comillas que haga pasar a un operador por un escaneo que ignora las comillas en primer lugar — pero sí significa que se rechaza alguna entrada legítima.

  • TOCTOU: una ruta validada y luego utilizada puede cambiar en el intervalo. Tanto safety/paths.py como servers/files/apply.py comprueban la contención o la ocupación y luego actúan en una syscall separada; un enlace simbólico intercambiado o un archivo creado en ese intervalo no está cubierto. Documentado en el código como un límite deliberado y nombrado (comentarios # NOTE: en ambos archivos), con una ruta de actualización (O_NOFOLLOW más operaciones relativas a dir-fd) anotada por si alguna vez se necesita.

  • Los procesos nietos no se recolectan. Los procesos hijos de un comando eliminado o con tiempo agotado no están en un grupo de procesos separado; el ejecutor mata solo al hijo directo, por lo que cualquier cosa que ese comando haya generado puede sobrevivirle.


Trabajo relacionado


Licencia

MIT — ver LICENSE.

A
license - permissive license
Not graded
quality - not tested
A
maintenance

Maintenance

1Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

  • An MCP server for deep research or task groups

  • Run commands and read/write files on your servers over Termalin's keyless tunnels (hosted MCP).

  • A MCP server built for developers enabling Git based project management with project and personal…

  • An MCP server that provides read access to your cloud storage providers, bank accounts and more.

View all MCP Connectors

Related MCP Servers

View all related MCP servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Asaad-Suliman/safe-mcp-suite'

If you have feedback or need assistance with the MCP directory API, please join our Discord server