crafting-table/mcp/README.md
Kayos ecb9d76e6d v0.1 wave 2B (step 7): MCP server — stdio JSON-RPC, 8 tools
- mcp/ subpackage: crafting-table-mcp (separate pip install)
- Self-contained requests-based HTTP client (mirrors clawdforge_mcp pattern)
- 8 tools: list_projects / register_project / run_audit / run_build / run_test / get_job / get_findings / draft_patch (stub)
- draft_patch is stubbed — full impl lands in wave 3 / step 9
- tests/: client + tool coverage, 401/404 surfacing
- Tools designed for LLM consumption; descriptions tuned for "when to use" guidance

Spec: memory/spec-crafting-table.md
2026-04-29 08:38:29 -07:00

294 lines
11 KiB
Markdown

# crafting-table-mcp
Model Context Protocol (MCP) server that bridges to the
[crafting-table](http://192.168.0.5:3001/Sulkta-Coop/crafting-table) LAN HTTP
service (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://192.168.0.5: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://192.168.0.5: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://192.168.0.5: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://192.168.0.5: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://192.168.0.5:3001/.../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": ["cobb@sulkta.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": "<uuid>", "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": "<uuid>"
}
// 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": "<uuid>"
}
// 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": "<uuid>", // 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 clawdforge 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/<finding-id>` 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 every Sulkta repo doesn't need each agent to ship its own swift /
rust / dotnet / php toolchain. MCP is the natural integration layer — any
MCP-aware client (Claude Desktop, Claude Code, Cursor, Zed, custom agents)
can now treat crafting-table as a native tool surface. Spec lives at
`memory/spec-crafting-table.md` in the openclaw-workspace repo (LAN-only).