URLs, mount paths, and LAN host bindings parameterized via env or relative paths
so the repo stands up from a clean clone anywhere. Drop cross-codebase refs
("mirrors clawdforge's pattern"), Sulkta-Coop client/merchant test fixtures,
and audit-changelog scaffolding from comments. README terser, technical content
preserved.
11 KiB
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:
pip install -e mcp
# or with test deps
pip install -e 'mcp[test]'
Once published:
pip install crafting-table-mcp
This installs:
- the
crafting_table_mcpPython package - a
crafting-table-mcpconsole script (alias forpython -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
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):
{
"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:
{
"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:
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
// 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
// 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:
{
"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
// 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
// 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
// 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
// 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:
- Pull the finding(s) off the job
- Send them to the configured agent host with the project context
- Get back a unified diff
- Apply in a worktree, re-run the failing recipe
- If it passes, push to a
crafting-table/auto/<finding-id>branch and open a Gitea PR
Testing
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 thecrafting_table_mcplogger. - Errors are wrapped, not raised. Auth failures, transport errors,
upstream 502s — all get formatted into a single short
crafting-table 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 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_projectsso 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.