Skip to main content
Glama
Asaad-Suliman

safe-mcp-suite

safe-mcp-suite

Zwei MCP-Server – ein Terminal und ein Dateiorganisator – teilen sich einen Sicherheitskern, den keiner von ihnen umgehen darf.

Python 3.12+ License: MIT CI MCP


Worum es geht

Suche auf GitHub nach einem MCP-Server, der Shell-Befehle ausführt, und du wirst immer wieder dieselbe Datei finden: ein Tool, dekoriert mit @mcp.tool(), ein Aufruf von subprocess.run(command, shell=True), und das Ergebnis wird direkt an das Modell zurückgegeben. Die Dateisystem-Server haben dieselbe Form – os.rename in einer Schleife, vielleicht mit einem try/except drumherum.

Sie funktionieren. Das ist das Problem. Sie funktionieren, bis das Modell etwas produziert, das niemand vorhergesehen hat, und dann ist die Löschung bereits passiert, und es gibt keine Aufzeichnung darüber, was ausgeführt wurde oder warum es erlaubt war.

Dieses Repo ist diese beiden Server, neu aufgebaut, sodass die Sicherheit das Design ist, nicht ein Wrapper, der obendrauf geschraubt wurde. Ein einziges safety/-Paket besitzt jede Entscheidung, die dir schaden könnte – was erlaubt ist, wo die Grenze liegt, was aufgeschrieben wird, was verborgen wird. Die beiden Server darunter sind nur Verkabelung. Keiner kann am Kern vorbeireichen, weil keiner eine eigene Regel implementiert.

Die Wette dahinter: Deterministische Politik ist vertrauenswürdiger als Modellurteil. Ein Prompt kann ein Modell davon überzeugen, nicht vorsichtig zu sein. Er kann eine Pfad-Containment-Prüfung nicht dazu bringen, True zurückzugeben.


Related MCP server: Safe Terminal MCP Server

Schnellstart

Du brauchst Python 3.12+ und 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

Das letzte Skript legt sandbox/terminal, sandbox/files und state/ an, damit beide Server einen legalen Arbeitsbereich haben. Starte dann den, den du möchtest:

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

Über das --config-Flag

Es ist nicht optional, und es gibt keinen Fallback. Kein automatisches Suchen nach ./policy.toml, kein Laden von .env, kein Standard-Root, der still auf dein Home-Verzeichnis zeigt. Wenn weder --config noch SAFE_MCP_POLICY_FILE gesetzt ist, gibt der Server den Grund aus und beendet sich.

Das ist beabsichtigt und es ist die am stärksten meinungsbildende Sache am Setup. Eine Sandbox, die stillschweigend auf einen bequemen Ort zurückfällt, ist eine Sandbox, die eines Tages auf einen teuren Ort zurückfallen wird. Sich zu weigern zu starten, ist der billigste mögliche Fehler.

Zwei Policy-Dateien werden mit dem Repo ausgeliefert, und sie sind nicht austauschbar:

Datei

Was es ist

Wird es starten?

policy.example.toml

Funktionierendes Beispiel, verwurzelt in ./sandbox

Ja – starte es jetzt

policy.toml

Kommentierte Vorlage mit auskommentierten Roots

Nein, absichtlich

policy.toml weigert sich zu starten, bis du jail_root und workspace_root selbst ausfüllst. Diese Weigerung ist ein Feature, und es gibt einen Regressionstest, der sie aufrechterhält. Kopiere es, bearbeite es, weise es auf echte Verzeichnisse, wenn du bereit bist.

Die Roots können auch aus der Umgebung kommen, wenn du das bevorzugst:

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

Registrieren bei einem MCP-Client

{
  "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"
      }
    }
  }
}

Demo

Alles unten ist echte aufgezeichnete Ausgabe. Nichts hier ist handgeschrieben, gekürzt oder nachträglich verschönert – das sind die tatsächlichen OperationResult-Envelopes, die von einem Live-MCP-Client zurückgegeben werden, der mit beiden Servern spricht, ausgeführt gegen die Sandbox aus ./scripts/make_demo_sandbox.sh mit policy.example.toml.

Lies das code-Feld in jedem. Diese Taxonomie ist der ganze Punkt: Jedes Ergebnis, ob Erfolg oder Ablehnung, kommt in derselben Form zurück.

Terminal-Server

Ein erlaubter Befehl:

>>> 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"
}

Ein abgelehnter Befehl:

>>> 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"
}

Beachte, was die Ablehnung nicht ist: kein Traceback, keine ausgelöste Exception, kein String, den das Modell erraten muss. Es ist ein typisierter Code mit einem Grund.

Datei-Server

Diese Sequenz läuft gegen einen vorbereiteten Arbeitsbereich mit gewöhnlichen Dateien, einem Installer, einer Dotfile und einem Symlink – jeweils eines von dem, was die Policy unterschiedlich behandelt.

plan_organize schlägt Verschiebungen vor und listet Übersprungenes auf:

>>> 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 führt denselben Plan aus:

>>> 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)"
}

Jetzt das interessante Paar. Gleiches Tool, zwei benannte Ziele, zwei verschiedene Antworten.

move_file verweigert durch PROTECTION – derselbe Symlink, den plan_organize oben übersprungen hat, explizit benannt:

>>> 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 erfolgreich beim Installer, den plan_organize mit NEEDS_EXPLICIT_REQUEST aufgeschoben hat, jetzt explizit benannt:

>>> 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"
}

Der Planer hat beide abgelehnt. Direkt gefragt, hat der Verschieber einen abgelehnt und den anderen gemacht. Dieser Unterschied ist keine Inkonsistenz – es ist das Zwei-Schichten-Modell, und es bekommt weiter unten einen eigenen Abschnitt.


Systemdesign

Die Server sind Verkabelung, nicht Policy

Keine Serverdatei enthält eine Sicherheitsregel. Jede lebt in safety/, und beide Server erreichen dieselben Funktionen, um dieselben Antworten zu bekommen.

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

Das ist keine Ordnung um ihrer selbst willen. Es bedeutet, dass ein Sicherheitsfix an genau einer Stelle landet, und es bedeutet, dass ein Prüfer, der dieses Repo auditiert, safety/ liest und fertig ist. Es gibt keine zweite Implementierung, die sich in einem Servermodul versteckt und stillschweigend aus dem Takt mit der ersten gerät.

Wie eine Anfrage tatsächlich fließt

  1. Parsen. Die Befehlszeichenkette wird in eine argv-Liste aufgeteilt. Shell-Metazeichen werden hier abgelehnt, bevor irgendetwas interpretiert wird – auch innerhalb von Anführungszeichen. Der letzte Teil ist bewusst konservativ und eine dokumentierte Grenze, kein Versehen.

  2. Bewerten. Die Policy-Engine antwortet mit erlauben oder verweigern. Verweigern gewinnt immer. Alles, was nicht explizit erlaubt ist, wird abgelehnt, also ist eine leere Policy ein nutzloser Server, kein offener.

  3. Eingrenzen. Jeder Pfad wird aufgelöst und gegen den Jail-Root geprüft. Symlinks werden als Links inspiziert und nie durchgefolgt.

  4. Handeln. subprocess.run mit shell=False, einer bereinigten Umgebung von genau PATH, HOME, LANG, einem Timeout und einer Ausgabegrenze. Oder auf der Dateiseite eine einzelne geschützte Verschiebung, die in einem Journal aufgezeichnet wird.

  5. Redigieren, dann kürzen. In dieser Reihenfolge, immer.

  6. Aufzeichnen. An das Audit-Log anhängen. Wenn dieser Schreibvorgang fehlschlägt, schlägt die Operation mit ihm fehl.

Sechs Entscheidungen, die es wert sind, gestohlen zu werden

Fehler sind Daten, keine Ausnahmen. Jedes Ergebnis ist ein OperationResult mit einem typisierten ResultCode. Kein Traceback erreicht jemals den Client. Ein Modell, das einen Stacktrace erhält, wird versuchen, ihn zu umgehen; ein Modell, das POLICY_DENIED erhält, hat etwas Nützliches und Eindeutiges erfahren.

Verweigern gewinnt, immer. Erlauben- und Verweigern-Regeln werden nicht gewichtet oder in einen Tiebreak geordnet. Wenn eine Verweigern-Regel zutrifft, ist die Antwort nein. Argumentregeln matchen Tokens unabhängig von der Reihenfolge, also ist das Umsortieren von Flags kein Bypass.

Nichts startet ohne expliziten Root. Im Schnellstart behandelt, aber es gehört auch in die Designliste, weil „sinnvoller Standard" der Weg ist, auf dem die meisten Sandboxes ihre erste Flucht erwerben.

Das Audit-Log darf dich stoppen. Standardmäßig fail-closed: Wenn das Log nicht geschrieben werden kann, passiert die Operation nicht. Du kannst es mit SAFE_MCP_AUDIT_FAIL_MODE auf fail-open umstellen, und das ist eine Entscheidung, die du absichtlich triffst, in einer Konfigurationsdatei, wo jemand sie sehen kann.

Redigieren vor Kürzen. Drehe die beiden um, und eine 64-KB-Ausgabegrenze kann ein Geheimnis in zwei Hälften schneiden und das erste Fragment ausgeben, ohne ein Muster getroffen zu haben. Das ist eine Zwei-Zeilen-Reihenfolgeentscheidung, die eine ganze Kategorie von Leck schließt.

Entropiebasierte Geheimniserkennung ist standardmäßig aus. Sie feuert ständig auf Hashes, UUIDs und Base64-Payloads. Ein Redaktor, der Wolf schreit, wird von seinen eigenen Benutzern deaktiviert, was schlimmer ist als einer, der seine Grenzen im Voraus zugibt.

PROTECTION vs. RESTRAINT

Die Überspringungsregeln teilen sich in zwei Schichten, und jede Regel deklariert, zu welcher sie gehört.

PROTECTION wird von jedem Tool durchgesetzt, ohne Ausnahme. Nur ein Teil davon ist eine deklarierte Regel – unsafe-name deckt Steuerzeichen in einem Dateinamen ab. Der Rest ist strukturell: Ein Pfadausbruch scheitert an der Containment-Prüfung in safety/paths.py, ein belegtes Ziel scheitert am lstat-Check in servers/files/apply.py, und ein Symlink matcht überhaupt keine Erlauben-Regel und fällt auf Deny-by-default zurück – was evaluate_layered absichtlich als PROTECTION klassifiziert. Siehe die Anmerkung unten. In jedem Fall gibt es kein Flag, keinen Override, kein „Ich weiß, was ich tue"-Argument. Das sind Invarianten.

RESTRAINT wird nur von plan_organize durchgesetzt. Installer, Anwendungsordner, Systemdateien, versteckte Dateien, Verzeichnisse. Diese sind nicht gefährlich – sie sind Dinge, über die ein automatischer Klassifikator nicht in deinem Namen raten sollte. Der Planer meldet sie als NEEDS_EXPLICIT_REQUEST und macht weiter.

Die Konsequenz ist das Demo-Paar oben. move_file wird einen Installer verschieben, den du selbst benannt hast, weil das zu verweigern Paternalismus wäre, nicht Sicherheit. Es wird keinen Symlink verschieben, egal wie explizit du fragst, weil das Containment ist.

Eine Nuance, die es wert ist, explizit zu sein: Der Ablehnungsgrund des Symlinks lautet "'organize' is not permitted by any rule (deny by default)" und nicht irgendetwas, das „Symlink" erwähnt. Ein Symlink matcht keine Erlauben-Regel in keiner Schicht, also fällt er auf Deny-by-default zurück – und safety.policy.evaluate_layered klassifiziert einen ungematchten Deny-by-default-Fallthrough absichtlich als PROTECTION, als die strengste Lesart von etwas, für das die Policy kein Vokabular hat. Die allgemeine Formulierung ist keine schwächere Garantie; move_file, das den Symlink direkt benennt, oben, ist der Beweis.

Ein Implementierungsdetail, das ich überall wiederholen würde: Die PROTECTION-Menge wird durch Filtern der vollständigen Regelsatz abgeleitet, nie als eigene Liste zusammengestellt. Zwei handgepflegte Listen driften auseinander, und die Fehlerart des Driftens ist hier eine Schutzregel, die stillschweigend verschwindet. Ein Filter kann nicht vergessen.

Zu den Tests

661 Tests, alle hermetisch. Kein Test schreibt echten Zustand: Alles, was ausgeführt wird, läuft gegen tmp_path, und die beiden Suiten, die die mitgelieferten policy.toml und policy.example.toml lesen, kopieren sie zuerst nach tmp_path. Kein echtes Audit-Log oder Arbeitsbereich wird jemals berührt. CI führt die Suite plus einen gitleaks-Scan bei jedem Push und Pull Request aus.

Die Zahl wird veraltet sein, sobald ich einen Test hinzufüge. Die Eigenschaft, die zählt, ist die Isolation, nicht die Anzahl.


Garantien

Jede Zeile nennt das Modul und die Funktion, die sie durchsetzt. Wenn eine Behauptung hier nicht durch Code gestützt ist, den du öffnen kannst, sollte sie nicht in der Tabelle stehen.

Garantie

Durchgesetzt durch

Standardmäßig verweigern, in beiden Servern

safety/policy.py evaluate() – keine passende Regel ist eine Verweigerung

Eine passende Verweigerung schlägt immer eine passende Erlaubnis

safety/policy.py evaluate() scannt jede Übereinstimmung; Verweigerung unterbricht

Keine Shell im Terminal-Server

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

Shell-Metazeichen werden vor jeder Richtlinienprüfung abgelehnt

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

Befehle sind auf ein Verzeichnis beschränkt

safety/paths.py PathJail.resolve, geprüft auf cwd

Dateiverschiebungen sind auf einen Arbeitsbereich beschränkt, symlink-sicher bei der letzten Komponente

safety/paths.py PathJail.resolve_for_write – löst nur das übergeordnete Element auf, folgt niemals einem Link beim Namen, der geschrieben wird

Zweischichtige Dateirichtlinie: Sicherheitsinvariante vs. Vermeidung von Vermutungen

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

Kein Ziel wird jemals stillschweigend überschrieben

servers/files/apply.py move_one() – vor jeder Umbenennung per lstat geprüft

Es gibt kein Löschwerkzeug, weder geschützt noch sonst

servers/files/server.py – sechs Werkzeuge, keines davon löscht; kein Argument erzeugt eines

Jede Verschiebung ist rückgängig machbar, einschließlich eines gesamten angewendeten Plans

servers/files/journal.py + undo_last_action / redo_last_action

Ein veralteter Plan verschiebt überhaupt nichts

servers/files/plan.py verify() – der gesamte Plan wird abgelehnt, nicht eine Teilmenge

Ein hängender Befehl wird nach einer Frist beendet

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

Die Ausgabe wird begrenzt, nach der Schwärzung, niemals davor

safety/redact.py redact_and_truncate()

Der Kindprozess erhält eine bereinigte Umgebung

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

Geheimnisse werden sowohl aus der Ausgabe als auch aus den Audit-Feldern entfernt

safety/redact.py BUILTIN_PATTERNS, angewendet über einen gemeinsamen Schwärzer

Das Audit-Protokoll ist standardmäßig fail-closed

safety/audit.py AuditLogger.record() – ein nicht beschreibbares Protokoll löst eine Ausnahme aus, und die Operation findet nie statt

Jede Ablehnung wird mit einem Grund protokolliert

servers/*/server.py _serve() – der einzige Ergebnis-Schreibpunkt pro Werkzeug


Bedrohungsmodell – ausdrücklich nicht im Geltungsbereich

  • Ein gefährlicher Befehl, den Sie auf die Zulassungsliste gesetzt haben. Wenn Sie einen Interpreter oder ein shell-ähnliches Werkzeug (bash, python, sh, find -exec, awk, env, …) zulassen, kann das Modell alles tun, was dieses Werkzeug kann. Die Stärke der Richtlinie liegt vollständig in der Zulassungsliste des Betreibers.

  • Kernel-/Sandbox-Escapes. Das Jail ist eine Pfad-Containment-Prüfung, keine Kernel-Sandbox – keine Namespaces, cgroups oder seccomp. Siehe „Ehrliche Einschränkungen“.

  • Host-Zugriff auf die Zustandsdateien. Das Audit-Protokoll und das Undo-Journal sind manipulationssicher gegenüber dem Server, nicht manipulationssicher gegenüber jedem mit Host-Dateisystemzugriff.

  • Vollständigkeit der Schwärzung. Musterbasiert und nach bestem Bemühen; eine Geheimnisform, die die Muster nicht erkennen, wird durchgelassen.

  • Multi-Tenant-Identität oder Ratenbegrenzung. Es gibt keine Authentifizierung pro Aufrufer in beiden Servern – die Vertrauensgrenze ist „wer diesen Prozess starten kann“, was ein MCP-Client durch das Starten durchsetzt, nicht dieser Code.

  • Ein Wettlauf zwischen der Validierung eines Pfads und der Aktion darauf (TOCTOU). Siehe „Ehrliche Einschränkungen“ unten.


Im Vergleich zu einem naiven MCP-Server

Viele schnelle MCP-Server umhüllen subprocess.run(cmd, shell=True) für den Terminalzugriff und os.rename für Dateiverschiebungen. Beides ist bequem und unsicher. Diese Tabelle ist sachlich, kein Anspruch auf perfekte Sicherheit.

Bedenken

Naiver MCP-Server

safe-mcp-suite

Befehlsausführung

subprocess.run(cmd, shell=True) – alles, was die Shell parsen kann

nur argv, shell=False, standardmäßig verweigernde Zulassungsliste, Sperrliste gewinnt

Shell-Metazeichen

Interpretiert (;, |, $(), Umleitung)

Abgelehnt vor jeder Richtlinienprüfung

Dateiverschiebungen

os.rename überall, wo der Prozess hinkommt

Auf einen Arbeitsbereich beschränkt; ein Symlink an einem Ende wird abgelehnt, niemals verfolgt

Überschreiben einer Datei

Normalerweise still – POSIX rename ersetzt das Ziel

Immer abgelehnt; keine nummerierte Variante (report(1).pdf) wird jemals erfunden

Rückgängig machen

Keine

Jede Verschiebung wird protokolliert; undo_last_action / redo_last_action

Löschen von Dateien

Oft vorhanden, oft ungeschützt

Es gibt kein Löschwerkzeug in diesem Server, Punkt.

Umgebung, die einem Kindprozess gegeben wird

Vollständige Elternumgebung, einschließlich Geheimnisse

Bereinigt auf PATH / HOME / LANG

Geheimnisse in Ausgabe oder Protokollen

Durchgereicht

Geschwärzt, vor der Kürzung, sowohl in der Antwort als auch im Audit-Protokoll

Prüfbarkeit

Standardmäßig keine

Nur-Anhängen-JSONL, standardmäßig fail-closed

Automatische vs. ausdrücklich angeforderte Aktion

Ein Codepfad behandelt beide gleich

plan_organize (unaufgefordert) ist an beide Regelwerke gebunden; move_file (eine benannte Anfrage) ist nur an die Sicherheitsinvarianten gebunden

Beide Werkzeuge geben ein OperationResult zurück:

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)command gegen die Richtlinie auswerten und, falls erlaubt, sandboxed ausführen. cwd ist optional und muss innerhalb des Jail-Roots aufgelöst werden; ein Traversal, Symlink oder absoluter Pfad, der entkommt, gibt PATH_ESCAPE zurück, ohne etwas auszuführen. Jeder Aufruf wird protokolliert; bei fail-closed-Protokollierung gibt ein nicht beschreibbares Protokoll AUDIT_UNAVAILABLE zurück, anstatt unprotokolliert auszuführen. Ein Exit-Code ungleich Null ist immer noch ok: true – der Befehl wurde ausgeführt; ob er erfolgreich war, ist seine eigene Sache.

  • explain_command(command: str) – der Probelauf. Erreicht dieselbe Analyse und Auswertung wie run_command und kehrt vor dem Ausführenden zurück, sodass detail niemals stdout, stderr oder einen Exit-Code enthält – nichts wurde ausgeführt.

Ergebniscodes, die dieser Server zurückgeben kann: OK, POLICY_DENIED, INVALID_REQUEST, PATH_ESCAPE, TIMEOUT, OUTPUT_TRUNCATED, OPERATION_FAILED, AUDIT_UNAVAILABLE, INTERNAL_ERROR.

Sechs Werkzeuge, und die Liste ist das Design – es gibt kein siebtes, und keines davon löscht.

  • list_files(subdir: str | None = None) – schreibgeschützt. Meldet den Namen, die Größe, die Kategorie jedes Eintrags, ob der Organisator ihn verschieben würde, und warum nicht, wenn er es nicht täte.

  • plan_organize() – schlägt Verschiebungen und Überspringungen vor. Ändert nichts, nicht einmal Zielordner. Gibt eine plan_id zurück, die an apply_plan übergeben wird.

  • apply_plan(plan_id: str) – führt einen Plan aus. Jede Datei wird zuerst erneut geprüft; wenn sich seit der Planung etwas geändert, verschoben oder verschwunden hat, wird der gesamte Plan abgelehnt. Einmalig verwendbar – eine ID kann nicht erneut abgespielt werden.

  • move_file(src: str, dest: str) – verschiebt eine benannte Datei. dest ist der vollständige Zielpfad, kein Ordner. Ein Ziel, das bereits existiert, wird abgelehnt, niemals überschrieben und niemals umbenannt. Gehorcht nur den PROTECTION-Regeln – siehe „Das zweischichtige Modell“ oben.

  • undo_last_action() – macht die letzte Verschiebung oder den letzten angewendeten Plan als eine Aktion rückgängig. Nichts wird überschrieben, um Platz für eine wiederhergestellte Datei zu schaffen.

  • redo_last_action() – wendet die zuletzt rückgängig gemachte Aktion erneut an. Der Redo-Stapel wird geleert, sobald neue Arbeit aufgezeichnet wird.

Ergebniscodes, die dieser Server zusätzlich zurückgeben kann: NEEDS_EXPLICIT_REQUEST (alle Sicherheitsinvarianten erfüllt, nur abgelehnt, weil unaufgefordertes Handeln eine Vermutung wäre – benennen Sie das Ziel direkt und fragen Sie).

Eine Datei, policy.toml, wird von beiden Servern gelesen:

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 = "..."

Die Richtliniendatei selbst hat keinen Standardort. Weisen Sie mit --config darauf hin:

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

Umgebungsvariablen werden weiterhin unterstützt und jede überschreibt den passenden policy.toml-Schlüssel. SAFE_MCP_POLICY_FILE ist die eine Ausnahme, die erwähnenswert ist: Es ist eine Alternative zu --config, keine Überschreibung davon – --config gewinnt, wenn beide angegeben sind, und der Start verweigert, wenn keines angegeben ist.

Variable

Bedeutung

Standard

SAFE_MCP_POLICY_FILE

Pfad zu policy.toml (erforderlich, hier oder --config)

keiner — Start wird verweigert

SAFE_MCP_JAIL_ROOT

Terminal-Jail-Verzeichnis (erforderlich, hier oder jail_root)

keiner — Start wird verweigert

SAFE_MCP_WORKSPACE_ROOT

Arbeitsbereichsverzeichnis für Dateien (erforderlich, hier oder files.workspace_root)

keiner — Start wird verweigert

SAFE_MCP_FILES_JOURNAL

Undo/Redo-Journalpfad (muss außerhalb des Arbeitsbereichs liegen)

organizer-journal.json

SAFE_MCP_AUDIT_LOG

Gemeinsamer Audit-Pfad (muss außerhalb beider Jails liegen)

audit.jsonl

SAFE_MCP_AUDIT_FAIL_MODE

closed oder open

closed

Der Start schlägt laut fehl — eine gedruckte fatal:-Meldung und ein Exit-Code ungleich Null — wenn überhaupt kein Richtliniendateipfad angegeben ist (weder --config noch SAFE_MCP_POLICY_FILE), eine fehlende oder ungültige policy.toml, ein nicht gesetztes oder kein Verzeichnis darstellendes Jail-/Arbeitsbereichs-Root, ein ungültiger Operator-Redaktions-Regex, ein unbeschrifteter [[files.skip]]-Eintrag oder ein Audit-Protokoll/Journal, das sich innerhalb eines Jails befindet, das der Server dann verschieben oder fälschen könnte.


Ehrliche Einschränkungen

Dies ist eine Härtungsschicht, kein Tresor. Lesen Sie diese, bevor Sie einen der beiden Server bereitstellen.

  • Das Jail ist eine Pfad-Eindämmungsprüfung, keine Kernel-Sandbox. Keine Namespaces, cgroups oder seccomp. Ein Kernel-Exploit oder eine aus einem zugelassenen Binärprogramm erreichbare Fluchtmöglichkeit ist nicht eingedämmt.

  • Das Audit-Protokoll ist manipulationssicher, nicht manipulationsfest, und hat keine Rotation. Append-only mit Flush + fsync pro Datensatz bedeutet, dass es bei einem Absturz keine Datensätze verliert, aber jeder mit Host-Dateisystemzugriff auf audit.jsonl kann es lesen, ändern oder löschen — und die Datei wächst unbegrenzt; es gibt keine eingebaute Rotation oder Aufbewahrungsrichtlinie.

  • Die Redaktion ist musterbasiert und nach bestem Bemühen. Sie erfasst häufige Geheimnisformen; ein neuartiges oder ungewöhnliches Format passiert unredigiert. Der optionale Entropie-Fallback ist standardmäßig deaktiviert, weil er bei git-SHAs, UUIDs und base64-Daten verrauscht ist, nicht weil er schwach ist.

  • Terminal-Metazeichen werden auch innerhalb von Anführungszeichen abgelehnt — eine bekannte Grenze. echo "a;b" wird abgelehnt, obwohl das ; innerhalb der Anführungszeichen inert ist, weil die Prüfung eine rohe Teilstring-Prüfung ohne Kenntnis von Anführungszeichen ist. Das ist die sichere Richtung, um falsch zu liegen — es gibt keinen Anführungszeichen-Trick, der einen Operator an einer Prüfung vorbeibringt, die Anführungszeichen von vornherein ignoriert — aber es bedeutet, dass einige legitime Eingaben abgelehnt werden.

  • TOCTOU: Ein Pfad, der validiert und dann bearbeitet wird, kann sich dazwischen ändern. Sowohl safety/paths.py als auch servers/files/apply.py prüfen Eindämmung oder Belegung und handeln dann auf einem separaten Syscall; ein ausgetauschter Symlink oder eine in dieser Lücke erstellte Datei ist nicht abgedeckt. Im Code als bewusste, benannte Grenze dokumentiert (# NOTE:-Kommentare in beiden Dateien), mit einem Upgrade-Pfad (O_NOFOLLOW plus dir-fd-relative Operationen) für den Fall, dass er jemals benötigt wird.

  • Enkelprozesse werden nicht eingesammelt. Die eigenen Kindprozesse eines beendeten oder abgelaufenen Befehls befinden sich nicht in einer separaten Prozessgruppe; der Ausführer tötet nur das direkte Kind, sodass alles, was dieser Befehl erzeugt hat, ihn überleben kann.


Verwandte Arbeiten


Lizenz

MIT — siehe 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