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.
292 lines
11 KiB
Markdown
292 lines
11 KiB
Markdown
# 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": "<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 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/<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 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.
|