# crafting-table-mcp Model Context Protocol (MCP) server that bridges to the crafting-table HTTP service (default port 8810). Drops the crafting-table tool surface into any MCP-aware client — Claude Desktop, Claude Code, Cursor, Zed, custom agents — so the model can register projects, kick off audit/build/test jobs, and read structured findings as native tools. Same auth lives in one place on the LAN. ## What it exposes | Tool | Backed by | Use it for | | ----------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------- | | `crafting_table_list_projects` | `GET /projects` | Discover registered project names visible to this token. | | `crafting_table_register_project` | `POST /projects` | Register a new project. Argument is `project_json` — a JSON-encoded string of the Project shape. | | `crafting_table_run_audit` | `POST /projects/{name}/jobs` | Kick off an `audit` recipe job. Returns `{job_id, status}`. | | `crafting_table_run_build` | `POST /projects/{name}/jobs` | Kick off a `build` recipe job. | | `crafting_table_run_test` | `POST /projects/{name}/jobs` | Kick off a `test` recipe job. | | `crafting_table_get_job` | `GET /jobs/{id}` | Poll a job for state + log tail. Two content blocks: prose summary + full JSON. | | `crafting_table_get_findings` | `GET /jobs/{id}/findings` | Read structured findings (CVEs, lints, test failures) once a job has finished. | | `crafting_table_draft_patch` | (wave 3 / step 9) | STUB — returns "not yet implemented". The surface is stable across waves so callers won't regress. | The admin endpoints (`/admin/tokens`) are deliberately NOT exposed — token minting is a human-gated operation. ## Install From a checkout of the crafting-table repo: ```sh pip install -e mcp # or with test deps pip install -e 'mcp[test]' ``` Once published: ```sh pip install crafting-table-mcp ``` This installs: - the `crafting_table_mcp` Python package - a `crafting-table-mcp` console script (alias for `python -m crafting_table_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 | | ------------------------------ | ------------------------ | ----------------------------------------------------------- | | `CRAFTING_TABLE_BASE_URL` | `http://localhost:8810` | Override to your crafting-table host (e.g. `http://crafting-table.internal:8810`). | | `CRAFTING_TABLE_TOKEN` | (required) | App bearer token (`ct_...`). Mint with `POST /admin/tokens` against the live service. | | `CRAFTING_TABLE_MCP_LOG` | `WARNING` | Optional. Set `INFO` or `DEBUG` for stderr logs. | ### Quickstart ```bash pip install crafting-table-mcp export CRAFTING_TABLE_BASE_URL=http://crafting-table.internal:8810 export CRAFTING_TABLE_TOKEN=ct_... crafting-table-mcp # stdio JSON-RPC server ``` ### Claude Desktop Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): ```json { "mcpServers": { "crafting-table": { "command": "crafting-table-mcp", "env": { "CRAFTING_TABLE_BASE_URL": "http://crafting-table.internal:8810", "CRAFTING_TABLE_TOKEN": "ct_REPLACE_ME" } } } } ``` Or if you'd rather not rely on `crafting-table-mcp` being on `$PATH`: ```json { "mcpServers": { "crafting-table": { "command": "/usr/bin/python3", "args": ["-m", "crafting_table_mcp"], "env": { "CRAFTING_TABLE_BASE_URL": "http://crafting-table.internal:8810", "CRAFTING_TABLE_TOKEN": "ct_REPLACE_ME" } } } } ``` A ready-to-paste version lives at `examples/claude-desktop.json`. ### Claude Code Pass the config via `--mcp-config`: ```sh 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 `crafting-table-mcp` command and pass `CRAFTING_TABLE_BASE_URL` and `CRAFTING_TABLE_TOKEN` in the env block. ## Tool reference ### `crafting_table_list_projects` ```jsonc // args: none // returns: [ // {name, git_url, default_branch, languages, subprojects, schedule, notify, ...}, // ... // ] ``` Returns all projects visible to the bearer token. Use this before calling any of the `run_*` tools to discover what `project_name` values are valid. ### `crafting_table_register_project` ```jsonc // args: { "project_json": "{\"name\": \"alpha\", \"git_url\": \"http://...\", \"subprojects\": [...]}" } // returns: the registered project dict. ``` `project_json` is a **JSON-encoded string** of the Project shape (not an object). Encoding as a string keeps the MCP schema simple — the server-side shape is rich enough that the LLM is much more reliable producing it as a single JSON literal than as a deeply-nested schema-validated object. Project shape: ```jsonc { "name": "alpha", // slug, required "git_url": "http://git.example.com/org/alpha.git", // required "default_branch": "main", // optional, default "main" "languages": ["python", "rust"], // optional "subprojects": [ { "path": "clients/python", "language": "python", "build": "pip install -e .[test]", "test": "pytest tests/", "lint": "ruff check . && mypy --strict src/", "audit": "pip-audit", "timeout_secs": 1800 } ], "schedule": {"audit": "0 2 * * *"}, // optional cron strings "notify": { // optional "email": ["alerts@example.com"], "on": ["audit_fail", "cve_found"] } } ``` Returns 409 if a project with that name already exists for this token, 404 if the name is taken under a different token (existence-leak guard). ### `crafting_table_run_audit` / `run_build` / `run_test` ```jsonc // args: { "project_name": "alpha", // required, slug from list_projects "subproject": "clients/python", // optional, path inside repo "branch": "main" // optional, default = project.default_branch } // returns: {"job_id": "", "status": "queued"} ``` Kicks off the named recipe. Returns immediately with the queued job id — the actual pipeline runs in the runner pool. Poll `crafting_table_get_job` to watch state, or call `crafting_table_get_findings` once status is terminal (`succeeded`, `failed`, `timed_out`, `cancelled`). If `subproject` is omitted, the server picks the first subproject that defines a non-empty command for the requested recipe kind. ### `crafting_table_get_job` ```jsonc // args: { "job_id": "" } // returns: two content blocks: // [0] plain text — short summary (status, exit_code, last 30 log lines) // [1] JSON — {"job": {...}, "log_tail": [...]} ``` The two-block layout means the LLM consumer sees the prose summary directly without having to parse JSON, while tool-calling agents that want to introspect can still get the full structured response from the second block. ### `crafting_table_get_findings` ```jsonc // args: { "job_id": "" } // returns: two content blocks: // [0] plain text — short summary ("3 finding(s). by severity: warn=2, high=1. by kind: lint=2, cve=1.") // [1] JSON — {"findings": [{kind, severity, file?, line?, code?, message, suggested_fix?}, ...]} ``` Each finding has `{id, job_id, kind, severity, file?, line?, code?, message, suggested_fix?, fingerprint, created_at}`. Empty list means either the job had no issues OR there's no parser yet for the language — check `crafting_table_get_job` for `exit_code` to disambiguate. ### `crafting_table_draft_patch` ```jsonc // args: { "job_id": "", // required "finding_id": 42 // optional; if omitted, drafts patches for all open findings } // returns: {"ok": false, "pending": true, "message": "draft patch — not yet implemented (lands in wave 3 / step 9)"} ``` **Wave 2B stub.** The tool surface is callable today so MCP clients that cache tool catalogues don't have to re-handshake when wave 3 ships. No patch is drafted right now — the message says so explicitly. Once wave 3 lands, the server will: 1. Pull the finding(s) off the job 2. Send them to the configured agent host with the project context 3. Get back a unified diff 4. Apply in a worktree, re-run the failing recipe 5. If it passes, push to a `crafting-table/auto/` branch and open a Gitea PR ## Testing ```sh pip install -e 'mcp[test]' python -m pytest mcp/tests ``` The tests stub out the HTTP layer with `responses` — no live crafting-table 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 `crafting_table_mcp` logger. - **Errors are wrapped, not raised.** Auth failures, transport errors, upstream 502s — all get formatted into a single short `crafting-table 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 HTTP call doesn't stall heartbeats. - **404 hints.** When a tool surfaces a 404 (project or job not found), the wrapper appends a hint pointing the LLM at `crafting_table_list_projects` so it can self-recover. ## Why this exists crafting-table centralizes the polyglot build/audit toolchain on one LAN host so each agent doesn't need to ship its own swift / rust / dotnet / php toolchain. MCP is the integration layer — any MCP-aware client (Claude Desktop, Claude Code, Cursor, Zed, custom agents) can treat crafting-table as a native tool surface.