Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-mcp/README.md

Snapshot at 34e3175

Containing directory

18,223 bytes ยท textView raw
1# Agent MCP library
2
3This package exports a stateless Streamable HTTP MCP handler and a single-Worker OAuth configuration. It has no Worker entrypoint, routes in core, wrangler configuration or deployment. FH-012 owns the real core adapter, Access validation, mounting and owner configuration. Tests use labeled in-process fixtures; they do not establish live acceptance.
4
5## Integration boundary
6
7`createMcpHandler({core, publicOrigin, path})` accepts direct enrolled core API bearer credentials. `createOAuthHandler({core, publicOrigin, path, project, authorizeOwner})` adds OAuth discovery, dynamic registration, consent, PKCE and token exchange, while still accepting direct agent credentials. Use the bounded OAuth handler for production composition; `createOAuthConfiguration` exposes its underlying `workers-oauth-provider` options for integration/testing, and is not independently a bounded HTTP facade.
8
9FH-012 reads the host/path from `deployment.local.jsonc` and supplies a canonical HTTPS `publicOrigin` plus `path` (default `/mcp`). They are separate from branding. Library display names come from the root `brand.json`. Requests must match the configured origin and endpoint. Browser origins default to that origin; explicit additional origins can be configured for authorized clients. Consent POSTs always require the authorization server's own origin. The `/authorize`, `/oauth/*` and discovery namespaces are reserved.
10
11The typed `CoreClient` requires three operations:
12
13- `authenticate(token, signal)` returns trusted `{project, agent}` for an enrolled agent, or null. It must reject owner tokens and never derive identity from tool arguments. **The real trusted core identity adapter does not exist in this packet.** FH-012 must wire a verified internal capability or approved core identity response; syntactic token checks are insufficient.
14- `execute(token, operation, signal)` executes the agent route variants below. `createCoreClient` implements this part over a supplied trusted binding `fetch`, preserving the bearer and enforcing bounded JSON responses. Its fixed `https://core.invalid` Request origin is an internal service-binding address, not a fetch destination on the public Internet. Do not wire it to global `fetch`.
15- `enrollAgent(owner, project, agent, signal)` uses owner authority held inside the adapter to call `POST /projects/:id/agents`. It returns `{project, agent, apiToken}` once. This capability is used only after verified owner consent, never exposed as a tool. Core must independently authorize the owner.
16
17All adapters must honor `signal` and check it before starting writes. Core remains authoritative: every commit independently checks that the caller, live claim, bound repository and registered repository ownership agree. A timeout does not roll back a write already accepted by core, so clients must inspect state before retrying.
18
19## Tool contract
20
21| Tool             | Arguments                                 | Core route                              | Read-only |
22| ---------------- | ----------------------------------------- | --------------------------------------- | --------- |
23| `get_context`    | `project`                                 | `GET /projects/:id/state` + `/me`       | yes       |
24| `status`         | `project`                                 | `GET /projects/:id/state` + `/me`       | yes       |
25| `activity`       | `project`, optional `after`, `limit`      | `GET /projects/:id/activity`            | yes       |
26| `refresh_token` | `repoName`, optional `ttlSeconds`         | `POST /projects/:id/forks/:repo/token` | no        |
27| `join`           | `project`, `agent`, optional `ttlSeconds` | `POST /projects/:id/join`               | no        |
28| `claim`          | `project`, `packet`, optional `repoName`  | `POST /projects/:id/packets/:pid/claim` | no        |
29| `report_usage`   | `project`, `packet`, `inputTokens`, `outputTokens`, optional `model` | `POST /projects/:id/usage` | no |
30| `request_review` | `project`, `packet`, `repoName`, `sha`    | `POST /projects/:id/reviews`            | no        |
31| `submit_work`    | `project`, `packet`, `files`, `message`   | `POST /projects/:id/forks/:repo/commit` | no        |
32| `list_judge_races` | `project` | `GET /projects/:id/state` (caller assignment filter) | yes |
33| `get_review` | `project`, `review` | `GET /projects/:id/reviews/:rid` | yes |
34| `get_review_diff` | `project`, `review`, `path` | `GET /projects/:id/reviews/:rid/diff?path=...` | yes |
35| `submit_verdict` | `project`, `packet`, `ranking`, `rationale` | `POST /projects/:id/races/:packet/verdict` | no |
36
37`get_context` and `status` preserve the state snapshot and add `myWork` from the bearer-authenticated `/me` `work`. Each item also has a separate text content line: `<packet> on <repoName> โ†’ next: <next> โ€” <hint>`. FH-041 owns the next-step rules; this library does not recompute them. Missing/malformed work is an explicit dependency failure, not an empty summary. Tests use labeled FH-041 contract fixtures.
38
39Packet records expose `mode: "standard" | "race"`; legacy records default to `"standard"`. Both read tools add `myRaces`, selecting `/state.races` entries where the authenticated caller has entered, including closed entries. Each caller race gets a standings text line showing all its entrants, their furthest recorded milestone, authoritative places, closure, winner, string `ledgerEntryId` (the promote's `ledger[].id`) and optional `promoteRejected` evidence. The project snapshot's `races` stays intact. Milestone order is Claimed โ†’ First push โ†’ Review requested โ†’ Checks passed โ†’ Approved โ†’ Promoted. Core supplies all timestamps and places; MCP never ranks entrants or changes FH-042 resume hints. Missing legacy `races` means no races; supplied malformed race records fail the tool instead of silently hiding standings.
40
41Both tools add `myUsage`, a caller-owned projection of state claims and race entrants: `{packet, repoName, usage: {reported, metered}, tokenBudget, budget}`. Usage sources are null or `{inputTokens, outputTokens, model?, source, at}`. Missing legacy usage stays null, never zero. Standard claims have `tokenBudget: null` and `budget: "unchecked"`. Race budget states (`unchecked`, `within`, `near`, `over`) come from core; this library does not recalculate them or infer safety when a budgeted entrant lacks a state. The full snapshot keeps historical `reviews[].usageAtReview`.
42
43When present, `myRaces` also preserves core's `tokenBudget` and each entrant's `usage` and `budget` alongside milestones and places. Legacy standings without these fields remain valid. The combined workerd fixtures verify both caller projections against source-derived core race/usage mapper output; this is offline wire evidence, not live core acceptance.
44
45`report_usage` forwards only `{packet, inputTokens, outputTokens, model?}` with the caller's bearer to core's usage route. Each count is an integer from 0 to 1,000,000,000. Agents report cumulative totals before every `request_review` and whenever asked; these are self-reported, not provider-metered. Core owns monotonicity, live claim/entrant binding, review snapshots and promotion gates. There is no retry or automatic zero report. Inspect status after conflicts or uncertain writes. This contract uses labeled FH-060 fixtures; **integrate and merge only after accepted FH-060**.
46
47`refresh_token` derives the project from the authenticated identity, forwards that caller's bearer, and requests `{scope:"write", ttlSeconds}` (default 3600, range 60โ€“3600). Core independently checks registered fork ownership and refuses canonical credentials. The tool returns only `{remote, token, expiresAt}`; it never logs credentials, retries issuance or stores tokens. Use the returned token only as an `http.extraHeader`, never print/log it or persist it in Git configuration. Resume uses the existing claim; never join again. Both successes and failures retain `Cache-Control: no-store`.
48
49The join/claim workflow explicitly passes the returned `repoName` to `claim`. If omitted, core defaults it to the most recently successfully registered fork. Claim creation atomically returns the immutable `{packet, agent, repoName}` binding. `state.claims` must contain live bindings in that same shape. `submit_work` filters them by the authenticated caller and packet, refuses missing/ambiguous bindings, and uses exactly the bound repository. Later joins never redirect that binding. Releasing and reclaiming belongs to core's claim lifecycle; no release tool or route is invented here.
50
51For race packets, `claim` uses that same caller-authenticated route and fork argument. FH-052 enforces one entrant per agent on its own fork, a maximum of **three distinct agents ever**, no re-entry after closure, and cross-packet path conflicts. Entrants in the same race may overlap. FH-053 refuses entry after finish. MCP forwards sanitized core conflicts once, without retrying or creating another fork. `submit_work` resolves the caller's entrant from `/state.races`, requires an open race and `closedAt === null`, and commits to that entrant's immutable `repoName`. It never falls back to `state.claims` for a race packet. Missing, foreign, closed, duplicate or finished entrant bindings refuse before the commit call. Core still independently authorizes every write.
52
53`files` contains `{path, content}` objects, up to 200 unique safe relative paths. The commit body is bounded to 1 MiB; the enclosing MCP request must fit strictly below 1 MiB, so practical file payloads are smaller. All outbound core Requests use manual redirect handling: every upstream 3xx is refused with a sanitized error without following or exposing its Location.
54
55The six read tools advertise `readOnlyHint: true`; writes are marked non-idempotent. Enrollment, rotation, packet creation, approval and promotion are absent from the tool inventory. There are thirteen tools, below the 200-tool cap.
56
57## Judge advice (FH-063b)
58
59This seam is source-integrated with accepted FH-063. Post-merge checks remain pending; the saved judge capture is labeled fixture evidence. `list_judge_races` returns `{races}` filtered by the bearer-authenticated agent's immutable judge assignment, including finished races as history. Judge/advice fields and public entrant evidence such as reported usage are preserved. Existing `myRaces` remains entrant-scoped; a judge uses the dedicated list tool and project context to locate review IDs.
60
61`get_review` and `get_review_diff` forward the caller's bearer unchanged. Core authorizes an agent's own review, or the assigned judge's access to entrants' reviews for that race; every other cross-agent read stays refused. MCP does not grant this exception itself, expose the owner-only full receipt endpoint or retry denied reads. Detail supplies receipt summaries, changed-file counts, commit information, timing and reported usage; diffs retain binary and truncation flags. Review IDs are bounded safe path components, and diff paths use the existing safe relative file-path schema and URL query encoding.
62
63`submit_verdict` forwards only `{ranking, rationale}`. Ranking names are unique valid agent names, at most three; core validates the exact permutation of current entrants, including whether an empty ranking is appropriate. Rationale is 1โ€“2000 characters, preserved as plain text. Core enforces judge-only submission, replacement until finish, and a 409 after finish; the owner and other agents cannot submit. Owner credentials are rejected at the MCP authentication boundary. Success response shape is forwarded from core; the fixed packet specifies status 200 without prescribing its body. Tests label their `{verdict}` response as a fixture.
64
65The [judge skill](../../skills/forkhorse/SKILL.md#judge-an-assigned-race) weighs receipts, diff size/content, recorded time and **reported** tokens, explains the ranking and names evidence gaps. Advice never approves or promotes. A judge needs no entrant fork and cannot enter its assigned race. After an uncertain write, inspect the latest advice before retrying. No history or revision counter is invented.
66
67The confirmed FH-063 roster is the full retained `/state.races[].entrants` list, including withdrawn and closed entrants. Exact roster membership remains core-authoritative; the merged core source confirms that roster and authorization seam. Judging does not change placings.
68
69Each call has a 25-second deadline, including authentication, input and output body reads. Serialized HTTP responses stay strictly below 1 MiB, including JSON-RPC wrapping and escaping. Bodies are counted while streaming, regardless of Content-Length. No persistent GET/SSE stream is offered: POST uses the SDK's JSON response mode and GET/DELETE return 405. Each request gets a fresh server/transport, so no in-memory MCP session holds a credential. Responses are `no-store`; provider errors are replaced with fixed public messages.
70
71## OAuth and owner consent
72
73The real `@cloudflare/workers-oauth-provider` library handles RFC 9728 resource discovery, authorization-server discovery, DCR, token exchange and encrypted grant props. The composed core Worker supplies `OAUTH_KV`; the provider supplies `OAUTH_PROVIDER` to its authorization handler.
74
75- Discovery: `/.well-known/oauth-protected-resource/mcp` (path-qualified for the configured MCP path; the bare root form returns 404), plus `/.well-known/oauth-authorization-server`.
76- Registration/token endpoints: `/oauth/register`, `/oauth/token`.
77- Consent: `/authorize`, protected by the required `authorizeOwner(request, signal)` callback. FH-012 must validate Access JWT signature, issuer, audience, expiry and the owner allowlist. Merely checking an Access header is not sufficient. The fixture test callback is explicitly not a deployment implementation.
78- Public client authorization requires S256 PKCE and the coarse `mcp:agent` scope. That scope permits only the agent tools, subject to core authorization.
79- Consent requires an exact registered `redirect_uri`, including loopback ports. The browser response remains a 302 to that callback with only the OAuth result parameters added. The final Location is checked against the registered target and result fields; the wrapper rejects every other 3xx, and never fetches a returned Location.
80- The page shows escaped client metadata, redirect host, project and permissions. Provider helpers supply anti-framing headers and a browser-bound consent handle that is refused on reuse; the pinned provider's KV consume is not atomic, so two simultaneous submits from the same signed-in browser could each enroll an agent (known limit). Denial creates no agent.
81- Each approved grant enrolls a fresh agent. Its core credential is stored only in encrypted provider props, never sent to the OAuth client. MCP revalidates it on every call, so core rotation immediately removes effective access.
82- Access tokens last one hour; refresh grants and dynamic registrations expire after one day. Client metadata URL fetching and token-exchange grants are disabled.
83
84Enrollment and OAuth grant persistence span two systems. If grant completion fails after enrollment, the owner may need to remove/rotate the unused enrolled credential. This library never retries enrollment automatically. FH-012 must establish live KV, Access, core and deployment acceptance, plus appropriate public DCR rate limits. Before any deployment, the owner must verify that the deploy token includes the configured MCP zone.
85
86## Local checks
87
88FH-063b adds actual-workerd tests over labeled fixed-contract fixtures for caller-scoped judge races, detail/diff routes and authorization refusals, advice replacement/finish refusal, rationale/ranking bounds, bearer preservation, no retries and honest tool annotations. Its fake core policy is not acceptance of FH-063's authorization implementation. A Node test can emit a credential-free, explicitly offline MCP capture when `FH063_CAPTURE_PATH` names a local output file; the capture is test evidence, not a UI screenshot or live acceptance.
89
90FH-054 adds offline workerd fixtures of the fixed FH-052/053 wire contracts, covering packet modes, three caller-bound entrant forks, standings and finish/photo-finish data, authoritative race resume hints, sanitized claim refusals and closed/ambiguous binding rejection. A source-derived fixture additionally preserves the integrated core mapper output, including `places` and explicit null `promoteRejected`. The fixture generator executes only the pure core race/resume mappers with in-memory storage in Node; MCP consumes that output in workerd. These fixtures do not validate core Worker execution, storage transactions, ranking or live race execution. Full-root checks, fresh-clone acceptance and D5 require coordinator reservation and are separate from focused package checks.
91
92After a reviewed core contract change, regenerate the source-derived fixture with `node packages/forkhorse-mcp/__tests__/generate-race-wire-fixture.mjs`. Use `--check` to verify the saved fixture against the current core sources without changing it.
93
94```sh
95pnpm --filter forkhorse-mcp test:run
96pnpm --filter forkhorse-mcp types:check
97pnpm test
98pnpm lint
99```
100
101The official MCP SDK client initializes and lists tools against this library. Tests also cover exact route/body/token mappings, immutable binding across later joins, input/authentication rejection, size/deadline limits and redacted conflicts. OAuth tests run the real provider against fixture KV and a minimal Node-only `WorkerEntrypoint` shim, checking discovery, DCR, consent, PKCE exchange, replay refusal and core-token revocation. A separate `workerd` Vitest project uses the real Worker runtime and local KV without that shim. It covers bearer-bound refresh success/refusal, exact TTL/body mapping, response field projection, no token logging, FH-041 resume fixtures, authenticated MCP tools/list, OAuth discovery and registration, manual refusal of upstream 3xx responses (including a core 302), valid approve/deny consent responses, mismatched callback URIs and unexpected provider redirects. Outbound fetch is blocked in those fixtures. No test calls live Cloudflare services.
102