> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formation.texturehq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Remote MCP

> Connect remote MCP servers and govern their tools, authentication, and evidence.

Formation connects each agent to remote MCP servers over **Streamable HTTP with an
agent-owned credential**: either a bearer token or API key the operator pastes,
or an OAuth grant the operator authorizes in the browser (below, "Connect with
OAuth"). Connections support both read and write tools and are available in
every conversation with that agent, including new conversations. Multiple
servers and multiple accounts at the same endpoint can be connected independently. Native GitHub and GitLab
tools keep their own credentials and enablement.

## Connect a server

1. Open the agent's **Tools** page and choose **Add tool**.
2. Select a service and use its recommended sign-in method or supported API-key
   alternative. For **Custom MCP server**, choose authentication and enter a
   connection name and public HTTPS endpoint under **Connection details**.
3. Choose **Connect** for an API key, or **Continue to sign in** for OAuth.
   Formation saves this agent's credential, discovers the catalog,
   and activates every supported tool, including writes, for the next turn.
4. Optionally choose **Manage tools** to exclude tools, then **Save tool preferences**.
   **Select all**, **Deselect all**, **Select read-only**, and individual checkboxes
   change this optional selection. Saving with every tool turned off is allowed.

If discovery fails, the account remains saved and the page shows catalog health.
Formation retries automatically. Connecting requires no extra approval checklist.

Linear's standard endpoint supports reading, creating and editing issues and
comments using a bearer API key or OAuth access token. The token must have the
corresponding permissions in Linear. See [Linear's MCP documentation](https://linear.app/docs/mcp).
Formation discovers the actual tools and schemas offered to that account; it does not
maintain a vendor-specific tool list.

For an existing bearer connection using `/mcp/readonly`, choose **Edit connection**,
open **Connection details**, change **MCP endpoint** to
`https://mcp.linear.app/mcp`, and choose **Save changes**. Leave the token blank
to reuse the saved credential. The expanded catalog is discovered automatically;
saved exclusions persist. An OAuth account needs a new sign-in at the new
endpoint: connect Linear from **Add tool**, then turn off the old read-only
connection. Tool exclusions on the old account do not transfer to a new account. Endpoint edits
replace only this agent's binding; other agents using the old endpoint keep their
own connections. Named account edits never overwrite a sibling account. Older,
endpoint-based connections retain their collision check when moving to an
endpoint already occupied by another legacy connection.

## Multiple accounts at one endpoint

Each new dashboard connection creates a separate account identity, for both
bearer tokens and OAuth. Give accounts recognizable names, such as “Linear —
Engineering” and “Linear — Support.” Each has its own encrypted credential,
tool preferences, version, tool namespace, and turn-off action. Reauthorizing or rotating
one account does not change another account, even at the same endpoint. Accounts
remain agent-owned; connecting one does not grant another agent access.

API clients opt into this behavior with `new_account: true` on
`POST /api/agents/{slug}/mcp/providers` or
`POST /api/agents/{slug}/mcp/oauth/start`. To update an existing account, use its
returned `provider_id`: the versioned PUT route for bearer edits, or
`provider_id` in OAuth start for reauthorization. OAuth start rejects requests
that specify both `provider_id` and `new_account: true`.

Omitting `new_account` preserves the legacy endpoint-based reconnect slot. It
never selects or overwrites an explicitly created account. Existing connection
IDs, encrypted secret references, tool approvals, historical aliases, and pending
OAuth authorizations survive the migration unchanged. Pending authorizations
created before the upgrade retain legacy reconnect semantics.

The migration replaces the global endpoint uniqueness constraint with uniqueness
for legacy endpoint slots. **Do not run older binaries against the migrated
schema:** their endpoint upsert does not recognize the partial unique index.
Drain old application replicas before applying this migration and starting new
replicas. A rollback to older binaries requires restoring the pre-upgrade database
backup, or explicitly removing/reconciling additional accounts and restoring the
original endpoint constraint. Deleting duplicates automatically would discard
credentials, approvals, or historical provenance and is not a supported rollback.

The [unified capability editor](/runtime/capability-setup.md) and
[connection API](/runtime/connection-management.md) manage native, MCP and web
connections. Sandbox delivery of the agent's own coding credentials is also
implemented, with a separate [authorization contract](/runtime/workspace-credentials.md).
MCP credentials are not automatically delivered to a shell. Separate accounts
retain separate cache entries keyed by agent and provider identity.

## Connect with OAuth

Some servers issue no token to paste. Contentful's hosted MCP server
(`https://mcp.contentful.com/mcp`) is one: it implements the
[MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization),
so the only way in is to sign in and consent in a browser. Formation is the OAuth
client in that flow.

1. In **Add tool**, choose a service that recommends OAuth, or choose
   **Custom MCP server** and select **Sign in with OAuth**. For a custom server,
   enter a connection name and HTTPS MCP endpoint under **Connection details**.
   Leave the pre-registered client fields empty
   unless the server's documentation gave you a client ID for Formation.
2. Choose **Continue to sign in**. Formation discovers the server's authorization
   server, settles its own client identity, and sends the browser to the
   server's sign-in page. Consent there is the server's, not Formation's: it decides
   what the grant may reach (Contentful asks which spaces and environments).
3. The browser returns to the agent's tools page with the account connected and
   its supported tools active. **Manage tools** is optional. Reauthorizing an
   existing account retains its exclusions and explicit turn-off state.

What Formation does on your behalf, and refuses to do:

* **Discovery** follows the spec: an unauthenticated request for the server's
  `WWW-Authenticate` challenge, RFC 9728 protected resource metadata (from the
  challenge or the well-known locations), then RFC 8414 or OpenID metadata for
  the authorization server, in the spec's order. The authorization server must
  advertise PKCE (`S256`) or Formation stops. Every URL involved must be HTTPS and
  public; redirects are never followed.
* **Client identity** is chosen in the spec's priority: pre-registered
  credentials you entered, then a
  [Client ID Metadata Document](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00)
  when the authorization server supports one (Formation serves its own at
  `PUBLIC_URL/api/mcp/oauth/client`; this needs an HTTPS `PUBLIC_URL`), then
  RFC 7591 dynamic registration. A server offering none of these needs a
  pre-registered client ID from its operator.
* **The authorization request** carries a PKCE challenge, an unguessable
  `state`, the RFC 8707 `resource` (the server's own identifier from its
  metadata) and the scopes the server asked for in its challenge, or those its
  metadata lists. Formation requests nothing beyond that.
* **The redirect URI** is always `PUBLIC_URL/api/mcp/oauth/callback`. The
  callback is single-use and expires ten minutes after the start; it must be
  completed by the operator account that started it, and when the server says
  it echoes its issuer (RFC 9207) a mismatch is refused before any code is
  redeemed.
* **Tokens** are stored encrypted under `MESH_AGENT_MASTER_KEY` in the same
  `secrets` row a pasted token would use, together with the token endpoint and
  client identity needed to refresh them. Formation refreshes an access token
  itself, shortly before it expires or once when the server answers 401, under
  the connection's row lock so replicas never spend a single-use refresh token
  twice. Rotated refresh tokens replace the old one.
* **A refused refresh** (`invalid_grant`: the grant was revoked, or the server
  issued no refresh token and the access token expired) marks the connection
  with `auth_error`. Its tools stop being offered without stopping the turn,
  the dashboard shows the reason, and **Reconnect with OAuth** starts a new
  authorization for the same connection. Reauthorizing replaces the credential
  and preserves tool preferences; renaming keeps them too. Moving
  an OAuth connection to another endpoint means connecting that endpoint with
  OAuth: the grant is bound to the resource it was issued for.

Formation never asks for scopes on its own, never sends a token anywhere but the
registered endpoint and its token endpoint, and never surfaces access or
refresh tokens through the API or the dashboard.

## Identity and execution

`tool_providers` describes an endpoint. `agent_tool_providers` binds it to an
agent-owned encrypted secret, connection label, exclusions, optional legacy tool
classifications, and the credential's `auth_kind` (`bearer` or `oauth`). A pending
browser authorization lives encrypted in `mcp_oauth_authorizations` until its
callback consumes it. Sharing an endpoint never shares accounts, catalogs or
permissions between agents. Credentials are never returned by the API.

The access policy is **all supported tools except explicit exclusions**. New tools
are included automatically; changed definitions do not reset the selection.
Exclusions include removed names so a tool that reappears stays off. Credential
rotation and endpoint edits preserve preferences and explicit connection turn-offs.
All changes still fence offers created under an earlier connection version.

Tools use `ClassEffect`/`Effectful` automatically: Formation records the durable effect
boundary before the call and never replays an uncertain remote operation. The
operator does not have to classify tools. The legacy API still accepts a `query`
classification against an exact fingerprint. An unchanged explicit query keeps
`ClassQuery`/`Replayable`; a changed definition returns to effect semantics. Unknown
classes are refused on submission and interpreted as effects in stored legacy
rows. Server read-only annotations remain hints; they do not authorize replay.

In **Manage tools**, review each tool's **Execution behavior** separately from
its enabled checkbox. **May change external state** is the conservative default.
Choose **Read-only query (safe to replay)** only after verifying that the exact
advertised operation cannot change external state. Query calls can be retried
automatically. Bulk tool selection does not approve query classifications.
Changed definitions require another explicit review before query semantics
apply. A disabled tool retains its saved classification without becoming enabled;
changing these preferences does not turn on a disabled connection or rotate its
credential.

The versioned enable endpoint accepts an additive `tool_classifications` patch:
`[{"name":"read_issue","fingerprint":"<reviewed fingerprint>","class":"query"}]`.
Each entry must identify a currently supported tool and its exact discovered
fingerprint, with an explicit `query` or `effect` class. Omitted entries retain
their classifications, including temporarily missing tools. This field can be
combined with `disabled_tools` and `keep_state: true`; without an exclusions
change it preserves the existing selection. Do not combine it with the legacy
`tools` full-selection field. Connection responses expose saved reviews through
`tool_classifications`, including disabled tools; `tools` continues to report the
enabled effective catalog. Stale versions, stale fingerprints, unsupported or
duplicate entries fail without partially changing the connection.

These preferences govern future calls. They do not establish that an earlier
effect finished or release its unresolved mutation claim. Reconcile only the
specific recorded operation after independently verifying its outcome; never
classify a write as a query merely to bypass contention.

A retried query keeps its original tool-call identity. After current account,
definition, arguments, source, and execution checks, an exact completed tool
observation is reused without another remote call. A provenance reservation
without a completed observation is not an older result: only an explicitly
approved, still-pending query may repeat that read. Its evidence identity must
match exactly. Effect calls never gain this retry path, even if the server marks
them read-only. Query source reservations also retain their original connection
version and source identity atomically. A rotated connection, missing historical
source stamp, or mismatched stamp cannot adopt that earlier reservation.
Pending tool admissions also bind the account and connection version before any
source capture, so rebuilding a registry cannot move an older decision to a
rotated credential.

A query classification does not widen where a call may go: every MCP call,
whatever its class, is an outbound request to the registered endpoint and
nowhere else ([R21 (repository)](https://github.com/TextureHQ/formation/blob/main/docs/roadmap/risks.md#r21) is about query tools that fetch
arbitrary URLs from the key-holding process; a remote MCP tool cannot).

Discovery fingerprints the full advertised definition, including descriptions,
schemas and annotations. Every call verifies that its offered fingerprint still
matches the live definition. Refreshing adopts current definitions automatically;
an in-flight call with an older definition is rejected. Unsupported schemas are
shown in **Manage tools** and omitted from the offer. **Turn off** requires no
network access.

## Upgrading existing connections

Migration `20261002233000_mcp_tool_preferences.sql` converts reviewed subsets to
exclusions using a catalog from the current connection version. If the snapshot
is absent or stale, the old allowlist remains until the first successful discovery
can convert it. Stored classes and credential ownership are retained. Unreviewed
connections left off by the old setup default become active automatically.
Explicit turn-offs are preserved from
stored selections and the latest successful enable/disable audit event. Repeated
boots do not rewrite the migrated policy.

API clients can submit `{ "version": n, "disabled_tools": [] }` to the existing
`enable` route to allow all supported tools, or include the names to exclude.
Use `{ "version": n, "keep_preferences": true }` to turn an account back on
without replacing its preferences, including a legacy subset.
The version-fenced legacy `{ "version": n, "tools": [...] }` request remains
supported and is converted to exclusions. Do not supply both formats.

## Cached discovery

Turns normally discover available tools from the connection's stored catalog,
not by contacting every server before the first model call. A missing or
configuration-mismatched catalog is discovered live and saved for later turns.
A background worker also refreshes catalogs approximately every fifteen minutes,
using database leases to avoid duplicate refresh work.

The cache stores metadata, not authority:

* Each agent/account has its own catalog. Sharing an endpoint does not share a
  credential or the tools visible to that credential.
* Calls open a session and compare the offered definition fingerprint to the
  live definition before execution. A changed definition is refused for that
  stale offer, then current definitions can be adopted by a later refresh.
* Credential, endpoint, preference and enablement changes invalidate the old
  connection version. Old offers cannot execute under the new configuration.
* Temporary refresh failures keep a usable catalog for the unchanged connection;
  call-time authentication and definition checks still apply. A refused OAuth
  grant makes the account unavailable until repaired.

The connection's `catalog` reports `health` (`pending`, `ok`, `unavailable`,
`unauthorized`), bounded `detail`, `captured_at`, `failed_at`, `revision` and
`stale`. A stale catalog belongs to an older connection configuration and needs
refresh before use. A failed first discovery has no captured catalog to serve;
inspect the reported health rather than interpreting it as a server with no
tools. An account not yet inspected may have no `catalog` field.

Remote write failures can leave an uncertain outcome. Formation does not automatically
retry effectful calls after a crash or transport failure; an effect tool's
remote failure stops the turn with the effect recorded for recovery. A query
tool's remote failure is an observation the model sees and can recover from
(narrow, retry, or explain). Local persistence failures stop the turn for both.
MCP transport retries remain disabled for both classes; the query class changes
what the turn does with a failure, not how many times the wire is tried.

## Knowledge and provenance

MCP results belong to the agent, without a conversation allowlist or audience
snapshot. Before calling a tool, Formation validates the persisted call's agent/run
ownership and records provider, remote name and fingerprint in
`tool_source_evidence`. Successful observations retain that evidence in memory
writes and reply provenance. Tool-source leaves carry no conversation restriction;
`public` in this evidence representation means unrestricted across the owning
agent's conversations, not shared credentials or cross-agent memory access.

Ordinary human-message evidence still retains its own privacy rules. A memory
that combines external data with private conversation content retains the
restrictions belonging to that human content. Migration `0056_agent_wide_mcp.sql`
removes the old MCP conversation scopes and updates existing tool-source leaves
in memories and replies without changing human-source leaves.

## Supported protocol and resource limits

* Public HTTPS endpoints on port 443, authenticated by an `Authorization: Bearer`
  header sent only to the registered URL. Private addresses, redirects,
  environment proxies, URL credentials and query parameters are refused.
* Up to 256 discovered tools and 16 discovery pages per connection operation.
  Every supported, non-excluded tool is available for discovery across enabled connections;
  there is no additional aggregate tool-count or 64 KiB schema selection cap.
  Progressive `tool_search` loading controls which definitions enter a model request;
  loaded definitions still consume its ordinary context budget.
* Input/output schemas up to 32 KiB; descriptions up to 4 KiB; HTTP response
  streams up to 2 MiB; model-visible results up to 512 KiB. A larger result is
  **truncated to fit, never refused**: structured content is dropped first
  (it usually duplicates the text), then the text's tail is cut on a rune
  boundary, and the envelope carries a `truncated` notice with the total and
  shown sizes and how to ask for less. The credential check runs on the whole
  result before the cut.
* Validated inline JSON Schemas, at most 64 levels deep. Schema references and
  `x-mcp-header` are unsupported. Unsupported tools are displayed but not selected.
* Text and structured JSON results. Non-text results return errors;
  resource links and attachments are not fetched.
* A 20-second operation deadline, at most four concurrent provider discoveries,
  and a 40-second overall discovery budget. Completed catalogs remain available
  if other servers time out. Local database failures and parent cancellation
  stop discovery.
* Up to 1,000 tool-source IDs per memory or reply. Combined reply lineage beyond
  5,000 sources uses the existing unknown-provenance compaction behavior.

The official Go MCP SDK handles negotiation and Streamable HTTP; its `oauthex`
and `auth` packages parse challenges and metadata and perform dynamic
registration, while Formation owns the browser round-trip, token storage and
refresh (`internal/tool/mcp/oauth.go`). Local stdio, step-up scope challenges
(`insufficient_scope` at runtime), roots, sampling, elicitation, resources and
prompts are not supported by this connection flow.

## Administration API

Routes require the existing authenticated agent-access authorization and CSRF
boundary, and return `Cache-Control: no-store`.

| Method | Suffix under `/api/agents/{slug}/mcp/providers` | Purpose |
| - | - | - |
| GET | — | Connections and optional Linear preset endpoint. Each connection carries `auth_kind`, any `auth_error`, and `catalog` health once the connection has been looked at |
| POST | — | Save `{name, endpoint, token, new_account?: true}`; `new_account: true` creates an independent account, omission reconnects the legacy endpoint slot; discovers and activates supported tools, preserving saved off states on reconnect |
| PUT | `/{provider}` | Edit `{version, name, endpoint, token?}`; omitted/blank token reuses the saved credential |
| POST | `/{provider}/discover` | Discover the agent's current catalog |
| POST | `/{provider}/enable` | Save exclusions and optional `tool_classifications: [{name, fingerprint, class}]` with `version`; use `keep_state: true` to retain connection state, or `{version, keep_preferences: true}` to turn it on; legacy `tools` remains a full selection |
| POST | `/{provider}/disable` | Disable without a network call |

An owner may explicitly review `completion_contract: "linear-atomic-append-v1"`
on an `effect`-class `save_issue` entry for the official
`https://mcp.linear.app/mcp` endpoint and its exact current discovered fingerprint:

```json theme={null}
{
  "version": 7,
  "keep_state": true,
  "tool_classifications": [{
    "name": "save_issue",
    "fingerprint": "<current fingerprint from discovery>",
    "class": "effect",
    "completion_contract": "linear-atomic-append-v1"
  }]
}
```

The additive `tool_classifications` field preserves other reviews and selected
tools. The legacy `tools` field replaces the selected set: include the other tools
and their existing reviews when retaining them. Exclusion-only and keep-preferences
saves preserve recorded contracts. Omission grants no completion contract; server
annotations never grant one. Unknown contracts, queries, other tools/endpoints,
and stale fingerprints cannot acquire this review. Definition drift withdraws
the effective contract until a fresh explicit review.

This contract trusts termination of one existing issue's atomic append invocation:
exactly a canonical issue UUID and one nonempty append patch, with a supported
final issue response naming that same UUID. It does not cover create, other
patch modes, relationships, bulk work, async acceptance or an entire task. A
returned description excerpt may leave the content postcondition unverified;
issue workflow state and downstream webhooks are not invocation-completion
signals. Review does not execute a tool, permit replay of a write, or retroactively
upgrade an older receipt. Account ownership and the original connection version
remain fenced independently of this semantic review.

For a fresh supported invocation, the runtime records compact original-operation
proof in `invocation_completion` observation metadata. A trusted terminal receipt
and release of that operation's target claims commit together. Recovery may reuse
that exact receipt without contacting the provider; an absent or uncertain receipt
never authorizes another write. `postcondition_verified` is diagnostic only: it
does not complete an issue, a task, or its evidence requirements. Unsupported
arguments and results retain conservative holds even on a reviewed connection.

OAuth routes. The start is agent-scoped; the callback and client metadata are
install-wide because an authorization server sees one redirect URI and one
client identity per install.

| Method | Path | Purpose |
| - | - | - |
| POST | `/api/agents/{slug}/mcp/oauth/start` | `{name, endpoint, provider_id?, new_account?: true, client_id?, client_secret?}`; returns `{authorization_url}` for the browser. `provider_id` reauthorizes an existing connection; `new_account: true` creates an independent account and is rejected when `provider_id` is provided |
| GET | `/api/mcp/oauth/callback` | The registered redirect URI; session required. Redirects to the agent's tools page with `mcp_oauth=connected&provider=…` or `mcp_oauth=error&reason=…&detail=…` |
| GET | `/api/mcp/oauth/client` | The install's Client ID Metadata Document; public, served only when `PUBLIC_URL` is HTTPS |

Connections list `auth_kind` and, when a grant was refused, `auth_error`. They
also list `catalog` once the connection has been looked at — see "Cached
discovery" above for its fields and for what a stale or unavailable catalog
means.

Migration 0069 adds `mcp_catalog_snapshots`, one row per connection. Its tool
documents are stored in a `json` column rather than `jsonb` on purpose: a
fingerprint is taken over the exact bytes a server advertised, and `jsonb`
stores a parsed structure that it re-renders with sorted keys, so a round trip
through it would change every fingerprint and read as though every reviewed tool
had been modified.

Migration 0055 requires ordinary PostgreSQL and is applied by the normal migration
runner. It drops `conversation_scopes`; older MCP binaries cannot be run against
this schema. The upgrade preserves enabled connections and selected tools, making
them agent-wide. Existing read-only endpoints remain read-only until edited.

Integration tests use real PostgreSQL and local TLS MCP servers. They cover
multiple agents and servers, read/write calls, new conversations, encrypted
credential reuse, stale reviews, revocation, schema validation and provenance.
They do not contact Linear or require production credentials.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.