Skip to main content
Glama
Asaad-Suliman

safe-mcp-suite

safe-mcp-suite

하나의 안전 코어(safety core)를 공유하는 두 개의 MCP 서버 — 터미널 서버와 파일 정리 서버. 어느 쪽도 이 코어를 우회할 수 없다.

Python 3.12+ License: MIT CI MCP


이 프로젝트가 무엇인가

GitHub에서 셸 명령을 실행하는 MCP 서버를 검색해 보면, 똑같은 파일이 계속 나온다. @mcp.tool()로 데코레이트된 도구 하나, subprocess.run(command, shell=True) 호출 하나, 그리고 그 결과를 그대로 모델에게 돌려주는 코드. 파일시스템 서버도 비슷한 형태다 — 루프 안의 os.rename, 어쩌면 그 주변에 try/except 하나.

그것들은 작동한다. 그게 문제다. 모델이 아무도 예상하지 못한 무언가를 만들어낼 때까지는 작동한다. 그리고 그 시점에는 이미 삭제가 일어난 뒤이고, 무엇이 실행되었는지, 왜 허용되었는지에 대한 기록은 없다.

이 저장소는 그 두 서버를 안전이 설계 자체가 되도록, 즉 겉에 덧붙인 래퍼가 아니라 설계의 일부가 되도록 재구축한 것이다. 하나의 safety/ 패키지가 당신에게 해를 끼칠 수 있는 모든 결정을 소유한다 — 무엇이 허용되는지, 경계가 어디인지, 무엇이 기록되는지, 무엇이 숨겨지는지. 그 아래의 두 서버는 배선(wiring)에 불과하다. 어느 쪽도 코어를 넘어설 수 없다. 어느 쪽도 자체 규칙을 구현하지 않기 때문이다.

그 뒤에 깔린 가정: 결정론적 정책은 모델의 판단보다 더 신뢰할 만하다. 프롬프트는 모델을 조심하지 않도록 설득할 수 있다. 그러나 경로 포함 검사(path-containment check)가 True를 반환하도록 설득할 수는 없다.


Related MCP server: Safe Terminal MCP Server

빠른 시작

Python 3.12+와 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

마지막 스크립트가 sandbox/terminal, sandbox/files, state/를 시드(seed)하여 두 서버가 합법적으로 작동할 곳을 마련한다. 그런 다음 원하는 서버를 시작하라:

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

--config 플래그에 대하여

이 플래그는 선택 사항이 아니며, 폴백(fallback)도 없다. 암묵적인 ./policy.toml 탐색도, .env 로딩도, 조용히 홈 디렉터리를 가리키는 기본 루트도 없다. --configSAFE_MCP_POLICY_FILE도 설정되지 않으면, 서버는 이유를 출력하고 종료한다.

이것은 의도적이며, 이 설정에서 가장 강한 주장이 담긴 부분이다. 조용히 어딘가 편리한 곳을 기본값으로 삼는 샌드박스는 언젠가 조용히 비싼 곳을 기본값으로 삼는 샌드박스가 된다. 시작을 거부하는 것은 가능한 가장 저렴한 실패 방식이다.

저장소에는 두 개의 정책 파일이 포함되어 있으며, 서로 대체할 수 없다:

파일

무엇인가

시작되나?

policy.example.toml

작동하는 예제, ./sandbox를 루트로 함

예 — 지금 실행 가능

policy.toml

루트가 주석 처리된 주석 달린 템플릿

아니요, 의도적으로

policy.tomljail_rootworkspace_root를 직접 채울 때까지 시작을 거부한다. 그 거부는 기능이며, 이를 지키는 회귀 테스트(regression test)가 있다. 복사하고, 편집하고, 준비가 되면 실제 디렉터리를 가리키게 하라.

원한다면 루트를 환경 변수에서 가져올 수도 있다:

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

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

데모

아래의 모든 것은 실제 캡처된 출력이다. 여기에는 손으로 쓰거나, 다듬거나, 사후에 미화한 것이 하나도 없다 — 이는 ./scripts/make_demo_sandbox.sh로 시드된 샌드박스와 policy.example.toml을 사용하여, 두 서버와 대화하는 실제 MCP 클라이언트가 반환한 실제 OperationResult 봉투(envelope)들이다.

각 항목의 code 필드를 읽어 보라. 그 분류 체계가 핵심이다: 성공이든 거부든 모든 결과가 동일한 형태로 돌아온다.

터미널 서버

허용된 명령:

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

거부된 명령:

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

거부가 아닌 것이 무엇인지 주목하라: traceback도, 발생한 예외도, 모델이 추측해야 하는 문자열도 아니다. 이유가 첨부된 타입화된 코드다.

파일 서버

이 시퀀스는 일반 파일, 설치 프로그램, 도트파일(dotfile), 심볼릭 링크가 포함된 시드된 워크스페이스에서 실행된다 — 정책이 다르게 취급하는 각 유형이 하나씩.

plan_organize가 이동을 제안하고 건너뛸 항목을 나열:

>>> 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이 동일한 계획을 실행:

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

이제 흥미로운 쌍이다. 같은 도구, 두 개의 명명된 대상, 두 개의 다른 답변.

move_file이 PROTECTION에 의해 거부됨 — 위에서 plan_organize가 건너뛴 바로 그 심볼릭 링크를 명시적으로 지정:

>>> 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_fileplan_organizeNEEDS_EXPLICIT_REQUEST로 미룬 설치 프로그램을 명시적으로 지정하여 성공:

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

플래너는 둘 다 거부했다. 직접 물었을 때, 무버(mover)는 하나를 거부하고 다른 하나를 수행했다. 그 차이는 불일치가 아니다 — 그것은 2계층 모델이며, 아래에 별도 섹션이 있다.


시스템 설계

서버는 배선이지 정책이 아니다

어느 서버 파일에도 안전 규칙이 없다. 모든 규칙은 safety/에 있으며, 두 서버 모두 동일한 함수에 도달하여 동일한 답을 얻는다.

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

이것은 그 자체를 위한 정돈이 아니다. 보안 수정이 정확히 한 곳에만 적용된다는 뜻이며, 이 저장소를 감사하는 검토자가 safety/를 읽으면 끝이라는 뜻이다. 서버 모듈에 숨어서 첫 번째 구현과 조용히 어긋나가는 두 번째 구현이 없다.

요청이 실제로 흐르는 방식

  1. 파싱(Parse). 명령 문자열이 argv 목록으로 분할된다. 셸 메타문자는 무엇이든 해석되기 전에 여기서 거부된다 — 따옴표 안의 것까지 포함해서. 마지막 부분은 의도적으로 보수적이며, 문서화된 한계이지 실수나 누락이 아니다.

  2. 평가(Evaluate). 정책 엔진이 허용 또는 거부를 답한다. 거부가 항상 우선한다. 명시적으로 허용되지 않은 것은 모두 거부되므로, 빈 정책은 열린 서버가 아니라 쓸모없는 서버다.

  3. 포함(Contain). 모든 경로가 해석되어 jail 루트에 대해 검사된다. 심볼릭 링크는 링크로서 검사되며 절대 통과하여 따라가지 않는다.

  4. 실행(Act). subprocess.runshell=False로, 정확히 PATH, HOME, LANG만 남긴 정화된 환경, 타임아웃, 출력 상한과 함께 실행한다. 또는 파일 쪽에서는 저널(journal)에 기록되는 단일 보호된 이동(move)을 수행한다.

  5. 리댁트(Redact), 그런 다음 잘라내기(truncate). 이 순서로, 항상.

  6. 기록(Record). 감사 로그에 추가한다. 그 쓰기가 실패하면, 작업도 함께 실패한다.

훔칠 가치가 있는 여섯 가지 결정

오류는 데이터이지 예외가 아니다. 모든 결과는 타입화된 ResultCode를 가진 OperationResult다. 어떤 traceback도 클라이언트에 도달하지 않는다. 스택 트레이스를 받은 모델은 그 주변을 우회하려 할 것이다. POLICY_DENIED를 받은 모델은 유용하고 모호함 없는 정보를 받은 것이다.

거부가 항상 우선한다. 허용 규칙과 거부 규칙은 가중치가 부여되거나 순서로 결정되지 않는다. 거부가 매치되면 답은 '아니요'다. 인자 규칙은 순서와 무관하게 토큰을 매치하므로, 플래그를 재배열하는 것은 우회가 아니다.

명시적 루트 없이는 아무것도 시작하지 않는다. 빠른 시작에서 다루었지만, 설계 목록에도 속한다. "합리적인 기본값"이 대부분의 샌드박스가 첫 탈출구를 얻는 방식이기 때문이다.

감사 로그는 당신을 멈출 수 있다. 기본적으로 fail-closed다: 로그를 쓸 수 없으면 작업은 일어나지 않는다. SAFE_MCP_AUDIT_FAIL_MODE로 fail-open으로 전환할 수 있으며, 그것은 누군가가 볼 수 있는 설정 파일에서 의도적으로 내리는 결정이다.

리댁트 후 잘라내기. 이 둘을 뒤집으면 64KB 출력 상한이 비밀을 반으로 자르고, 어떤 패턴도 매치하지 않은 채 첫 번째 조각을 내보낼 수 있다. 이것은 누출의 한 전체 범주를 닫는 두 줄짜리 순서 선택이다.

엔트로피 기반 비밀 탐지는 기본적으로 꺼져 있다. 해시, UUID, base64 페이로드에서 끊임없이 발동한다. 늑대를 부르는 리댁터는 사용자 스스로에 의해 비활성화되며, 그것은 한계를 미리 인정하는 것보다 더 나쁘다.

PROTECTION vs RESTRAINT

건너뛰기 규칙은 두 계층으로 나뉘며, 모든 규칙은 자신이 어느 계층에 속하는지 선언한다.

PROTECTION은 예외 없이 모든 도구에 의해 강제된다. 그중 일부만 선언된 규칙이다 — unsafe-name은 파일 이름의 제어 문자를 다룬다. 나머지는 구조적이다: 경로 이탈은 safety/paths.py에서 포함 검사에 실패하고, 점유된 대상은 servers/files/apply.pylstat 검사에 실패하며, 심볼릭 링크는 어떤 허용 규칙에도 매치하지 않아 deny-by-default로 떨어진다 — 그리고 evaluate_layered는 이를 의도적으로 PROTECTION으로 분류한다. 아래 주석을 참조하라. 어느 쪽이든 플래그도, 오버라이드도, "내가 뭘 하는지 안다"는 인자도 없다. 이것들은 불변식(invariants)이다.

RESTRAINTplan_organize에 의해서만 강제된다. 설치 프로그램, 애플리케이션 폴더, 시스템 파일, 숨김 파일, 디렉터리. 이것들은 위험한 것이 아니다 — 자동 분류기가 당신을 대신해 추측해서는 안 되는 것들이다. 플래너는 이를 NEEDS_EXPLICIT_REQUEST로 보고하고 넘어간다.

그 결과가 위의 데모 쌍이다. move_file은 당신이 직접 이름을 지정한 설치 프로그램은 이동한다. 그것을 거부하는 것은 안전이 아니라 가부장적 태도이기 때문이다. 그러나 아무리 명시적으로 요청해도 심볼릭 링크는 이동하지 않는다. 그것은 포함(containment) 문제이기 때문이다.

명시적으로 짚고 넘어갈 한 가지 뉘앙스: 심볼릭 링크의 거부 이유는 "'organize' is not permitted by any rule (deny by default)"로 표시되며 "symlink"를 언급하는 어떤 것도 아니다. 심볼릭 링크는 어느 계층의 허용 규칙에도 매치하지 않으므로 deny-by-default로 떨어지며 — 그리고 safety.policy.evaluate_layered는 매치되지 않은 deny-by-default 폴스루를 의도적으로 PROTECTION으로 분류한다. 정책이 표현할 어휘가 없는 것에 대한 가장 엄격한 해석으로서 말이다. 일반적인 문구는 더 약한 보장이 아니다. 위에서 move_file이 심볼릭 링크를 직접 명명한 것이 그 증거다.

어디서든 반복하고 싶은 구현 세부 사항 하나: PROTECTION 집합은 전체 규칙 집합을 필터링하여 파생되며, 결코 자체 목록으로 조립되지 않는다. 손으로 유지보수하는 두 목록은 어긋나며(drift), 여기서 어긋남의 실패 모드는 보호 규칙이 조용히 사라지는 것이다. 필터는 잊을 수 없다.

테스트에 관하여

661개의 테스트, 모두 밀폐형(hermetic)이다. 어떤 테스트도 실제 상태를 쓰지 않는다: 실행하는 것은 모두 tmp_path에 대해 수행되며, 배포된 policy.tomlpolicy.example.toml을 읽는 두 스위트는 먼저 tmp_path로 복사한다. 실제 감사 로그나 워크스페이스는 절대 건드리지 않는다. CI는 모든 푸시와 풀 리퀘스트에서 테스트 스위트와 gitleaks 스캔을 실행한다.

테스트를 하나 추가하는 순간 그 숫자는 낡은 것이 된다. 중요한 속성은 개수가 아니라 격리성이다.


보장(Guarantees)

각 행은 이를 강제하는 모듈과 함수를 명명한다. 여기의 주장이 열어볼 수 있는 코드로 뒷받침되지 않는다면, 그 표에 있어서는 안 된다.

보장 사항

적용 방식

두 서버 모두 기본 거부(deny by default)

safety/policy.py evaluate() — 일치하는 규칙이 없으면 거부

일치하는 거부(deny)는 항상 일치하는 허용(allow)보다 우선

safety/policy.py evaluate()가 모든 일치 항목을 검사하며, 거부가 단락(short-circuit)됨

터미널 서버에 셸 없음

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

셸 메타문자는 정책 검사 전에 거부됨

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

명령은 단일 디렉터리에 격리(jail)됨

safety/paths.py PathJail.resolve, cwd에서 검사됨

파일 이동은 단일 작업공간에 격리되며, 최종 구성요소에서 심볼릭 링크 안전

safety/paths.py PathJail.resolve_for_write — 부모만 해석하며, 쓰여지는 이름의 링크는 절대 따라가지 않음

2계층 파일 정책: 안전 불변식 vs 추측 회피

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

대상이 조용히 덮어써지는 일은 없음

servers/files/apply.py move_one() — 모든 이름 변경 전에 lstat으로 검사됨

삭제 도구는 보호 여부와 무관하게 존재하지 않음

servers/files/server.py — 여섯 개 도구, 그중 어느 것도 삭제가 아님; 어떤 인자도 삭제를 만들지 않음

모든 이동은 실행 취소 가능하며, 적용된 전체 계획도 포함

servers/files/journal.py + undo_last_action / redo_last_action

오래된(stale) 계획은 아무것도 이동하지 않음

servers/files/plan.py verify() — 부분 집합이 아닌 전체 계획이 거부됨

응답 없는 명령은 데드라인에 종료됨

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

출력은 편집(redaction) 이후에만, 절대 그 전에 상한 적용됨

safety/redact.py redact_and_truncate()

자식 프로세스는 정화된 환경을 받음

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

비밀은 출력과 감사 필드 모두에서 편집됨

safety/redact.py BUILTIN_PATTERNS, 단일 공유 편집기를 통해 적용됨

감사 추적은 기본적으로 실패 시 폐쇄(fail-closed)됨

safety/audit.py AuditLogger.record() — 쓸 수 없는 로그는 예외를 발생시키고, 작업은 절대 실행되지 않음

모든 거부는 이유와 함께 기록됨

servers/*/server.py _serve() — 도구별 단일 결과 기록 지점


위협 모델 — 명시적으로 범위 외

  • 사용자가 허용 목록에 추가한 위험한 명령. 인터프리터나 셸 유사 도구 (bash, python, sh, find -exec, awk, env, …)를 허용하면, 모델은 그 도구가 할 수 있는 모든 것을 할 수 있습니다. 정책 강도는 전적으로 운영자의 허용 목록에 달려 있습니다.

  • 커널 / 샌드박스 탈출. 격리는 경로 포함 검사이지 커널 샌드박스가 아닙니다 — 네임스페이스, cgroup, seccomp가 없습니다. "정직한 한계"를 참조하세요.

  • 상태 파일에 대한 호스트 접근. 감사 로그와 실행 취소 저널은 서버에 대해 변조 감지(tamper-evident)가 가능할 뿐, 호스트 파일시스템 접근 권한이 있는 사람에 대해 변조 방지(tamper-proof)는 아닙니다.

  • 편집의 완전성. 패턴 기반이며 최선 노력(best-effort)입니다. 패턴이 인식하지 못하는 비밀 형태는 통과합니다.

  • 멀티테넌트 신원 인증 또는 속도 제한. 두 서버 내부에는 호출자별 인증이 없습니다 — 신뢰 경계는 "이 프로세스를 시작할 수 있는 사람"이며, MCP 클라이언트가 프로세스를 실행함으로써 이를 강제하지, 이 코드가 강제하지 않습니다.

  • 경로 검증과 실행 사이의 경쟁 조건(TOCTOU). 아래 "정직한 한계"를 참조하세요.


순진한 MCP 서버와의 비교

많은 빠른 MCP 서버는 터미널 접근에 subprocess.run(cmd, shell=True)를, 파일 이동에 os.rename을 감쌉니다. 둘 다 편리하고 안전하지 않습니다. 이 표는 사실에 근거하며, 완벽한 보안을 주장하는 것이 아닙니다.

우려 사항

순진한 MCP 서버

safe-mcp-suite

명령 실행

subprocess.run(cmd, shell=True) — 셸이 파싱할 수 있는 모든 것

argv만, shell=False, 기본 거부 허용 목록, 거부 목록이 우선

셸 메타문자

해석됨 (;, |, $(), 리다이렉션)

정책 검사 전에 거부됨

파일 이동

프로세스가 도달할 수 있는 모든 곳에 os.rename

단일 작업공간에 격리; 양쪽 끝의 심볼릭 링크는 거부되며, 절대 따라가지 않음

파일 덮어쓰기

보통 조용히 — POSIX rename은 대상을 교체함

항상 거부됨; 번호가 붙은 변형(report(1).pdf)이 만들어지는 일은 없음

실행 취소

없음

모든 이동이 저널링됨; undo_last_action / redo_last_action

파일 삭제

흔히 존재하며, 흔히 보호되지 않음

이 서버에는 삭제 도구가 존재하지 않음, 그 이상도 이하도 아님

자식 프로세스에 주어지는 환경

전체 부모 환경, 비밀 포함

PATH / HOME / LANG으로 정화됨

출력 또는 로그의 비밀

그대로 통과됨

응답과 감사 추적 모두에서, 잘림 전에 편집됨

감사 가능성

기본적으로 없음

추가 전용(append-only) JSONL, 기본적으로 실패 시 폐쇄

자동 vs 명시적 요청 작업

하나의 코드 경로가 둘을 동일하게 취급

plan_organize(요청 없음)는 두 규칙 계층 모두에 묶임; move_file(명명된 요청)은 안전 불변식에만 묶임

두 도구 모두 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)command를 정책에 대해 평가하고, 허용되면 샌드박스에서 실행합니다. cwd는 선택 사항이며 격리 루트 내부로 해석되어야 합니다. 탈출하는 트래버설, 심볼릭 링크 또는 절대 경로는 아무것도 실행하지 않고 PATH_ESCAPE를 반환합니다. 모든 호출은 감사됩니다. 실패 시 폐쇄 감사에서는 쓸 수 없는 로그가 기록 없이 실행하는 대신 AUDIT_UNAVAILABLE을 반환합니다. 0이 아닌 종료 코드는 여전히 ok: true입니다 — 명령이 실행된 것이며, 성공 여부는 명령 자신의 몫입니다.

  • explain_command(command: str) — 드라이 런(dry run)입니다. run_command와 동일한 파싱 및 평가에 도달하고 실행기 전에 반환하므로, detail에는 stdout, stderr 또는 종료 코드가 절대 포함되지 않습니다 — 아무것도 실행되지 않았습니다.

이 서버가 반환할 수 있는 결과 코드: OK, POLICY_DENIED, INVALID_REQUEST, PATH_ESCAPE, TIMEOUT, OUTPUT_TRUNCATED, OPERATION_FAILED, AUDIT_UNAVAILABLE, INTERNAL_ERROR.

여섯 개 도구이며, 그 목록 자체가 설계입니다 — 일곱 번째는 없고, 그중 어느 것도 삭제하지 않습니다.

  • list_files(subdir: str | None = None) — 읽기 전용입니다. 각 항목의 이름, 크기, 분류, 정리기가 이동할지 여부, 그리고 이동하지 않을 경우 그 이유를 보고합니다.

  • plan_organize() — 이동과 건너뛰기를 제안합니다. 대상 폴더조차 변경하지 않습니다. apply_plan에 전달할 plan_id를 반환합니다.

  • apply_plan(plan_id: str) — 계획을 실행합니다. 모든 파일이 먼저 재검사됩니다. 계획 수립 이후 변경, 이동 또는 사라진 파일이 하나라도 있으면 전체 계획이 거부됩니다. 일회용입니다 — id는 재생(replay)될 수 없습니다.

  • move_file(src: str, dest: str) — 이름이 지정된 파일 하나를 이동합니다. dest는 폴더가 아닌 전체 대상 경로입니다. 이미 존재하는 대상은 거부되며, 덮어쓰지도 않고 이름을 바꿔 우회하지도 않습니다. PROTECTION 규칙만 따릅니다 — 위의 "2계층 모델"을 참조하세요.

  • undo_last_action() — 가장 최근의 이동 또는 적용된 계획을 하나의 작업으로 되돌립니다. 복원된 파일을 위한 공간을 만들기 위해 덮어쓰는 일은 없습니다.

  • redo_last_action() — 가장 최근에 실행 취소된 작업을 다시 적용합니다. 새 작업이 기록되면 다시 실행 스택은 비워집니다.

이 서버가 추가로 반환할 수 있는 결과 코드: NEEDS_EXPLICIT_REQUEST (모든 안전 불변식을 통과했지만, 요청 없이 행동하는 것이 추측이 될 수 있어 거부된 경우 — 대상을 직접 명명하고 요청하세요).

하나의 파일, policy.toml을 두 서버가 모두 읽습니다:

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

정책 파일 자체에는 기본 위치가 없습니다. --config로 지정하세요:

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

환경 변수는 계속 지원되며, 각각은 일치하는 policy.toml 키를 덮어씁니다. SAFE_MCP_POLICY_FILE은 언급할 가치가 있는 유일한 예외입니다: 이는 --config의 대안이지, 이를 덮어쓰는 것이 아닙니다 — 둘 다 주어지면 --config가 우선하며, 둘 다 없으면 시작이 거부됩니다.

변수

의미

기본값

SAFE_MCP_POLICY_FILE

policy.toml 경로 (필수, 여기 또는 --config)

없음 — 시작 거부

SAFE_MCP_JAIL_ROOT

터미널 격리(jail) 디렉터리 (필수, 여기 또는 jail_root)

없음 — 시작 거부

SAFE_MCP_WORKSPACE_ROOT

파일 작업 공간 디렉터리 (필수, 여기 또는 files.workspace_root)

없음 — 시작 거부

SAFE_MCP_FILES_JOURNAL

실행 취소/다시 실행 저널 경로 (작업 공간 외부에 있어야 함)

organizer-journal.json

SAFE_MCP_AUDIT_LOG

공유 감사 추적 경로 (두 격리 영역 모두 외부에 있어야 함)

audit.jsonl

SAFE_MCP_AUDIT_FAIL_MODE

closed 또는 open

closed

정책 파일 경로가 전혀 제공되지 않거나(--configSAFE_MCP_POLICY_FILE도 없는 경우), policy.toml이 없거나 유효하지 않은 경우, 격리/작업 공간 루트가 설정되지 않았거나 디렉터리가 아닌 경우, 운영자 redaction 정규식이 유효하지 않은 경우, 레이블이 없는 [[files.skip]] 항목이 있는 경우, 또는 감사 로그/저널이 나중에 이동하거나 위조할 수 있는 격리 영역 내부에 있는 경우, 시작은 fatal: 메시지 출력과 0이 아닌 종료 코드로 명확하게 실패합니다.


솔직한 한계 고지

이것은 강화 계층이지 금고가 아닙니다. 두 서버를 배포하기 전에 다음을 읽으십시오.

  • 격리(jail)는 경로 포함 검사이지 커널 샌드박스가 아닙니다. 네임스페이스, cgroups, seccomp가 없습니다. 커널 익스플로잇이나 허용 목록에 등록된 바이너리에서 도달할 수 있는 탈출 경로는 차단되지 않습니다.

  • 감사 로그는 변조 감지가 가능할 뿐 변조 방지가 아니며, 로테이션도 없습니다. 레코드별 flush + fsync가 있는 추가 전용(append-only) 방식이므로 충돌 시 레코드를 잃지 않지만, 호스트 파일시스템에서 audit.jsonl에 접근할 수 있는 사람은 누구나 읽거나, 변경하거나, 삭제할 수 있습니다. 또한 파일은 무한정 커지며, 내장된 로테이션이나 보존 정책이 없습니다.

  • Redaction은 패턴 기반이며 최선의 노력(best-effort)입니다. 일반적인 비밀 형식을 잡아내지만, 새로운 형식이나 특이한 형식은 redaction되지 않은 채 통과합니다. 선택적 엔트로피 폴백은 git SHA, UUID, base64 데이터에서 노이즈가 많기 때문에 기본적으로 꺼져 있습니다. 약해서가 아닙니다.

  • 터미널 메타문자는 따옴표 안에서도 거부됩니다 — 알려진 한계입니다. echo "a;b";가 따옴표 안에서 비활성이더라도 거부됩니다. 스캔이 따옴표를 인식하지 않는 원시 부분 문자열 검사이기 때문입니다. 이는 틀려도 안전한 방향입니다. 처음부터 따옴표를 무시하는 스캔을 우회할 수 있는 따옴표 트릭은 없기 때문입니다. 다만 일부 합법적인 입력이 거부된다는 의미이기도 합니다.

  • TOCTOU: 검증된 경로가 그 사이에 변경될 수 있습니다. safety/paths.pyservers/files/apply.py 모두 포함 또는 점유 여부를 확인한 후 별도의 syscall로 작업을 수행합니다. 그 틈에 심볼릭 링크가 교체되거나 파일이 생성되면 보호되지 않습니다. 두 파일 모두에서 의도적이고 명명된 한계(# NOTE: 주석)로 코드에 문서화되어 있으며, 필요할 경우를 대비한 업그레이드 경로(O_NOFOLLOW 및 dir-fd 상대 연산)도 명시되어 있습니다.

  • 하위 프로세스는 수거되지 않습니다. 종료되거나 시간 초과된 명령의 자식 프로세스는 별도의 프로세스 그룹에 있지 않습니다. 실행기는 직접 자식만 종료하므로, 해당 명령이 생성한 모든 것은 그보다 오래 살아남을 수 있습니다.


관련 작업


라이선스

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