# MCP server and connectors directory

> The agent may declare its intent. It may read the verdict. It must never control the independent facts, the
> governing contract, or the final evidence.

Two pieces, built on that line:

- **The evidence server**, inside the API at `${API_URL}/mcp`: an assistant (Claude, ChatGPT, Cursor, an agent
  framework) reads actions, verdicts, reasons and evidence pointers, contracts, coverage and usage; validates,
  dry-runs and drafts contracts without saving; and, with the `actions:record` scope, declares an action before it
  happens.
- **The recorder**, `recorder/python` and `recorder/js`: a stdio proxy in front of any MCP server an agent uses,
  which records a mapped tool call as an action before forwarding it. See the READMEs there.

Nothing on either surface writes a fact, saves or activates a contract, overrides a verdict, deletes anything, or
reads a credential. No scope grants any of these.

## Transport and auth

Streamable HTTP, stateless, JSON responses (`json_response=true`), mounted by `app/api/main.py` at the root so the
OAuth metadata documents sit at the issuer. Bearer tokens of three kinds are accepted (`app/mcp/auth.py`):

| Token | Who | Scopes |
|---|---|---|
| `spk_…` API key | A service, an agent runtime, a local MCP client | The key's scopes (Settings → API keys; keys from before scopes carry all) |
| `cla_…` OAuth access token | A remote client a member approved on the consent page | The grant, capped to the member's current role on every call |
| Session JWT | The web app itself | The member's role |

Unauthenticated requests get `401` with `WWW-Authenticate: Bearer resource_metadata=…/.well-known/oauth-protected-resource/mcp`,
which is how an OAuth-capable client discovers the authorization server.

## Scopes (`app/mcp/scopes.py`)

`actions:read`, `actions:record`, `facts:write`, `contracts:read`, `contracts:validate`, `contracts:draft`,
`reconcile:read`, `connectors:read`, `usage:read`. Presets: `recorder` (`actions:record`, `actions:read`),
`evidence` (`facts:write`, `actions:read`, `connectors:read`, bound to named sources), `reader` (every read scope
plus validate and draft), `full` (all). Owners, admins and analysts may hold every scope; viewers only the read
scopes. A model-facing principal (an OAuth token, any bearer on this server) never holds `facts:write`: the
model may declare its intent and read the verdict; the evidence arrives by another credential. Over REST an API
key acts as an analyst when it may write and as a viewer otherwise, and each write endpoint checks its scope.

## Tools

| Tool | Scope | What it returns |
|---|---|---|
| `get_action(action_ref)` | actions:read | The action with both lenses, status, reason codes, checks, evidence pointers |
| `list_actions(contract_id?, workflow_id?, verdict?, period?, limit)` | actions:read | Actions with their latest verdict, newest first |
| `pending_actions(limit)` | actions:read | Provisional actions, what they wait on, when they settle |
| `workflow_summary(period?)` | actions:read | Per-contract counts for a month |
| `record_action(contract_id, subject_id, …)` | actions:record | The provisional verification and the allowance; `channel: mcp` |
| `verify_hash(hash)` | actions:read | Whether a statement / verdict / dispute hash exists and its chain holds |
| `connector_health()` | connectors:read | Connections with status, trust, provides, last sync, watermark; never credentials |
| `usage()` | usage:read | Plan, limits, features, usage, allowance |
| `list_contracts()`, `get_contract(contract_id, version?)` | contracts:read | Summaries; YAML, compiled rules, plain language, versions |
| `validate_contract(yaml)` | contracts:validate | Errors, compiled rules, plain language, weakening warnings |
| `dry_run_verify(contract_id, period, yaml?, version?)` | contracts:validate | Counts per lens, verified amount, reasons; nothing persisted |
| `draft_contract(action_type? \| contract_id?, …)` | contracts:draft | Proposed YAML with statistics and warnings; nothing saved |
| `list_reconciliations(vendor?, period?, limit)`, `get_reconciliation(id)` | reconcile:read | Reconciliation views (coverage, claims, run, statement totals) |

Resources: `commitlayer://reason-codes` (every code with label and explanation), `commitlayer://actions/{ref}`.
Prompts: `investigate_failed_action(action_ref)`, `review_contract_change(contract_id, yaml)`.

Every tool carries a title and the standard annotations (0.9.12). Fourteen are `readOnlyHint: true`, so a client
such as Claude runs them without asking the person before each call; `record_action` is `readOnlyHint: false`,
`destructiveHint: false`, `idempotentHint: false` (recording without an `action_id` twice is two actions).
`openWorldHint` is false throughout. The hint must say what the scope enforces: `tests/test_mcp.py` fails when a
tool's `readOnlyHint` disagrees with its `_call` scope, and the exported `product-catalog/mcp-tools.yaml` derives
`read_only` and `destructive` from the annotations.

Errors are returned as content, never as a stack trace: `{"error": {"code", "message", …}}` with codes
`insufficient_scope` (plus `required_scope`), `plan_required` (plus `upgrade_to`), `not_found`, `invalid_request`,
`invalid_contract` (plus `errors`), or an engine code. Every call is audited as `mcp.tool_called` with
`object_id` = tool name, the actor (`apikey:<prefix>`, `<email> via <client_id>`, or the email) and
`metadata {…args, ok, code}`.

## OAuth 2.1 (`app/mcp/auth.py`, `app/api/routers/oauth.py`)

The API is the authorization server. Endpoints (served by the MCP SDK): `/.well-known/oauth-authorization-server`,
`/.well-known/oauth-protected-resource/mcp`, `POST /register` (dynamic client registration, open), `GET /authorize`
(PKCE S256 required), `POST /token` (`authorization_code`, `refresh_token`), `POST /revoke`.

Flow: the client registers → `/authorize` redirects the browser to `${WEB_URL}/oauth/consent?req=<signed request,
10 min>` → the signed-in member sees the client, the workspace and the requested scopes (`GET /v1/oauth/consent`),
unticks what they do not want, approves or denies (`POST /v1/oauth/consent {req, approve, scopes?}`) → the answer
carries `redirect_to` (the client's redirect URI with `code` and `state`, or `error=access_denied`) → the client
exchanges the code with its verifier at `/token`.

Codes are single use and live 10 minutes; access tokens 1 hour; refresh tokens 30 days. A refresh rotates the pair
and the old pair dies. Tokens are stored hashed (`oauth_tokens`), bound to the member and the workspace they chose;
a member who leaves the workspace loses every token, and a token never outranks the member's current role. Client
secrets are stored envelope-encrypted because the token endpoint compares them in full. Audit:
`oauth.approved {scopes, client_name}`, `oauth.denied`.

## The Claude connectors directory

The server meets the directory's published requirements (checked against the live API on 2026-11-02): remote
Streamable HTTP over HTTPS, a `401` with `resource_metadata` on unauthenticated calls, protected-resource metadata
naming `/mcp` as the resource, authorization-server metadata with PKCE S256 and open dynamic client registration
(a client that registers as a public client gets no secret and is accepted), authorization-code and refresh
grants, per-tool errors as content, titles and annotations on every tool. Submission is at
`claude.ai/directory/manage` (Submit new → MCP connector) from an account on a paid Claude plan. What the form
asks for, and the answers:

- **Connection**: `https://api.commitlayer.ai/mcp`, a single URL.
- **Listing**. Name: *CommitLayer*. One-liner (≤200): *Read what your AI agents actually did: PASS, FAIL or
  UNKNOWN verdicts with the evidence, from the systems the action touched.* Description (≤2000): the first
  paragraph of `docs/mcp.md` in plain words: CommitLayer verifies agent actions against independent systems of
  record (Stripe, Shopify, CRMs, your own events) and returns a verdict with the step that did not land; through
  this connector Claude reads recorded actions and their verdicts, reasons and evidence pointers, lists what is
  still pending and what each is waiting on, reads contracts, validates and dry-runs contract changes without
  saving, drafts a contract from recorded actions, checks connector health and the workspace's plan and usage,
  and, when the token carries `actions:record`, records an action before it is performed. It never writes a fact,
  changes a verdict, activates a contract or deletes anything. Categories: Developer tools, Productivity.
  Documentation URL: `https://commitlayer.ai/mcp`. Privacy policy URL: `https://commitlayer.ai/legal/privacy`.
  Support: `support@commitlayer.ai`. Icon: `stateprf-ui/public/logo/icon-512.png`. Slug: `commitlayer`
  (permanent once published).
- **Use cases**, three prompts that work on the demo workspace: *"Which of my agent's refunds failed this month,
  and why?"* (list_actions with verdict FAIL, then get_action); *"What is still waiting on a system of record,
  and when does it settle?"* (pending_actions); *"Draft an action contract from the refunds recorded in the last
  30 days and tell me what limits it would set."* (draft_contract). Prerequisites: a CommitLayer workspace (free
  tier). The connector reads; it writes only through `record_action`, on a token that carries that scope.
- **Authentication**: required; OAuth with dynamic client registration (`oauth_dcr`); the person signs in to
  CommitLayer and approves the client for one workspace with the scopes they choose.
- **Data handling**: the API is our own; no health data, no sponsored content.
- **Test & launch**: a reviewer account with sample data. Create a member of the synthetic demo workspace (181
  verified refund-workflow actions, connectors, contracts) with a password sign-in, enter its email and password
  in the portal's test-account card, and write: *Add the URL as a custom connector, sign in with the credentials
  above when CommitLayer's consent page opens, approve the default scopes, then run `list_actions` with
  `verdict: FAIL`, `get_action` on one of the results, and `pending_actions`.* Confirm every tool was run
  through MCP Inspector or as a custom connector first.
- **Compliance**: the seven acknowledgments; the server collects nothing from the conversation, stores no
  conversation content, and calls no API it does not own.

A Community listing follows the automatic policy scan; a Verified one, where reviewers run every tool, is
Anthropic's call later. Escalations: `mcp-review@anthropic.com`.

## Testing

`tests/test_mcp.py` starts the API under uvicorn and drives it with the MCP Python client: tool catalog, every
read tool, resources, audit rows, a read-only key refused `record_action` over MCP and `POST /v1/actions` over REST,
a recorder key recording, a revoked key refused, metadata documents, and the full OAuth flow (registration,
authorize, consent describe / deny / approve with trimmed scopes, wrong verifier, single-use code, refresh rotation,
revocation).
