clawdforge/clients/mcp
Kayos 093021cb36 clients/mcp: initial MCP server for clawdforge
Drops a Model Context Protocol server into clients/mcp/ that wraps the
clawdforge HTTP surface so MCP-aware clients (Claude Desktop, Claude Code,
Cursor, Zed, custom agents) can call it as a native tool — claude talking
to claude through the LAN bridge.

Three tools exposed:

  - clawdforge_healthz     -> GET /healthz
  - clawdforge_run         -> POST /run
  - clawdforge_upload_file -> POST /files

Admin endpoints intentionally NOT exposed; token minting stays human-gated.

Implementation notes:

  - Built on the official `mcp` Python SDK (>=1.0). asyncio-native server,
    stdio transport, low-level Server class with @list_tools / @call_tool
    handlers.
  - Self-contained `requests` HTTP wrapper rather than depending on the
    sibling clients/python SDK — keeps clawdforge-mcp installable
    standalone. Same error taxonomy (ForgeError / ForgeAPIError /
    ForgeAuthError / ForgeTransportError).
  - Sync HTTP calls offloaded via asyncio.to_thread so a slow `claude -p`
    can't stall the MCP event loop.
  - Errors are formatted into a single 'clawdforge error: ...' text block
    with isError=True; tracebacks never leak through the JSON-RPC pipe.
  - Logging goes to stderr (CLAWDFORGE_MCP_LOG=DEBUG to enable). stdout
    is reserved for JSON-RPC framing.
  - Config via env: CLAWDFORGE_URL (default http://localhost:8800) and
    CLAWDFORGE_TOKEN (required). MCP clients pass these via their `env`
    config block.

Tests: 12 unit tests covering tool discovery, healthz, run-success,
run-with-files, run-empty-prompt, run-subprocess-502, run-auth-401,
upload happy path, upload missing file, unknown tool, server factory.
HTTP layer mocked via `responses`. Plus a manual end-to-end stdio
smoke (initialize + tools/list round-trip) verified during build.

Includes ready-to-paste Claude Desktop and Claude Code config examples,
and a README documenting install, env, all three tools, and operational
notes (stdout-is-sacred, error wrapping, no streaming).
2026-04-28 22:37:08 -07:00
..
examples clients/mcp: initial MCP server for clawdforge 2026-04-28 22:37:08 -07:00
src/clawdforge_mcp clients/mcp: initial MCP server for clawdforge 2026-04-28 22:37:08 -07:00
tests clients/mcp: initial MCP server for clawdforge 2026-04-28 22:37:08 -07:00
pyproject.toml clients/mcp: initial MCP server for clawdforge 2026-04-28 22:37:08 -07:00
README.md clients/mcp: initial MCP server for clawdforge 2026-04-28 22:37:08 -07:00

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_mcp Python package
  • a clawdforge-mcp console script (alias for python -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.

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_run calls 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 -p takes 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.

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 the clawdforge_mcp logger.
  • Errors are wrapped, not raised. Auth failures, transport errors, upstream 502s — all get formatted into a single short clawdforge error: ... text content with isError=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 to asyncio.to_thread so a slow claude -p call doesn't stall heartbeats.
  • No streaming. clawdforge_run blocks 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."