safe-workspace-mcp
by Xs-trek
README.md
# safe-workspace-mcp
A minimal, security-focused [MCP](https://modelcontextprotocol.io) server that provides structured read/write access to **exactly one local workspace**, with built-in local Git checkpoints and rollback.
Designed to let a chat model (e.g. ChatGPT with MCP support) safely edit files in one project folder - and nothing else.
## Windows portable quick start (no Python, no Git, no Node)
1. Download the Windows release ZIP from [Releases](https://github.com/Xs-trek/safe-workspace-mcp/releases) and extract it.
2. Prepare a workspace directory (the one folder the server may touch).
3. Get an OpenAI **Secure MCP Tunnel ID** ([create here](https://platform.openai.com/settings/organization/tunnels)) and a **Runtime API Key** ([create here](https://platform.openai.com/settings/organization/api-keys)).
4. In the extracted folder run:
```powershell
.\Start-SafeWorkspaceMCP.ps1 -Workspace "D:\ChatGPT_Workspace\demo" -TunnelId "tunnel_..."
```
5. Enter the Runtime API Key when prompted (hidden input, never stored).
6. Keep the terminal open; `Ctrl+C` stops everything.
7. Connect the existing tunnel from ChatGPT Developer Mode - the account-side step you do yourself.
The launcher downloads the official OpenAI tunnel client (pinned `v0.0.11`, SHA-256 verified) on first run and caches it under `%LOCALAPPDATA%\SafeWorkspaceMCP\. No admin rights, no PATH/registry changes. See `README-PORTABLE.md` (or the Chinese edition `README-PORTABLE.zh-CN.md`) inside the ZIP for full details.
> Accurate claim: *portable local deployment with no Python/Git/Node installation required; the launcher bootstraps the tested OpenAI tunnel client automatically.* It is not "zero configuration" - you bring the workspace, tunnel ID, runtime key, and ChatGPT account-side setup.
## What it is
- One process = one configuration = **one fixed workspace** (chosen at startup, immutable at runtime)
- Structured text-file CRUD with atomic multi-file transactions
- Optimistic concurrency: every modification of an existing file requires its current `sha256`
- Managed local Git history (via [Dulwich](https://www.dulwich.io), never `git.exe`): pre/post-change checkpoints, diff, history, restore
- stdio MCP server, 9 tools total
## Non-goals (hard absent)
No shell, no terminal, no subprocess, no code execution, no compiler/test-runner/package-manager, no arbitrary HTTP or network tools, no remote Git, no workspace switching, no binary/image editing, no OS sandbox claims.
If a capability is not listed below, this server does not have it.
## Architecture
```
ChatGPT / any MCP client
│
OpenAI Secure MCP Tunnel (account-side, outbound-only)
│
tunnel-client.exe <- external deployment layer (official OpenAI binary,
│ pinned + SHA-256 verified by the launcher)
│ MCP over stdio (child process)
▼
Safe Workspace MCP <- this project (9 tools, no network, no exec)
│
fixed single workspace
│
┌────┴─────────────┐
│ │
structured file CRUD managed local Git checkpoints
```
- Web search / URL fetching is done by the chat host itself; this server has no network capability by design.
- The tunnel client is an external deployment component, not part of this server: the server process itself never opens sockets, and the launcher's only network activity is downloading the pinned, checksum-verified official tunnel client.
## The nine tools
| Tool | Read-only | Purpose |
|---|---|---|
| `workspace_info` | ✓ | Workspace name, limits, version |
| `list_directory` | ✓ | List one directory (internal/excluded entries hidden) |
| `read_file` | ✓ | Read UTF-8 text file → content, sha256, size |
| `search_text` | ✓ | Literal text search, capped results |
| `apply_changes` | ✗ | Atomic transaction: create/replace file, replace text, create dir, move, delete file, delete empty dir |
| `git_status` | ✓ | Working-tree changes since last checkpoint |
| `git_diff` | ✓ | Unified diff vs a checkpoint (default: last) |
| `git_history` | ✓ | Checkpoint list (newest first) |
| `git_restore` | ✗ | Restore workspace to a checkpoint (auto-checkpoints current state first, so restores are undoable) |
`apply_changes` operations all validate first (paths, hashes, policy, plan conflicts); if anything fails, **nothing** is applied. On mid-execution failure everything is rolled back.
## Installation
Two supported paths:
- **End user (Windows)**: download the portable release ZIP - no Python/Git/Node required (see quick start above).
- **Developer / Linux**: source checkout with Python 3.12+:
```
git clone https://github.com/Xs-trek/safe-workspace-mcp.git
cd safe-workspace-mcp
py -3.12 -m venv .venv
.venv\Scripts\pip install -e .
```
Runtime dependencies: `mcp==2.0.0` (official SDK), `dulwich==1.2.6`, Python stdlib. Nothing else. End-user prerequisites for the portable release are only: Windows 10/11, PowerShell, internet for the tunnel, a workspace folder, tunnel ID + Runtime API Key, and your own ChatGPT account setup.
## Configuration
TOML file, loaded once at startup, immutable afterwards. There is no tool (and no code path) that can change the configuration, the workspace root, or any limit at runtime.
```toml
[workspace]
root = "D:/ChatGPT_Workspace/demo"
max_file_bytes = 2097152 # largest file the server will write/track
max_read_bytes = 1048576 # largest read returned / searched per file
max_transaction_bytes = 10485760
max_search_results = 200
excluded = ["node_modules", "build", "dist", ".venv"] # plus built-ins
[paths]
reject_reparse_points = true # symlinks/junctions/mounts: always recommended
reject_hardlinks = true
require_same_filesystem = true
[write]
allow_create_file = true
allow_modify_file = true
allow_delete_file = true
allow_move = true
allow_create_directory = true
allow_delete_empty_directory = true
require_expected_hash = true
[git]
mode = "managed" # only mode in v0.1.1
author_name = "Safe Workspace MCP"
author_email = "safe-workspace-mcp@local"
[search]
include_hidden = false
[server]
transport = "stdio" # only transport in v0.1.1
```
See `examples/` for minimal / existing-source / large-source variants.
### Managed workspace
On first start with an **empty or plain source directory** (no `.git`), the server:
1. scans the directory (only regular text files are tracked),
2. initializes a managed repository at `<root>/.git`,
3. writes a `safe-workspace-mcp.managed-repository-format` marker into `.git/config` (so the repo is provably ours on later restarts),
4. creates the `initial snapshot` checkpoint.
On subsequent starts, the server **reopens its own managed repo** (identified by the marker). A workspace containing a foreign `.git` (plain `git init`, a clone, or a repo without the managed marker) is rejected with `EXISTING_GIT_REPOSITORY_NOT_SUPPORTED`. Adopting existing repositories, worktrees, submodules, and remotes remains out of scope.
**Editable ⇒ Recoverable**: every regular file the MCP can modify or delete is tracked in the managed repository, so it can always be restored from a checkpoint. Excluded directories (node_modules, build artifacts, virtualenvs, …) are invisible to every tool — not readable, not writable, not searched, not checkpointed.
## Running
```
.venv\Scripts\safe-workspace-mcp path\to\config.toml
```
The server speaks MCP on stdio and logs to stderr. It refuses to start if the workspace root does not exist or is unsafe.
### Multiple projects
One process serves exactly one workspace. Run several processes with several configs:
```
safe-workspace-mcp project-a.toml
safe-workspace-mcp project-b.toml
```
### Importing existing source
Point `workspace.root` at an existing source directory **without** `.git`. The initial snapshot commits the current state as the baseline; from then on the directory is managed. Large generated directories should be added to `excluded`.
## Portable usage scenarios (Windows)
- **First run on a new PC**: extract the ZIP, create/select a workspace, run the launcher, provide tunnel credentials. The launcher downloads and verifies the pinned tunnel client automatically.
- **Second run**: same launcher; the cached tunnel client is reused - no re-download, no reinstall.
- **Switching projects**: same release, different `-Workspace` path. Each MCP process still serves exactly one fixed workspace (no runtime switching).
- **Offline install (advanced)**: pre-download the official `tunnel-client-<version>-windows-<arch>.zip` yourself, verify it against the official `SHA256SUMS.txt`, and point `-TunnelClientPath` at the extracted official `tunnel-client.exe`. This is an advanced operator override: it skips the launcher's pinned SHA-256 guarantee (existence and `--version` are still checked). Not needed for normal use.
## Testing with MCP Inspector
```
npx @modelcontextprotocol/inspector .venv\Scripts\safe-workspace-mcp -- args/config.toml
```
(Or `mcp dev` from the MCP SDK CLI.) Verify `tools/list` shows exactly nine tools, the read-only annotations are correct, and exercise read → search → apply_changes → git_diff/git_history/git_restore against a disposable workspace first.
## Connecting ChatGPT Desktop / ChatGPT Web
ChatGPT reaches a local MCP server through OpenAI's Secure MCP Tunnel (Developer Mode / connectors). **This project is only the stdio server plus an operator-run launcher** - it contains no tunnel transport, no OAuth, no credentials storage, and it never reads or writes ChatGPT/Codex configuration.
Recommended flow:
1. Pass the full local test suite with a disposable workspace (see above).
2. Create a Secure MCP Tunnel in the OpenAI Platform and run the portable launcher (or `tunnel-client run` yourself) with that tunnel ID.
3. In ChatGPT, connect the existing tunnel as a developer/app connector while the launcher terminal is running.
4. Use a dedicated test workspace first, then switch the config to your real project.
Always configure ChatGPT manually in its UI.
## Security overview
- **Workspace confinement** — workspace-relative paths only; traversal, absolute/drive/UNC paths, reserved device names, ADS colons, trailing dot/space names all rejected; containment is filesystem-aware (realpath-based), never string-prefix.
- **Links** — any reparse point (symlink, junction, mount, unknown tag) in any component of an existing path ⇒ deny. Hard-linked regular files (st_nlink > 1) ⇒ deny.
- **Internal isolation** — `.git` is inaccessible through every file tool; it is only touched by the managed Git store.
- **Atomic writes** — temp sibling → fsync → validate → `os.replace`; a failed write never truncates the original.
- **Optimistic concurrency** — stale `expected_sha256` ⇒ `HASH_MISMATCH`, the user's newer file is never overwritten.
- **No execution / no network** — production code contains no subprocess/socket usage (AST-enforced by tests, scanning every module for imports and calls); dulwich's unconditional hook-execution path is neutralized at import time and regression-tested with planted hook files; the managed repo never gets hooks, filters, or remotes.
- **Resource limits** — max file/read/transaction bytes and search results; reaching a limit fails closed.
- **Prompt injection** — not solved, contained: a misled model can only perform structured, checkpointed file edits inside one folder, which you can always roll back.
See `SECURITY.md` and `THREAT_MODEL.md` for the full analysis and residual risks.
## Known limitations (v0.1.1)
- Text (UTF-8) files only; binary files are refused.
- Windows is the primary security target; Linux is supported and CI-tested.
- No concurrent multi-client coordination beyond hash checks (run one writer).
- Checkpoint history grows unboundedly (no gc in v0.1.1).
- Restores are file-level; excluded directories are untouched by restore.
## Security reporting
Please open a private security advisory (GitHub "Report a vulnerability") rather than a public issue.
## License
Apache-2.0 — see `LICENSE`.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessUnresponsive