crafting-table/mcp
Cobb Hayes b335405c02 Public-flip audit: generalize internal hosts/paths + drop Sulkta-internal refs
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.
2026-05-27 11:25:47 -07:00
..
examples Public-flip audit: generalize internal hosts/paths + drop Sulkta-internal refs 2026-05-27 11:25:47 -07:00
src/crafting_table_mcp Public-flip audit: generalize internal hosts/paths + drop Sulkta-internal refs 2026-05-27 11:25:47 -07:00
tests Public-flip audit: generalize internal hosts/paths + drop Sulkta-internal refs 2026-05-27 11:25:47 -07:00
pyproject.toml Public-flip audit: generalize internal hosts/paths + drop Sulkta-internal refs 2026-05-27 11:25:47 -07:00
README.md Public-flip audit: generalize internal hosts/paths + drop Sulkta-internal refs 2026-05-27 11:25:47 -07:00

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_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

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:

  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

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.