Skip to main content
Glama
EsraaKamel11

forge_mcp

by EsraaKamel11

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.

Related MCP server: CrewAI Enterprise MCP Server

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.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityActive
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/EsraaKamel11/forgekit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server