Skip to main content
Glama
EsraaKamel11

forge_mcp

by EsraaKamel11
README.md
# forgekit

One typed Python SDK over the three internal services that run a manufacturing floor, and an
MCP server on top of it, so an engineer or an agent can say "start this run on cnc-01 and tell
me whether the part came out good" and get an answer that is true, or an honest "unknown, ask a
person" when the two systems that hold the answer disagree.

## The platform, and the problem

The platform team reverse-engineered a floor of instruments, CNC mills, a robot arm and SLS
printers, into API-controllable machines. Their engineers, and increasingly their agents, run
production jobs through three internal services, each written by a different team at a different
time:

| service | what it holds | authentication | pagination | error shape |
|---|---|---|---|---|
| orchestration | work orders, the lifecycle state machine, dependencies | bearer token | `limit` / `offset` | `{"error": {"code", "message"}}` |
| machine control | the machines, runs (long-running), abort, purge, key attenuation | API key header, scoped | `page` / `page_size` | `{"detail": "..."}` |
| production data | results, QC records, telemetry | token in the query string | cursor | plain text |

Consuming them is miserable. A new hire spends a week learning which service wants which header,
which page shape and which error body, and agents are worse at it than people. The published
OpenAPI document is behind the code: it names a field `state` that is live as `status`, documents
run dispatch as synchronous when the live endpoint answers `202` with an id to poll, describes 4
of the 17 live endpoints, and omits the whole production-data service.

Two things about the floor make this harder than plumbing. A machine run takes fifteen to forty
minutes. And some machine operations are destructive: abort a running job, purge a queue. An
agent holding the wrong credential can stop a live cut.

## What was tried before, and why it failed

An in-house wrapper existed before this work. It read the OpenAPI document and generated one MCP
tool per endpoint, about forty flat tools, all authenticating with the master admin key baked
into the server configuration, with "start a run" implemented as a tool that blocks and polls
until the run finishes.

It failed on the floor in three ways, each observed rather than predicted. The agent picked the
wrong tool constantly, because forty near-identical verbs with no shape between reads, writes and
destructive actions is not a surface anyone can choose from. The blocking start-run tool sat there
until the client killed it on a real mill run, and the agent then reported the run as failed while
the spindle was still cutting. And a teammate's agent, asked to "clean up cnc-02", went looking for
a purge tool and found one, on the admin key.

Two instincts were on the table for fixing it, and both are wrong in ways worth stating, because
they are what a competent engineer reaches for first. Regenerating the tools cleanly from the
OpenAPI document produces a surface that is still too large to choose from and still wrong,
because the document is wrong, and it re-rots at the next drift. Using MCP roots to keep the agent
away from destructive operations fences nothing, because roots are advisory to a client, not an
authorization decision a server can rely on.

## The platform team's concerns, and what this does about each

Their four concerns, in their order, and the mechanism that answers each one. Every mechanism is
one a test attacks rather than a paragraph that asks to be believed; the test named beside each
is the one to read first.

| the concern | what this does | where |
|---|---|---|
| **Long-running work.** A run is fifteen to forty minutes; a tool that waits for it times out, and a dropped connection loses the only handle to a run that is still cutting | Call-now, fetch-later. `start_run` returns a run handle in well under a second and the call ends; the server owns the run in a persisted task row, a background poller advances it, and the answer is one cheap read later. A restart re-adopts tracking from the rows on disk, and a row that has no run id yet is surfaced for a person and re-dispatched by nothing (`test_orphans_are_reported_and_never_retried`) | `src/forge_mcp/store.py`, `src/forge_mcp/poller.py`; `test_start_run_is_asynchronous_not_synchronous` in `tests/contract` |
| **Safety.** Everything ran under the admin key; an agent could abort a live job or purge a queue | The credential, not the prompt. At boot the server spends the master key once to mint an attenuated child key without `run:abort` and `machine:purge`, and the agent surface holds only that key, so abort and purge answer `403` from the platform whatever any tool description says (`test_child_key_cannot_abort_a_real_running_job` starts a genuine run and fails to stop it). The destructive operations are reserved for a separately keyed admin surface that is designed and not built; until it exists nothing on this server can reach them at all, and `start_run` itself is annotated destructive so a client asks a human before committing a machine | `src/forge_sdk/keys.py`, `src/forge_mcp/server.py`; `test_child_key_is_403_on_abort_and_purge` |
| **Not one tool per endpoint.** Forty flat tools, the agent picks wrong | A curated, mechanically split surface: a state-changing endpoint is a tool, a read-only endpoint is a resource, a fixed workflow is a prompt. Applied to all 17 live endpoints the map comes to four agent tools, five admin tools, a twelve-entry resource tree and five prompts, and nothing falls off it: reads change shelf, so they stop competing with actions for the agent's choice. Built in this phase: `start_run`, `get_run_status` and the two run resources, which is the whole call-now, fetch-later path; the other tools, the admin surface and the prompts are designed with their shelf fixed, and listed as such in the map's section 10 | `docs/map/phase0-surface-map.md` sections 1 to 4 and 10; `src/forge_mcp/server.py` |
| **The mess itself.** Three auth schemes, three paginations, three error shapes | One SDK. One HTTP seam above which nothing knows which service it is talking to; three auth strategies behind one interface, each redacting itself in `repr`; three pagination envelopes behind one iteration protocol; three error bodies reduced to one exception hierarchy. The MCP server is thin on top of it, so the shop's own scripts and CI jobs get the same guards as the agents | `src/forge_sdk/transport.py`, `auth.py`, `pagination.py`, `decoders.py`, `errors.py`; `test_caller_params_cannot_shadow_the_credential` |

Two more things the brief did not name as concerns and the work made unavoidable:

- **The OpenAPI document is not the platform.** Every statement this repository makes about the
  three services came from a live probe of staging, and each is pinned by a contract test that
  runs against staging with no mocks: 59 collected, in two marker groups, so a red is legible.
  `contract` means something this depends on broke; `pinned_defect` means a known platform defect
  changed and someone should look. A fix and a regression look identical to an assertion, which is
  why they are separated (`tests/contract`).
- **"The run finished" and "the part is good" are two facts in two systems.** Orchestration says
  `done`; the QC bench says `wall_mm` came in under spec. A wrapper that reports lifecycle status
  says the part is good, and ships it. `RunVerdict` joins the two reads and derives its conclusion
  rather than accepting one, refuses to judge one run by another run's QC record
  (`test_a_result_from_another_machine_is_refused`), and answers `unknown` out loud when the join
  is not safe (`src/forge_sdk/verdict.py`, `src/forge_sdk/models.py`).

## The shape of it

```
   agent · engineer · CI job
         │  MCP over Streamable HTTP, loopback bind, Host and Origin checked
         ▼
┌────────────────────────────────────────────────────────────────────┐
│ forge_mcp                                                          │
│   tools       start_run           get_run_status                   │
│   resources   forge://runs/       forge://runs/{task_id}/status    │
│                                                                    │
│  ┌─────────────────┐   ┌───────────────────┐   ┌─────────────────┐ │
│  │ SqliteTaskStore │◀──│ RunTracker        │   │ ReadOnlyTaskDao │ │
│  │ one writer per  │   │ poll the run, join│   │ what resources  │ │
│  │ file, refused   │   │ the QC record,    │   │ read through;   │ │
│  │ at startup      │   │ write the verdict │   │ no write method │ │
│  │                 │   │ re-dispatch       │   │                 │ │
│  │                 │   │ nothing, ever     │   │                 │ │
│  └─────────────────┘   └─────────┬─────────┘   └─────────────────┘ │
└──────────────────────────────────┼─────────────────────────────────┘
                                   │  the child key: minted at boot from the master key, without
                                   │  run:abort or machine:purge; boot refuses if the mint came
                                   ▼  back with either scope
┌────────────────────────────────────────────────────────────────────┐
│ forge_sdk                                                          │
│   machines (four guards)  data (the QC read)  keys (mint, assert)  │
│               └──────────────────┬───────────────────┘             │
│                      ServiceClient: one HTTP seam                  │
│                      auth ×3   pagination ×3   error decoder ×3    │
└──────────────────────────────────┬─────────────────────────────────┘
                                   ▼
┌────────────────────────────────────────────────────────────────────┐
│ the platform: one host, three services                             │
│   /orchestration/v1        /machines/v2            /data           │
│   Bearer                   X-API-Key, scoped       ?api_token=     │
│   limit / offset           page / page_size        cursor          │
│   {"error": {...}}         {"detail": "..."}       plain text      │
└────────────────────────────────────────────────────────────────────┘
          ▲          59 contract tests, live against staging, no mocks          ▲
```

Not drawn: the admin surface, designed and not built, which will carry `abort_run`,
`purge_machine`, `cancel_work_order`, `approve_rerun`, `reconcile_orphan` and the audit resource
on a key of its own. Until it exists nothing on this server reaches a destructive operation, and
the reason is the credential in the middle of the picture rather than the absence of a tool.

## The second phase, and where it stands

The platform team's internal assistant answers questions about jobs and machines and can only
talk; the point of the surface above is that it can act through it: start a run, stage a re-run
with the reason attached for a person to dispatch, say what a stuck job is waiting on. The brief
for that phase is explicit about what would not convince them that it works: one clean question
on a healthy platform. What they worry about, in their order: an operator who asks badly at a
shift change; the floor changing under an answer, a machine dropping offline, a slow data service,
a stale read that says a machine is available when it is in maintenance; the assistant saying a
job is fine when it is not; and the thing quietly getting worse after someone edits a prompt.

`forge_assistant` is that thin assistant, acting through the MCP server and kept from destructive
operations by the same credential the agent surface holds. `forge_eval` is the evaluation
substrate that grades what it does: cases, a corpus, a judge with an agreement gate against
hand-labelled expectations, outcomes scored per skill and per failure mode. Both are exercised by
the unit tier with no key and no network. Both are wired to a live model through the `assistant` extra (`claude-agent-sdk==0.2.140`, the release that co-installs with this project's `mcp` pin; `pyproject.toml` records the history beside it), and the wiring is exercised by the model tier under `tests/model`: twelve tests, gated on `FORGE_LIVE_MODEL=1`, a key and the platform running, that boot this server as a subprocess, run real turns through a real client and read what happened off the server's own log, the task database and the transcript. The judge sits in a different model family from the assistant, on purpose. What ships today is the client library and those tests; an operator-facing entry point for the assistant is not built yet. The position that fixes the assistant's boundary is
`docs/design/2026-08-15-agent-sdk-boundary-position.md`.

## What is here

| | count | what it counts |
|---|---|---|
| source | 38 modules, 7,357 lines | every `.py` under `src/`: `forge_sdk` 12, `forge_mcp` 8, `forge_eval` 13, `forge_assistant` 5 |
| unit tests | 648 passing, 1 skipped | recorded transports and one real loopback socket; no staging, no credential |
| contract tests | 59 collected | live staging, no mocks, two marker groups |
| integration tests | 6 collected | live staging, except one that monkeypatches the mint |

A fifth tier under `tests/model`, twelve tests, drives the assistant against a live model and is not
part of these counts; it needs a key, the platform and the `assistant` extra.

The one skip is the POSIX file-lock branch, skipped on Windows with the platform gap named in the
skip reason (`test_on_posix_the_lock_is_advisory_and_does_not_block_io`); the Linux CI job is
where it runs.

## Run it

The unit suite, with no staging and no credentials:

```
python -m pytest tests/unit -q
```

The contract suite, against staging:

```
set FORGE_BASE_URL=http://your-staging
python -m pytest tests/contract -q
```

One contract test is gated behind `FORGE_STAGING_EXCLUSIVE=1` because it mutates global platform
state; the default tier creates only prefix-tagged records and cancels them at teardown, because
staging is shared with the people who work there.

The documentation-honesty checks, which every push runs in CI in `--strict` mode:

```
python tools/guarantee_lint.py
python tools/predicate_lint.py
```

A sentence in the scanned documents that promises a property of the code is compliant when it
names a test that resolves, or carries a reviewed note giving the reason no test applies. What the
tool checks is that the name resolves; whether the cited test attacks the sentence it sits under
is a review question, and six citations resolved and covered nothing before a review caught them.
Both tools report how many files they examined and refuse to pass on zero.

## Connect a client

```
set FORGE_MACHINE_KEY=...
set FORGE_DATA_TOKEN=...
forge-mcp
```

Then point a client at `http://127.0.0.1:8831/mcp`. `.mcp.json` at the root of this repository
does that for anyone who opens it in Claude Code, and `claude mcp add --transport http
forge-robotics http://127.0.0.1:8831/mcp` does it for one person. `docs/connecting.md` is the full
version: the seven environment variables, what boot prints, and how to confirm the client
connected rather than assuming it did. The two credentials have no defaults and the server refuses
to start without them, naming the missing variable; a deployment that forgot one used to come up
green against the wrong estate.

Nothing authenticates a client to this server in this phase. Every credential above is spent on
the hop from this server to the platform; on the hop from a client to this server there is no
token, and any process on the host can call `start_run`. The bind stays on loopback and the
endpoint validates `Host` and `Origin` against an allowlist derived from it. The fence for that hop
is the OAuth tier, designed against the platform's identity service and not built; section 8.9 of
the architecture note states it in those words.

## Where to read

- `docs/background.md`: the floor, the brief in the platform team's own terms, what was tried
  and failed, and the concern-to-mechanism map with what a reader checks for each.
- `docs/map/phase0-surface-map.md`: the client-facing surface map, what is built and what is
  designed, with the table of sentences that once claimed more than the code delivered and how
  each was found.
- `docs/connecting.md` and `docs/handover.md`: starting the server, pointing a client at it, and
  the transcript of that being done end to end.
- `docs/architecture/forge-sdk-architecture.md`: the long-form architecture note, the rejected
  alternatives, the failure taxonomy, the threat model.
- `docs/design/2026-07-26-forge-sdk-mcp-design.md`: the design of record, with the
  dated amendment that removed MCP tasks from the surface.