# Remote MCP connection

Field exposes an opt-in, OAuth-protected Streamable HTTP MCP resource at `https://FIELD_ORIGIN/mcp`. This is how AI agents and other systems drive Field. Do not connect agents with `field` CLI, `using-field`, `FIELD_TOKEN`, or the HTTP plugin `tokenEnv` bearer path. A public Field app plugin must authenticate the user against their own Field origin through this OAuth MCP surface. This maintained document owns the runtime contract.

## Field configuration

Set an HTTPS public origin with no path, query, credentials, or fragment:

```json
{
  "mcp": {
    "enabled": true,
    "publicOrigin": "https://field.example.com"
  }
}
```

`FIELD_PUBLIC_ORIGIN` overrides `mcp.publicOrigin`. A synthetic verification installation (`FIELD_ENV=field-verification`) is also an explicit opt-in: when `fieldVerification.enabled` is true and `mcp.enabled` is not explicitly false, Field enables MCP at `fieldVerification.publicUrl`, so a release can prove the integration without editing its synthetic config; setting `mcp.enabled` to false always wins. Every other installation requires the explicit `mcp.enabled: true` configuration above.

Production must terminate trusted HTTPS at this origin and route the discovery, `/oauth/*`, and `/mcp` paths to the same Field process. OAuth state is stored under Field's existing data boundary at `mcp/as-state.json` with mode `0600`; authorization codes and access/refresh tokens are random, returned only in protocol responses, and stored only as SHA-256 hashes. The on-disk path deliberately avoids the verification boundary marker `oauth`. Back up and restrict the data directory as customer authentication data.

The approval page is same-origin and authenticates through the existing Field human email/password authenticator. With Clerk sign-in (`FIELD_AUTH=clerk`, `docs/CLERK_SIGN_IN.md`) it asks for no password: only an owner already signed in to Field with a verified owner sign-in (Google Workspace, or a recent second factor) can approve, from that session; anyone else gets a link to Field's sign-in that returns to the approval page. Its HTML matches Field's default login visual language: Field. brand, email/password, listed scopes, and Authorize. Do not regress it to unstyled markup. Authentication and scope authorization are separate injected boundaries. Administrators may authorize every MCP scope; a non-admin may authorize only scope strings present exactly in that Field user's `permissions` array, and the complete requested set must be allowed. A later Google OIDC login may replace only the human authenticator/approval ceremony: Field is not currently an OpenID Provider and deliberately publishes no `/.well-known/openid-configuration`. Client registration, PKCE, redirect URIs, grants, scope authority, token formats, refresh rotation, and `/mcp` remain Field-owned.

## Connecting an MCP client

Any MCP client that supports OAuth for remote servers can connect: path-specific
RFC 9728 protected-resource discovery, RFC 8414 authorization-server discovery,
dynamic public-client registration, Authorization Code with PKCE S256,
resource indicators, refresh tokens, and Streamable HTTP.

Point the client at `https://field.example.com/mcp` with transport
`streamable-http` and OAuth auth, request the scopes it needs (for example
`catalog.read catalog.write quote.read quote.draft`), and limit it to the
`field_*` tools. The client registers itself with a loopback redirect URI such
as `http://127.0.0.1:8989/oauth/callback` (Field accepts `127.0.0.1` or
`localhost` when registered exactly), opens the Field approval URL, exchanges
the one-time code and stores and refreshes credentials in its own OAuth store.
Do not copy Field secrets into the client's config, and do not place the
callback URL or code in chat or logs.

## Tool and scope matrix

| Scope | Tools | Behaviour |
| --- | --- | --- |
| none after OAuth | `field_markup_preview` | Local calculation only. Required inputs are `cost`, `basis` (`markup` or `margin`), and `percent`. Do not expose XOR `markupPercent`/`marginPercent` fields; strict client schemas mark every property required. |
| `catalog.read` | `field_price_item_list`, `field_price_item_get`, `field_quote_catalog_list`, `field_pricing_review`, `field_catalog_structure` | Direct catalog reads |
| `catalog.read` + `catalog.write` | `field_price_item_update`, `field_catalog_structure_update` | Both scopes required, including dry-run; exact `APPLY` plus fresh `expectedVersion`; re-read race check; before/after audit |
| `quote.read` | `field_quote_list`, `field_quote_get`, `field_quote_validate`, `field_milestone_rules_list`, `field_milestone_check` | Direct Field selection/read semantics. `field_quote_get` defaults to a focused `quote` view; pass `view=full` only when evidence/activity is required. `field_milestone_rules_list` reads the organisation's milestone rules (#29); `field_milestone_check` shows, for one job, the rule that applies and each milestone's caps before issue (`blocked: true` is a hard cap). Rules are read-only here: only an owner changes them, in Settings or with the owner's CLI key. |
| `quote.read` + `quote.draft` | `field_quote_draft_create`, `field_quote_draft_update`, `field_quote_recalculate` | Both scopes required, including dry-run; unsent drafts only; exact `APPLY`/version race guard/audit. A `patch.paymentSchedule` is marked as an override of the account's milestone rule and never lifts its caps. |

There are no send, email, publish, issue, invoice, payment, acceptance, deletion, evidence, or lifecycle-delivery tools.

Catalogue tools name an item by uid: `item` on `field_price_item_get`,
`field_price_item_update`, `field_pricing_review` and `field_catalog_structure`,
and `parentItem` / `childItem` on `field_catalog_structure_update`. A legacy
item code is still accepted in those fields, and `code`, `parentCode` and
`childCode` remain as deprecated names for them. Catalogue answers carry no
item codes.

List tools declare object output schemas and return records in both `structuredContent` and the JSON text content: `field_quote_list` as `{ "quotes": [...] }`, `field_price_item_list` as `{ "items": [...] }`, and `field_quote_catalog_list` as `{ "catalogs": [...] }`. MCP structured content must remain a top-level object so clients can validate it as a record.

## Security contract

- MCP rejects missing, unknown, expired, revoked, wrong-resource and insufficient-scope access credentials. Its access tokens are audience-, client-, subject- and scope-bound. A 403 insufficient-scope response includes the OAuth `error`, complete required `scope`, and RFC 9728 `resource_metadata` parameters in `WWW-Authenticate`; MCP clients parse those exact challenge fields.
- Authenticated Field users can list their own active installations at `GET /api/mcp/installations` and revoke one at `DELETE /api/mcp/installations/:id`. An administrator may list all, filter by `?subject=USER_ID`, and revoke any installation. Non-admin users cannot inspect or revoke another subject's grant. With Clerk sign-in both routes are for an owner signed in to Field only, never a token. Each installation reports its client name, approver (`subject`), scopes, `createdAt` and `lastUsedAt` (recorded at most once a minute), never a token. Owners see and disconnect them in Settings, Team, Access, and every disconnect writes an access audit event (who, which installation and client name, whose, when; `GET /api/access/events`).
- Every `/mcp` call and every refresh is checked against the approving person's access now (`currentScopes`): a grant works only within the scopes that person may authorize today. A person removed, `pending`, turned off, out of the organisation, or removed, banned or locked in Clerk gets `401 invalid_token` on `/mcp` and `invalid_grant` on refresh, and that refresh family is revoked. A demoted owner's grant drops to their staff permissions. If Field cannot check, `/mcp` answers `503`.
- Existing broad Field bearer tokens remain compatibility credentials for current UI/API routes only, in the `Authorization` header only (never `?token=`). `/mcp` never resolves them.
- Dynamic registration accepts public Authorization Code/refresh clients only, exact registered HTTPS redirects or HTTP loopback redirects, and no client secret. Redirects reject credentials and fragments, at most eight redirects of at most 2,048 characters are accepted, client names are limited to 128 characters, and registration returns `client_id_expires_at`.
- Authorization requires exact registered redirect URI, nonempty state, exact MCP resource, allowed scopes, and PKCE S256. Approval requests and codes expire and are one-time.
- Refresh tokens rotate. Replaying a rotated refresh token revokes its whole family. Revocation also invalidates the family.
- The approval POST uses a one-time request plus a separately generated CSRF secret; responses are `no-store`. Passwords and protocol credentials are never logged by this implementation.
- Field rate-limits the OAuth endpoints (`/oauth/register`, `/oauth/authorize`, `/oauth/approve`, `/oauth/token`, `/oauth/revoke`) to 60 requests in 15 minutes per address, and failed approvals to 20 in 15 minutes. Each installation has its own `/mcp` budget of 600 calls a minute, counted only after its token checks out. A spent budget answers `429` with `Retry-After` (`docs/RATE_LIMITS.md`). An MCP client refreshes about every 10 minutes, well inside the budget.
- Production may also add edge request-size controls, and should monitor generic failure counts without recording request bodies or URLs.

## Deployment prerequisites

An installation requires an explicit MCP enablement and public origin. Before calling it complete: verify the final HTTPS origin and unrewritten discovery/OAuth/MCP paths, ensure the data directory is private and backed up, check `FIELD_TRUST_PROXY` matches the proxy hops in front of Field (rate limits count per address), then connect an external MCP client and run an authenticated initialize/list/call smoke test. Use the installation routes to prove subject visibility and revocation.
