HIGH: - S1: upload_file allow-root + symlink-resolve + size-cap. Env: CLAWDFORGE_UPLOAD_ROOT (default cwd), CLAWDFORGE_UPLOAD_MAX_BYTES (default 100MiB). README updated with threat-model paragraph. LOW: - S2: logger.propagate = False (stdout discipline defense-in-depth) - S3: catch-all error message no longer echoes str(e) (host paths) - S4: whitelist healthz/upload tool response fields - S5: pattern-validate ff_* file tokens in run schema - C1: strict-bool guard on timeout_secs/ttl_secs - C2: coerce empty-string model/system to None Deps: - requests>=2.32 (CVE-2024-35195) - urllib3>=2.2.2 (CVE-2024-37891) - mcp>=1.2.0 Audit: memory/clawdforge-audits/mcp-093021c.md
7.9 KiB
clawdforge-mcp
Model Context Protocol (MCP) server that bridges to the clawdforge LAN HTTP service.
Drops the clawdforge tool surface into any MCP-aware client — Claude Desktop,
Claude Code, Cursor, Zed, custom agents — so the model can delegate sub-tasks
to a separate Claude context window via claude -p. "Claude talking to
Claude," with the auth living in one place on the LAN.
What it exposes
| Tool | Backed by | Use it for |
|---|---|---|
clawdforge_healthz |
GET /healthz |
Verify clawdforge is up and the host's claude CLI is authenticated. |
clawdforge_run |
POST /run |
Run a one-shot prompt in a fresh Claude subprocess. Single-turn. Returns the parsed result. |
clawdforge_upload_file |
POST /files |
Stage a local file on the clawdforge host and get back a ff_... token to attach to a clawdforge_run call. |
The admin endpoints (/admin/tokens) are deliberately NOT exposed — token
minting is a human-gated operation.
Install
From a checkout of the clawdforge repo:
pip install -e clients/mcp
# or with test deps
pip install -e 'clients/mcp[test]'
This installs:
- the
clawdforge_mcpPython package - a
clawdforge-mcpconsole script (alias forpython -m clawdforge_mcp)
Configure
The server reads configuration from environment variables — your MCP client
sets these via its env block when it spawns the subprocess.
| Variable | Default | Notes |
|---|---|---|
CLAWDFORGE_URL |
http://localhost:8800 |
Override to your forge host (e.g. http://192.168.0.5:8800). |
CLAWDFORGE_TOKEN |
(required) | App bearer token (cf_...). Mint with /admin/tokens. |
CLAWDFORGE_MCP_LOG |
WARNING |
Optional. Set INFO or DEBUG for stderr logs. |
CLAWDFORGE_UPLOAD_ROOT |
process cwd | Allow-root for clawdforge_upload_file. Paths must resolve INSIDE this directory after symlink resolution. |
CLAWDFORGE_UPLOAD_MAX_BYTES |
104857600 (100 MiB) |
Hard cap on the size of a single file upload. Files exceeding this are refused before any bytes are sent. |
Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json
(macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"clawdforge": {
"command": "clawdforge-mcp",
"env": {
"CLAWDFORGE_URL": "http://192.168.0.5:8800",
"CLAWDFORGE_TOKEN": "cf_REPLACE_ME"
}
}
}
}
Or if you'd rather not rely on clawdforge-mcp being on $PATH:
{
"mcpServers": {
"clawdforge": {
"command": "/usr/bin/python3",
"args": ["-m", "clawdforge_mcp"],
"env": {
"CLAWDFORGE_URL": "http://192.168.0.5:8800",
"CLAWDFORGE_TOKEN": "cf_REPLACE_ME"
}
}
}
}
A ready-to-paste version lives at examples/claude-desktop.json.
Claude Code
Pass the config via --mcp-config:
claude --mcp-config examples/claude-code.json
examples/claude-code.json follows the same mcpServers schema as Claude
Desktop.
Cursor / Zed / others
Any client that follows the MCP server-spawn convention works the same way —
point it at the clawdforge-mcp command and pass CLAWDFORGE_URL and
CLAWDFORGE_TOKEN in the env block.
Tool reference
clawdforge_healthz
// args: none
// returns: {ok, claude_present, claude_version}
clawdforge_run
// args:
{
"prompt": "string (required)",
"model": "string (optional, default 'sonnet')",
"system": "string (optional system prompt)",
"files": ["ff_...", "..."], // optional, from clawdforge_upload_file
"timeout_secs": 60 // optional, 5..600
}
// returns: {result, duration_ms, stop_reason}
result is whatever claude -p --output-format json produced, auto-parsed
to JSON if possible, otherwise a string.
When to reach for it:
- Bounded sub-tasks that don't need to stay in your main conversation context — recipe parsing, log summarization, diff classification.
- Different system prompts — e.g. spawn a strict JSON-only sub-Claude for one extraction step.
- Cheap parallelism in spirit — a sequence of
clawdforge_runcalls is fine; each gets its own context window.
When NOT to reach for it:
- Long multi-turn conversations.
- Anything that needs streaming or partial output.
- Trivial prompts where the model can just answer in-context —
claude -ptakes seconds even for one-liners.
clawdforge_upload_file
// args:
{
"path": "/abs/or/relative/path/on/host",
"ttl_secs": 3600 // optional, 60..86400
}
// returns: {file_token, ttl_secs, size}
Path is interpreted on the host running the MCP server (typically the user's workstation), not whatever sandbox the LLM thinks it's in.
The path is constrained to an allow-root: CLAWDFORGE_UPLOAD_ROOT (default
the MCP server process's current working directory). Both symlinks and ..
traversal are neutralized via Path.resolve(strict=True) followed by an
is_relative_to(root) containment check. Files larger than
CLAWDFORGE_UPLOAD_MAX_BYTES (default 100 MiB) are refused before any
bytes are sent to the forge. Non-regular files (FIFOs, sockets, directories,
devices) are refused.
Threat model — why these guards exist
The MCP-specific question is: what can a malicious LLM-driven client do?
A model that has earned the user's trust can socially-engineer them into
running a tool call against any path it can name — and an MCP server that
reaches the local filesystem is a perfect exfiltration channel. The classic
shape is "let me upload your config so I can debug" pointed at
~/.ssh/id_rsa, ~/.aws/credentials, /etc/shadow, etc.
The defaults pin the upload root to the process cwd, which means an MCP
server launched from your project directory cannot reach into your home
directory at all. If you need broader access, set CLAWDFORGE_UPLOAD_ROOT
explicitly to a directory you've thought about — never to /. Symlink
resolution is mandatory: a symlink inside the root that points to /etc
will be rejected, not followed.
Testing
pip install -e 'clients/mcp[test]'
python -m pytest clients/mcp/tests
The tests stub out the HTTP layer with responses — no live clawdforge
required.
Operational notes
- stdout is sacred. The MCP transport pipes JSON-RPC frames over
stdin/stdout. Any stray
print()in the server process corrupts the stream. All diagnostics go to stderr via theclawdforge_mcplogger. - Errors are wrapped, not raised. Auth failures, transport errors,
upstream 502s — all get formatted into a single short
clawdforge error: ...text content withisError=True. Callers see a clean message, not a Python traceback. - Sync calls under async. The MCP SDK is asyncio; our HTTP client is
blocking
requests. Each tool offloads toasyncio.to_threadso a slowclaude -pcall doesn't stall heartbeats. - No streaming.
clawdforge_runblocks the MCP request until the subprocess returns. MCP clients handle this fine — it's a normal long-running tool call.
Why this exists
clawdforge centralizes the Claude CLI subscription auth on one LAN host so
every Sulkta service doesn't need its own login. MCP is the natural
integration layer: any MCP client can now treat clawdforge as a native
tool surface and call claude -p indirectly. Cobb's framing: "may as
well let claude talk to claude."