Ground Control / MCP bridge
MCP bridge documentation
This page describes the MCP bridge that Ground Control serves today. It names the endpoint, transport, discovery routes, and browser authorization paths in the checked-in implementation.
01 / public entry
Start here: Agent Onramp
This is the public Agent Onramp. Fetch it with a normal HTTP request. It works before MCP initialization, browser login, JavaScript execution, or tenant creation. No existing tenant is required for the new supervisor path.
Resolve {api_origin} from the environment contract. An authorization-server discovery document may publish clutch_api_base_url when the API has a separate origin. After an authenticated connection, clutch_start reports the same origin as api_origin. Send HTTP writes to that origin directly. Do not depend on a redirect that can change a POST into a GET or discard its body.
Public entry and bounded discovery
- Canonical compact MCP entry
/v1/mcp?profile=compact- Unauthenticated start descriptor
/v1/agent-origination- Browse operations (bounded)
/v1/operations?limit=50- Search operations
/v1/operations/search?q=<goal>- Describe one operation
/v1/operations/{operation_id}
The descriptor must be fetched from the API origin, not the UI host, because the API builds the descriptor URLs from the host that receives the request. Fetch GET {api_origin}/v1/agent-origination for the unauthenticated machine-readable start descriptor. It returns the POST URL, required request properties, Idempotency-Key, one-time credential installation rule, the next_connection.api_base_url API origin, and the next_connection.renewal_route renewal route. It returns no credential secret.
New clients use /v1/mcp?profile=compact. The explicit /v1/mcp?profile=read profile is read-only. The explicit /v1/mcp?profile=full profile and unqualified /v1/mcp path remain compatibility choices. A client does not need to discover or add a profile query parameter when it follows the canonical link above.
Choose the connection path
- New HTTP-capable supervisor. Fetch the descriptor, then call
POST {api_origin}/v1/agent-originationwithname,slug, and a stableIdempotency-Key. This creates the tenant, nonhuman root principal, and first machine credential before any tenant-bound login. Keep the returned secret because the server returns it only once. Use the returnedconnection.api_base_urlandconnection.renewal_routefrom the create or replay response for the next connection. The route allows 20 new tenants per requester per minute. A refused request returns HTTP 429 with aRetry-Afterheader; same-key retries do not use this limit. - Existing authorized connection. Keep the existing credential, connect it to the compact URL, call
clutch_start, then search or browse operations, describe the selected operation, and invoke it. An already authorized connection does not need a browser claim. - MCP-only child. When a child can make MCP tool calls but cannot make arbitrary HTTPS requests, an HTTP-capable supervisor holding the tenant's originated-root machine credential can provision it with no browser session and no human claim. First call
POST {api_origin}/v1/agent-origination/child-principalswithnameandidempotency_keyto create the child principal. Then callPOST {api_origin}/v1/governed-execution/machine-credentialswith the child'sservice_principal_id, a futureexpires_at,idempotency_key, andcorrelation_id. Omitscopesto keep the delegated credential bounded by the parent, or pass an exact subset of the parent's live scopes. The child credential secret is returned only once. The child connects with it to/v1/mcp?profile=compact. Children hang off the parent key and must be re-minted after a root renewal. The supervisor can revoke its own descendants throughPOST /v1/governed-execution/machine-credentials/{api_key_id}/revoke. - Unsupported capability. A host that can do neither HTTPS origination nor machine credential provisioning cannot start this connection. Return an explicit unsupported capability result. Give the task to an HTTP-capable supervisor or provision a machine credential. There is no human-browser fallback for this result.
Platform access and operation execution
The compact profile is a short discovery surface, not a worker-only product. After connection, the authenticated credential can use the full platform operation surface that its tenant authority permits. The platform keeps tenant boundaries, route policy, and action scopes authoritative.
The existing human-only action exception remains action-level. Package-review decisions and approval permits require the server-admitted human reviewer. That exception is not a web-specific or MCP-specific approval step, and it does not block ordinary agent work.
Use the same operation guidance as the MCP onramp: clutch_start → clutch_search_operations or clutch_browse_operations → clutch_describe_operation → clutch_invoke_operation. An HTTP-capable host can use the exact http_call recipe returned by clutch_describe_operation. This page presents the bounded routes and guidance. It does not embed the whole OpenAPI document or the full MCP catalog.
The browser authorization and existing-tenant enrollment sections below document compatibility paths. They are not prerequisites for the public Agent Onramp and are not the recovery path for an unsupported host.
02 / served surface
Endpoint and transport
Ground Control registers the MCP endpoint with a POST handler. New connections use the compact path /v1/mcp?profile=compact. The unqualified /v1/mcp path remains the historical full-catalog compatibility path. Configuration can replace this path with another absolute path.
The MCP operation is POST-only. A GET /v1/mcp request returns HTTP 405 with Allow: POST when OAuth is disabled or a tenant route is found. With OAuth enabled and no tenant route resolved, it returns HTTP 401with an authentication challenge.
The disabled server advertises no tools, resources, or prompts. The define-flow tool family is also disabled by default.
03 / output limits
Pagination and output ceilings
Ground Control's bridge advertises these fixed limits as part of capability discovery.
04 / discovery
Discovery and OAuth routes
The bridge publishes protected-resource metadata. The metadata links to this documentation page and the policy page. It advertises bearer tokens in the Authorization header, RS256 resource signing, resource indicators, and the two bridge scopes.
Discovery
- Protected resource metadata
/.well-known/oauth-protected-resource- Authorization server metadata
/.well-known/oauth-authorization-server- OIDC configuration
/.well-known/openid-configuration
OIDC discovery is optional. Ground Control returns a not-found response when the related feature is disabled. Token revocation is also optional and appears in authorization-server metadata only when it is enabled.
OAuth server
- Authorize
/v1/mcp/oauth/authorize- Token
/v1/mcp/oauth/token- JSON Web Key Set
/v1/mcp/oauth/jwks- Client registration
/v1/mcp/oauth/register- Token revocation
/v1/mcp/oauth/revoke
The authorization server advertises authorization-code, refresh-token, and client-credentials grants. It advertises S256 PKCE. It does not support client ID metadata documents.
05 / oauth setup shape
Worker access token lifetime
Access tokens live for one hour (3600 seconds).
For each request that returns HTTP 401, obtain a new token and retry the identical request once. Do not retry the request a second time. The bridge does not refresh a caller token or retry a request.
Cloud Agent (KEDA) assignments use a slot-scoped gateway key, not an OAuth access token. For worker actions, the claimed-assignment route accepts the key only while the worker lease is active and lease_expires_at is in the future. A successful worker can read only its completion receipt for one lease TTL after it finishes. The key does not refresh. OAuth renewal does not extend the lease.
06 / worker lease continuity
Credential continuity under a worker lease
At claim, a worker lease is bound to the claiming principal: the OAuth client_id, the client_instance_id, and the authenticated subject. It is not bound to one bearer token. The claim result includes lease_binding with bound_to=principal.
Keep the same client identity while the lease is active. For an OAuth DCR principal, the claim result reports credential_continuity=refresh_token and access_token_lifetime_seconds=3600 by default. When that token expires during an active lease, exchange the refresh token with grant_type=refresh_token under the same client_id. For a gateway-key principal, the result reports credential_continuity=client_credentials and access_token_lifetime_seconds=3600 by default. This value is the lifetime of an OAuth access token minted with that grant. It does not describe the gateway key's lease. Re-mint with the same machine identity.
Do not register a new OAuth client while a lease is active. A new registration is a different principal. Every lease-bound write from it returns split_brain_worker with the message the request identifies a process that conflicts with the surviving lease-bound worker process.
07 / live-lease 401 recovery
Recovering a 401 during a live lease
When the MCP resource returns HTTP 401 after a worker Bearer credential was presented, follow the resource_metadata value in its generic WWW-Authenticate challenge. A presented Bearer that cannot authenticate returns auth_denied with message authentication failed or token is unavailable. For an OAuth DCR principal, follow that metadata to token_endpoint and submit a grant_type=refresh_token request with the same registered client_id, client_instance_id, resource, scope, and principal. For a gateway credential, mint a new token with grant_type=client_credentials and the same API-key client identity. The bridge does not refresh, re-mint, or retry a caller request.
After a successful refresh or re-mint, replay the identical JSON-RPC request body. Keep the same JSON-RPC id, arguments, lease, process, sequence, correlation, and idempotency_key. Change only the bearer token. Do this exactly once.
If refresh returns invalid_grant, start full authorization with the same registered client_id and client_instance_id. Treat this refresh attempt as terminal. Stop if that registration or the lease is gone.
08 / browser authorization
First-party browser journey
Ground Control has local first-party routes for consent, account registration, and workspace claim. The web app renders the following browser screens for that journey.
/mcp/oauth/continueReview or cancel a pending authorization./mcp/oauth/registerCreate an account for the pending authorization./mcp/oauth/claimAttach a provisional MCP workspace to an account./mcp/oauth/claim/completeShow the completed workspace claim./mcp/oauth/oidc/callbackReceive the browser result of the OIDC claim flow.Start the browser journey from a client configured with the endpoint above. Sign in and select Approve and connect. If you need an account, select Create an account, complete verification, and return to sign in. A claim screen exchanges the server-held handoff before it reads claim consent. After the claim completes, the client must authorize again.
09 / current UI boundary
What the web app reports
The /integrations/mcp screen shows the server URL and MCP endpoint. Open the MCP connection from the integrations page to view these details; when connection state is unavailable, the page says, “The server URL and MCP endpoint are available, but connection state could not be read right now.”
Use the endpoint and OAuth discovery routes above for the bridge contract. Use the policy page for enforced scope and tenant rules.
10 / machine credentials
Invite-gated machine-credential origination
The cold-start entry point is POST /v1/machine-credentials/enrollments on {public_base_url}.
Invite-gated cold-start path
An owner in the target tenant issues an enrollment token for the agent with POST /v1/machine-credentials/enrollment-tokens. The issuer needs the owner role, the saas.api_key.create permission, a verified email, and fresh authentication not older than five minutes. The response returns enrollment_token and expires_at. The token is single-use, expiring, and tenant-bound to the target tenant. The agent must hold this token before it can redeem the token.
Cold-start route sequence
1. POST /v1/machine-credentials/enrollments
The agent redeems the enrollment token with POST /v1/machine-credentials/enrollments on {public_base_url}.
Request:
{
"tenant_id": "<target tenant_id>",
"enrollment_token": "<enrollment token>",
"display_name": "<machine principal name>",
"scopes": [
{
"tenant_id": "<target tenant_id>",
"permission": "mcp:bridge.read"
},
{
"tenant_id": "<target tenant_id>",
"permission": "mcp:bridge.call"
}
],
"expires_at": "<RFC 3339 timestamp>"
}expires_at sets the API-key expiry. It is required. It must be in the future and at most 90 days out. Each scope must be tenant-wide and use a permission from the worker-loop bundle that the endpoint enforces.
HTTP 201 returns service_principal, api_key, and secret. The service_principal identifies the new service principal. The api_key contains the API key id. The secret is shown once. Do not log it.
Redeem-time token refusals return HTTP 401 with these codes and messages:
| Code | Message | Remedy |
|---|---|---|
enrollment_token_expired | machine-credential enrollment token has expired; request a fresh token | The token aged out. Ask the tenant owner to issue a fresh enrollment token. Re-redeeming the old token can never succeed. |
enrollment_token_consumed | machine-credential enrollment token was already redeemed; the credential it issued is already active | The token was already redeemed and the credential it issued is already active. Recover or use that credential rather than re-redeeming. |
enrollment_token_invalid | machine-credential enrollment token is invalid | Request a valid enrollment token for the target tenant. |
When a redeem request field fails validation, the route returns HTTP 400 with the unchanged invalid_request code. In error.message, the text starts with machine-credential enrollment request is invalid and then names the offending field. Exactly one field is named per refusal: the first failing field in this order: tenant_id, enrollment_token, display_name, expires_at, scopes. Correct that field and submit the request again.
11 / executable origination
Headless customer origination
Use this contract when an agent must create a Ground Control customer without a pre-existing tenant or machine credential. Use these three host definitions for every route below:
{base_url} is the environment's public apex origin. It is the host the customer was given and the origin from which the environment discovery document was fetched. For example, it can be https://stg.groundcontrol.so. For the origination sequence below, send registration, verification, public session, review-authority reconciliation, bootstrap, service-principal, and API-key requests to {api_base_url} when the environment advertises clutch_api_base_url; otherwise send them to {base_url}. The optional /workspace-onboarding/state session read also uses {base_url}.
{mcp_base_url} is the origin of the discovery document's resource value. For example, it can be https://ui-<slot>.stg.groundcontrol.so. Here <ui-slot-host> means ui-<slot>.<env>.groundcontrol.so, the origin named by the discovery document. Use {mcp_base_url} only for /v1/mcp, the token endpoint, and the claim handoff. In production, discovery advertises the apex, so {mcp_base_url} equals {base_url}. In staging and development, the slot host answers every non-MCP /v1/ request with HTTP 301 to {base_url}. A client can re-issue that redirect as GET and receive HTTP 405 from {base_url}. Never send those requests to {mcp_base_url}.
{api_base_url} is the API origin for origination requests. When the operator has configured it, the environment discovery document advertises the API origin as clutch_api_base_url in the same authorization-server metadata. Read it with GET {base_url}/.well-known/oauth-authorization-server. This is the RFC 8414 root-prefixed form. Read the same document with GET {issuer}/.well-known/oauth-authorization-server. This is the issuer-suffixed form. For the built-in issuer, {issuer} is {base_url}/v1/mcp/oauth. Replace {api_base_url} with that value. When clutch_api_base_url is absent, use {base_url} for the origination sequence. Keep all values from one environment together. The advertised entry_endpoint values are already on the API origin when configured, so post to them as given.
The route sequence in discovery names five origination routes. This contract also names the required authority and credential steps between those routes. Follow all nine steps in order.
Keep passwords, verification tokens, session tokens, CSRF tokens, and API-key secrets in memory only. Do not place them in logs, prompts, or files.
Transport and header contract
Send Content-Type: application/json on JSON requests. Send one JSON object. The JSON handlers reject unknown fields and trailing JSON values.
Use the following browser-session contract when a step says to use the session contract:
- Send the
__Host-clutch_sessioncookie from the public session response. - Send an explicit allowlisted
Originon every browser-session write. - Send
x-clutch-csrf-tokenwith thecsrf_tokenfrom that session response on every browser-session write. - When a browser-session route validates
X-Clutch-Tenant-ID, send a valid UUID that matches the authenticated tenant. A mismatch fails closed.
For browser-session GET, HEAD, and OPTIONS requests, the handler can derive an origin from Host when Origin is absent. It derives https://<host> for a normal host. It derives http://<host> for localhost, loopback, and IPv6 loopback hosts. Browser-session GET reads require the session and origin checks. The /workspace-onboarding/state GET also requires a matching SaaS CSRF header. Browser-session writes require the session, an explicit allowlisted origin, and a matching SaaS CSRF header.
Public registration, verification consumption, public session creation, and machine-credential bootstrap do not use a gateway bearer. The service-principal and API-key creation routes use the bootstrap API-key secret as the SaaS gateway bearer. Keep the browser session and its CSRF header on those two writes as well.
The MCP first-party authorization handlers use a different CSRF contract. Protected POST continuation, consent approval, registration, and cancel routes require the clutch_mcp_local_continuation cookie, the clutch_mcp_local_csrf cookie, and an X-Clutch-MCP-CSRF header whose value matches the CSRF cookie. The GET consent route needs only the continuation cookie. Local claim completion uses the clutch_mcp_local_claim_token and clutch_mcp_local_claim_csrf cookies. Its body has email, password, and csrf_token fields. The body csrf_token must match the claim CSRF cookie. The handoff request uses an Authorization: Bearer handoff token. Do not use X-Clutch-MCP-CSRF for the SaaS routes in this document. Do not use x-clutch-csrf-token as a substitute on a first-party authorization continuation or consent route.
The OAuth handlers use the browser Origin when they validate a browser session. If Origin is absent, mcpoauth derives an origin from Host. It uses HTTPS for normal hosts and HTTP for localhost, loopback, and IPv6 loopback hosts. The browser-session write resolver keeps an absent Origin empty, so a write still needs an explicit allowlisted Origin.
OAuth token exchange is different from a browser-session write. It uses form data at the token endpoint. It does not use browser CSRF headers. The token endpoint accepts the client secret in the form body or in HTTP Basic authentication.
Error envelopes
Local-auth routes return a JSON body that starts with denied_reason:
{
"denied_reason": "<reason>"
}Known lifecycle denials can also contain gate, observed, and remediation. The unknown reason does not disclose missing field names or account state.
SaaS routes return this error envelope:
{
"error": {
"code": "<code>",
"message": "<message>"
}
}Review-authority onboarding returns an expanded error object with code, message, object_type, object_id, gate, observed, and remediation.
Nine-step walk
1. Public registration
Call the first advertised route:
POST {api_base_url}/v1/auth/local/public/registrationsSend the exact allowlisted Origin when you provide one. If you omit it, the server uses the configured default origin. Do not send Authorization.
The request body has these fields:
{
"email": "<email>",
"display_name": "<display_name>",
"password": "<password>",
"redirect_uri": "<redirect_uri>"
}Send an empty string for redirect_uri when you do not need a redirect.
The first successful registration returns HTTP 201 and this body shape:
{
"user_id": "<user_id>",
"tenant_id": "<tenant_id>",
"email": "<email>",
"display_name": "<display_name>",
"created_at": "<RFC3339 timestamp>",
"verification_id": "<verification_id>",
"verification_expires_at": "<RFC3339 timestamp>",
"workspace_slug": "<workspace_slug>",
"origin": "<origin>",
"verification_email_state": "accepted"
}The verification token is delivered in the verification email sent to the registered address.
A successful public registration always returns verification_email_state as accepted. This is local request acceptance, not provider delivery, and it does not disclose whether an account already exists.
With an Idempotency-Key, registration creates the tenant and its owner membership before verification. Without the key, registration stores a provisional account and defers the tenant and owner membership until verification. The account cannot use local-password login until verification succeeds.
Send a caller-generated Idempotency-Key header on every registration. The key must be a high-entropy random value. Its entropy must make guessing the key infeasible. This handler does not validate key length or entropy. The header is the only key carrier for this route; do not add an idempotency field to the JSON body. Use a new key for a new registration body.
If the response is lost because of a timeout, a gateway 504, or an untyped body, retry the identical body with the same Idempotency-Key. The server returns the original HTTP status and response body. The gateway timeout case is tracked in #12455.
The same key with a different registration body returns HTTP 409 with denied_reason: "idempotency_conflict" in the local-auth error envelope. Do not use a new key for the same body.
Duplicate-class accounts return neutral HTTP 201 data. Treat that response as non-disclosing. Do not use it to decide whether an account exists.
An unknown or expired key is processed as a new request. The caller sees the ordinary response for that request. A duplicate-class request therefore receives the neutral HTTP 201 registration shape. The response does not say whether the key was used before. The server retains a keyed registration outcome for 24 hours from creation. On a keyed lookup, the server reclaims the requested expired row and up to 100 other expired rows. A replay after 24 hours is processed as a new request. If a stored key is absent, use the same unknown-key behavior.
After a successful original outcome, the next step is to request the verification email again when needed:
POST {base_url}/v1/auth/local/public/email-verification-resendsSend the registered email and password. This route keeps its generic response for every account state.
Principal registration denials use the local-auth envelope. The source maps these denials as follows:
| Status | denied_reason |
|---|---|
400 | invalid_email |
400 | invalid_display_name |
400 | weak_password |
400 | public_registration_origin_unavailable |
400 | unknown for a missing, unknown, or wrongly typed field; includes details.field and details.required_fields |
500 | unknown |
Malformed JSON also fails with a local-auth error. Do not retry malformed JSON.
2. Obtain the verification link
Read the verification token from the verification email sent to the registered address.
Do not call the internal email-verification issue route.
A machine principal does not take this path at all. The steps above register a human account, and a person with access to the registered address must complete email verification once.
The path that is served for a machine principal starts from a tenant that already has a verified owner. That owner mints an enrollment token with an authenticated call to POST /v1/machine-credentials/enrollment-tokens, which returns enrollment_token and its expires_at, and hands the token to the agent out of band. The agent redeems it at POST /v1/machine-credentials/enrollments, sending tenant_id, enrollment_token, a display_name, the scopes it needs, and an expires_at. It receives a machine credential. That redemption needs no inbox, no account, and no session.
One human step therefore remains, and it is worth asking for plainly rather than working around: a person verifies their email once and issues the enrollment token. An agent that reaches this point should say "verify your email, then give me an enrollment token" instead of stopping.
3. Consume email verification
Call the second advertised route:
POST {api_base_url}/v1/auth/local/public/email-verification-consumptionsSend the required Origin header with the origin value from the registration response.
Do not send a bearer, session cookie, tenant header, or CSRF header. The body has two fields:
{
"verification_token": "<verification_token>",
"password": "<password>"
}For public provisional registration, including keyed registration, password is a new password chosen by the verifying mailbox owner. It replaces the registration password and need not match it. Invite and MCP verification are token-only only when the address has no pending public registration.
Successful consumption returns HTTP 200:
{
"user_id": "<user_id>",
"tenant_id": "<tenant_id>",
"email": "<email>",
"email_verified_at": "<RFC3339 timestamp>"
}The response can also contain invite_acceptance, session, and next_path. The optional invite_acceptance object has invite_id, membership_id, user_id, tenant_id, role, and accepted_at. The optional session object has tenant_id, session_id, actor_id, role, auth_assurance_level, auth_fresh_at, auth_fresh_until, idle_expires_at, expires_at, csrf_token, redirect_uri, evidence_refs, and workspace_name. The public registration path normally does not need the optional invite session.
Principal verification denials use the local-auth envelope:
| Status | denied_reason |
|---|---|
400 | invalid_token |
400 | password_required — Resend the same token with a password. |
400 | weak_password — Choose 12–256 characters with at least three of a-z, A-Z, 0-9, and any other character. Resend the same token. |
400 | verification_origin_required — Resend the same token with the Origin value from the registration response. |
400 | verification_origin_mismatch — Resend the same token with the Origin value from the registration response. |
400 | unknown for a missing verification_token, an unknown field, or a wrongly typed field — Correct the request fields before retrying. |
400 | invalid_email — Stop and use account recovery. |
404 | unknown_token |
404 | unknown_user — Stop and use account recovery. |
409 | token_expired |
409 | token_consumed |
409 | token_superseded |
409 | duplicate_account — Stop and use account recovery. |
500 | unknown |
Stop the flow when the token is expired, consumed, or superseded. Start a new registration when the token is unknown.
4. Create the public session
Call the third advertised route:
POST {api_base_url}/v1/auth/local/public/sessionsMint this public session on the origin to which steps 8 and 9 post: {api_base_url} when clutch_api_base_url is advertised, or {base_url} otherwise.
Send the allowlisted Origin. Do not send a bearer or a CSRF header for this new-session request. The handler reads tenant_id from the body and does not read X-Clutch-Tenant-ID on this route. Put the registration tenant ID in the body when it is known. A different value in that header is ignored.
The request body has these fields:
{
"tenant_id": "<tenant_id>",
"email": "<email>",
"password": "<password>",
"redirect_uri": "<redirect_uri>"
}Use the registration tenant ID when it is known. Send an empty string for redirect_uri when you do not need a redirect.
tenant_id may be an empty string. The handler then verifies the password first and resolves the tenant from the account's current memberships. It creates a session only when there is exactly one. With an empty tenant_id and a correct password, branch on these outcomes:
| Outcome | Status | Body | Session |
|---|---|---|---|
| Exactly one current membership | 201 | The session body below, with the resolved tenant in tenant_id | Yes |
| More than one current membership | 409 | denied_reason: "disambiguation_required", required_action: "select_membership", and memberships entries with tenant_id, role, tenant_name, and tenant_slug | No |
| Unverified account with at least one current membership, or an unverified pending registration | 409 | denied_reason: "email_not_verified", required_action: "verify_email", and recovery_read_route | No |
| Configured claim flow with an eligible workspace and no membership | 200 | claim_continuation, shown below | No |
| No current membership otherwise | 401 | denied_reason: "invalid_credentials" | No |
On 409 disambiguation_required, retry with one of the returned tenant_id values. A wrong password returns 401 invalid_credentials before any of these checks.
Normal success returns HTTP 201, sets the __Host-clutch_session cookie, and returns:
{
"tenant_id": "<tenant_id>",
"session_id": "<session_id>",
"actor_id": "<actor_id>",
"role": "<role>",
"auth_assurance_level": "<level>",
"auth_fresh_at": "<RFC3339 timestamp>",
"auth_fresh_until": "<RFC3339 timestamp>",
"idle_expires_at": "<RFC3339 timestamp>",
"expires_at": "<RFC3339 timestamp>",
"csrf_token": "<csrf_token>",
"evidence_refs": ["<evidence_ref>"],
"workspace_name": "<workspace_name>"
}redirect_uri appears only when the request asked for one. A session created with an empty or absent redirect_uri omits the field from this body; it does not return an empty string. Do not read the field's presence as a signal about anything other than what the request sent.
Store the cookie and the csrf_token in memory. Use them for browser-session writes. Do not print either value.
A configured claim flow can instead return HTTP 200 with this body:
{
"claim_continuation": {
"path": "<path>",
"handoff_token": "<handoff_token>",
"expires_at": "<RFC3339 timestamp>"
}
}Follow that continuation before using a session-protected route.
Principal session denials use the local-auth envelope:
| Status | denied_reason |
|---|---|
400 | unsafe_redirect or unknown for an invalid request |
401 | invalid_credentials |
409 | email_not_verified: the correct password was supplied for an unverified account. Request the verification email again with the existing verification re-issue route: POST {base_url}/v1/auth/local/public/email-verification-resends, then complete verification before retrying. |
403 | disallowed_origin |
403 | stale_session_authority |
403 | insufficient_session_authority |
500 | unknown |
Stale-authority and insufficient-authority responses can include the authority recovery fields. Do not replace the session cookie with a gateway credential.
5. Reconcile review authority
Reconcile the default review-authority profile before credential creation:
POST {api_base_url}/v1/project-controls/review-authority/onboarding/reconcileSend {} as the body. Use the browser-session contract. Do not send the bootstrap gateway bearer.
Creation returns HTTP 201. Repair and already-ready results return HTTP 200. Created and repaired bodies set reconciled to true. The created or repaired response contains:
{
"code": "<code>",
"tenant_id": "<tenant_id>",
"profile": "<profile>",
"status": "<status>",
"previous_status": "<previous_status>",
"reconciled": true,
"contract_id": "<contract_id>",
"contract_version": "<contract_version>",
"authority_matrix_id": "<authority_matrix_id>",
"authority_matrix_version": "<authority_matrix_version>",
"role_memberships_version": "<role_memberships_version>"
}An already-ready result uses code review_authority_onboarding_already_ready and status review_authority_ready:
{
"code": "review_authority_onboarding_already_ready",
"tenant_id": "<tenant_id>",
"profile": "<profile>",
"status": "review_authority_ready",
"previous_status": "review_authority_ready",
"reconciled": false,
"contract_id": "<contract_id>",
"contract_version": "<contract_version>",
"authority_matrix_id": "<authority_matrix_id>",
"authority_matrix_version": "<authority_matrix_version>",
"role_memberships_version": "<role_memberships_version>"
}reconciled: false with review_authority_onboarding_already_ready means no further call is needed; the tenant is already in the ready state.
Principal reconciliation denials use the review-authority error object. The source defines these primary codes:
| Status | error.code | Action |
|---|---|---|
403 | review_authority_onboarding_owner_required | Sign in as the tenant owner. |
409 | review_authority_onboarding_no_eligible_reviewer | Inspect or update role membership. |
409 | review_authority_onboarding_conflict | Inspect the review-authority artifacts. |
503 | review_authority_onboarding_unavailable | Retry the reconcile request. |
Advanced: supplying an explicit membership
Use this optional path when you want to supply an explicit membership for the bootstrap request. It is not needed to obtain service_principal.owner_membership_id; the step-6 201 response returns that value even when this path is omitted. When this optional path is omitted, use the service_principal.owner_membership_id returned by step 6 as the optional step-8 hint and the optional step-9 field.
Read the current workspace contexts:
GET {base_url}/workspace-onboarding/stateUse the browser-session contract. Send x-clutch-csrf-token with the csrf_token from the session response. This GET requires a valid session, origin, and SaaS CSRF token.
The HTTP 200 response contains verified, current_contexts, continuation, allowed_next_action, safe_reason, latest_attempt_id, and evidence_refs. Each current_contexts entry contains:
{
"tenant_id": "<tenant_id>",
"membership_id": "<membership_id>",
"role": "<role>",
"tenant_name": "<tenant_name>",
"tenant_slug": "<tenant_slug>",
"context_ref": "<context_ref>",
"review_authority": {
"profile": "<profile>",
"status": "<status>",
"previous_status": "<previous_status>",
"reconciled": true,
"contract_id": "<contract_id>",
"contract_version": "<contract_version>",
"authority_matrix_id": "<authority_matrix_id>",
"authority_matrix_version": "<authority_matrix_version>",
"role_memberships_version": "<role_memberships_version>"
}
}Select the entry whose tenant_id equals the registration tenant ID. Do not select a membership from a different tenant. Do not invent a membership ID when the matching context is absent. The selected membership_id is supplied as owner_membership_id in the step-6 bootstrap request body. When this optional path is omitted, use the service_principal.owner_membership_id returned by step 6 as the optional step-8 hint and the optional step-9 field.
6. Bootstrap a machine credential
Create the first machine credential with the browser session:
POST {api_base_url}/v1/machine-credentials/bootstrapSend Idempotency-Key. After trimming leading and trailing whitespace, its length must be between 16 and 256 bytes. Use a new stable value for this bootstrap attempt. Do not send a gateway bearer.
The request body has these fields:
{
"tenant_id": "<tenant_id>",
"display_name": "<bootstrap principal name>",
"scopes": [
{
"tenant_id": "<tenant_id>",
"permission": "mcp:bridge.read",
"object_type": "",
"object_id": ""
},
{
"tenant_id": "<tenant_id>",
"permission": "mcp:bridge.call",
"object_type": "",
"object_id": ""
}
],
"expires_at": "<RFC3339 timestamp within 90 days>",
"actor_ref": "actor:<actor_id>",
"now": "<RFC3339 timestamp or empty string>"
}owner_membership_id is optional and omitted by default. The server derives the founding owner's membership from the session actor. If supplied, it must match the caller's own membership.
The server binds owner_membership_id and actor_ref to the current actor authority. The handler clears now, and the service resolves the time from its server clock. Send matching membership and actor hints when you have them. The now value does not set the server time. The bootstrap scope list must be non-empty, tenant-wide, unique by permission, and contain no more than eight entries. The allowed permissions are:
mcp:bridge.readmcp:bridge.callworker_facade.claimgoverned_execution.readgoverned_execution.write
Every bootstrap scope must use the tenant ID, an empty object_type, and an empty or zero object_id. Use mcp:bridge.read for a first MCP read. Add mcp:bridge.call when the credential must call MCP tools.
Successful bootstrap returns HTTP 201:
{
"service_principal": {
"tenant_id": "<tenant_id>",
"service_principal_id": "<service_principal_id>",
"display_name": "<display_name>",
"owner_membership_id": "<owner_membership_id>",
"state": "<state>",
"created_at": "<RFC3339 timestamp>",
"updated_at": "<RFC3339 timestamp>",
"latest_audit_evidence_id": "<evidence_id>"
},
"api_key": {
"tenant_id": "<tenant_id>",
"api_key_id": "<bootstrap_api_key_id>",
"service_principal_id": "<service_principal_id>",
"owner_membership_id": "<owner_membership_id>",
"key_prefix": "<key_prefix>",
"scopes": [
{
"tenant_id": "<tenant_id>",
"permission": "mcp:bridge.read",
"object_type": "",
"object_id": ""
},
{
"tenant_id": "<tenant_id>",
"permission": "mcp:bridge.call",
"object_type": "",
"object_id": ""
}
],
"expires_at": "<RFC3339 timestamp>",
"created_at": "<RFC3339 timestamp>",
"latest_audit_evidence": "<evidence_id>"
},
"secret": "<bootstrap_secret>"
}Take service_principal.owner_membership_id from this 201 response and keep it as <owner_membership_id> for steps 8 and 9. Step 8 accepts it as an optional owner_membership_id hint and, when omitted, the server derives it from the authenticated session and calling credential's membership. Step 9 treats it as optional; when supplied, it must match the authenticated owner. The server rejects a mismatch. The optional workspace lookup is not needed to obtain it.
The API-key object can also contain revoked_at, revoked_reason, and last_used_at. The secret is returned once. Store it as the bootstrap gateway credential. Do not log it.
Principal bootstrap errors use the error envelope. The main codes are invalid_request (400), unauthenticated (401), tenant_scope_mismatch (403), actor_authority_mismatch (403), bootstrap_scope_not_allowed (403), normal_policy_required (403), bootstrap_replayed (409), bootstrap_rate_limited (429), actor_not_authorized (403), audit_evidence_unavailable (503), and internal_error (500). A second bootstrap attempt is not a way to mint a second first credential.
7. Exchange the one-time secret for an MCP bearer token
Keep two credential uses separate:
- Use
<bootstrap_secret>as the SaaS gateway bearer for steps 8 and 9. - Exchange the bootstrap API-key ID and secret for an OAuth access token for the MCP resource.
The OAuth issuer is {mcp_base_url}/v1/mcp/oauth unless the environment discovery document advertises another issuer. Post form data to its token endpoint:
POST {issuer}/token
Content-Type: application/x-www-form-urlencodedSend:
grant_type=client_credentials
client_id=<bootstrap_api_key_id>
client_secret=<bootstrap_secret>
resource={mcp_base_url}/v1/mcp
scope=mcp:bridge.read mcp:bridge.callThe response returns access_token, token_type, expires_in, and the granted scope. The client-credentials path does not return refresh_token. The access_token is the MCP bearer. Do not send it as the SaaS gateway bearer.
If bootstrap changes the current browser authority, create a new public session with step 4. Replace both the __Host-clutch_session cookie and the csrf_token before the next browser-session write. A stale browser session must not be used for steps 8 or 9.
8. Create a service principal
Use the bootstrap secret as the gateway bearer and keep the current browser session, allowlisted Origin, matching x-clutch-csrf-token, and optional matching X-Clutch-Tenant-ID:
POST {api_base_url}/v1/service-principals
Authorization: Bearer <bootstrap_secret>The request body has these fields. owner_membership_id is an optional hint; when supplied, use the UUID from service_principal.owner_membership_id in the step-6 201 response to state the intended membership explicitly. When omitted, the server derives it from the authenticated session and calling credential's membership. Do not invent a membership ID.
{
"tenant_id": "<tenant_id>",
"display_name": "<final principal name>",
"owner_membership_id": "<owner_membership_id>",
"actor_ref": "actor:<actor_id>",
"now": "<RFC3339 timestamp or empty string>"
}A successful response returns HTTP 201:
{
"service_principal": {
"tenant_id": "<tenant_id>",
"service_principal_id": "<final_service_principal_id>",
"display_name": "<display_name>",
"owner_membership_id": "<owner_membership_id>",
"state": "<state>",
"created_at": "<RFC3339 timestamp>",
"updated_at": "<RFC3339 timestamp>",
"latest_audit_evidence_id": "<evidence_id>"
}
}Take service_principal.service_principal_id from this response and keep it as <final_service_principal_id> for the step-9 request body.
Principal errors use the error envelope. The main codes are unauthenticated (401), tenant_scope_mismatch (403), actor_authority_mismatch (403), invalid_request (400), not_found (404), actor_not_authorized (403), audit_evidence_unavailable (503), and internal_error (500).
9. Create an API key, exchange it, and perform the first MCP read
Use the same bootstrap gateway bearer and browser-session headers:
POST {api_base_url}/v1/api-keys
Authorization: Bearer <bootstrap_secret>The request body has these fields. owner_membership_id is optional; when supplied it must match the authenticated owner. Use the UUID from service_principal.owner_membership_id in the step-6 201 response. The server rejects a mismatch; do not invent a membership ID.
{
"tenant_id": "<tenant_id>",
"service_principal_id": "<final_service_principal_id>",
"owner_membership_id": "<owner_membership_id>",
"scopes": [
{
"tenant_id": "<tenant_id>",
"permission": "mcp:bridge.read",
"object_type": "",
"object_id": ""
},
{
"tenant_id": "<tenant_id>",
"permission": "mcp:bridge.call",
"object_type": "",
"object_id": ""
}
],
"expires_at": "<RFC3339 timestamp within 365 days>",
"actor_ref": "actor:<actor_id>",
"now": "<RFC3339 timestamp or empty string>"
}The API-key scope contract is:
- Provide at least one scope and no more than eight scopes.
- Set every scope
tenant_idto the request tenant ID. - Set every
permissionto a non-empty value of at most 64 bytes. - Set
object_typeto at most 64 bytes. - Use an empty
object_typeand an empty or zeroobject_idfor a tenant-wide MCP scope. - Set
expires_atafter the current time and no more than 365 days ahead. - Do not use
saas.api_key.createorsaas.machine_credential.manageas an API-key policy scope on this route.
Use mcp:bridge.read for the first entitled MCP read. Add mcp:bridge.call for MCP tool calls. The OAuth scope requested later must be within the scopes on this API key.
Successful creation returns HTTP 201:
{
"api_key": {
"tenant_id": "<tenant_id>",
"api_key_id": "<final_api_key_id>",
"service_principal_id": "<final_service_principal_id>",
"owner_membership_id": "<owner_membership_id>",
"key_prefix": "<key_prefix>",
"scopes": [
{
"tenant_id": "<tenant_id>",
"permission": "mcp:bridge.read",
"object_type": "",
"object_id": ""
},
{
"tenant_id": "<tenant_id>",
"permission": "mcp:bridge.call",
"object_type": "",
"object_id": ""
}
],
"expires_at": "<RFC3339 timestamp>",
"created_at": "<RFC3339 timestamp>",
"latest_audit_evidence": "<evidence_id>"
},
"secret": "<final_secret>"
}The API-key object can also contain revoked_at, revoked_reason, and last_used_at. The secret is returned once. Use it only with the OAuth client-credentials exchange for this final API key. Keep the bootstrap secret and final secret separate.
Principal errors use the error envelope. The main codes are unauthenticated (401), tenant_scope_mismatch (403), actor_authority_mismatch (403), policy_scope_not_allowed (403), invalid_request (400), not_found (404), actor_not_authorized (403), audit_evidence_unavailable (503), and internal_error (500).
Exchange the final API key for the final MCP bearer. Use the final API-key ID and final secret at the same {issuer}/token endpoint:
grant_type=client_credentials
client_id=<final_api_key_id>
client_secret=<final_secret>
resource={mcp_base_url}/v1/mcp
scope=mcp:bridge.read mcp:bridge.callUse the returned access_token in the MCP Authorization header. Do not use the API-key secret at /v1/mcp.
Initialize the MCP connection with a supported protocol version. Then send the first read as a JSON-RPC tools/list request:
POST {mcp_base_url}/v1/mcp?profile=compact
Authorization: Bearer <final_mcp_access_token>
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}Use the normal MCP initialize request and notifications/initialized notification before tools/list when the client requires the handshake. Request mcp:bridge.call in addition to mcp:bridge.read before calling a tool.
The MCP operation is POST-only. GET /v1/mcp sets Allow: POST. With OAuth enabled, it returns HTTP 401 with an authentication challenge when no tenant route is resolved. It returns HTTP 405 when OAuth is disabled or a tenant route is found.
12 / tool authorization
Tool required scopes
The bridge shapes each caller's catalog profile from the caller's principal kind and the live route declarations. The served list is further narrowed by the credential's granted action scopes. The required scopes differ by principal kind. The generated table below shows the scopes each published tool needs for each principal kind.
The table is generated from the served contract. Its columns cover OAuth token, OAuth service principal, gateway credential, and provisional workspace. not available means that principal kind does not advertise the tool; object-bound means the scope must name the claimed worker instance; and none means that credential shape needs no additional scope. A tool absent for the caller's principal kind will not appear in that caller's catalog. The table uses this build's committed schema-reference artifact, so a tool name with no row is not declared by this deployment's contract for any principal kind: it is absent, not withheld, and this artifact can be stale relative to a newer served contract. The not available value shows withholding by principal kind, while narrowing by a credential's granted action scopes is per credential and is not a table value. After a worker claims a slot, its gateway credential is served exactly the 20 tools whose Gateway credential cell is marked (object-bound), and tools/list for that credential lists only those 20; a tools/call naming any other tool from that credential is refused as policy_denied. The four columns are the catalog principal kinds browser_session, service_principal, gateway_credential and provisional_workspace. The provisional_workspace column is a provisional MCP workspace credential held before the claim; its catalog includes clutch_start_local_workspace_claim, which is not available in every other column and starts the /mcp/oauth/claim journey described in the first-party browser journey. The fifth catalog principal kind, claimed_worker_gateway (a gateway key acting under a worker-assignment binding), has no column: a refusal names it as a claimed worker gateway in required_action and as claimed_worker_gateway in details.principal_kind, and this table does not show that kind's catalog.
| Tool | OAuth token | OAuth service principal | Gateway credential | Provisional workspace |
|---|---|---|---|---|
clutch_accept_package_candidate | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_accept_work_order | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_accept_workflow_agent_binding | mcp:bridge.read + mcp:bridge.call | not available | none | not available |
clutch_add_dependency_edge | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_add_wbs_node | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_apply_authorized_closeout | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write | not available |
clutch_apply_workflow_draft_package_bindings | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.workflow.write | project_controls.workflow.write | not available |
clutch_archive_package_candidate | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_archive_wbs_node | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_attach_evidence_ref | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write or worker_facade.write (object-bound) | not available |
clutch_bind_workflow_agent_node | mcp:bridge.read + mcp:bridge.call | not available | none | not available |
clutch_browse_operations | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_cancel_execution_run | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write | not available |
clutch_claim_package_review_request | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + worker_facade.claim | worker_facade.claim | not available |
clutch_claim_worker_slot | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + worker_facade.claim | worker_facade.claim | not available |
clutch_compile_work_order | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_compile_workflow_draft | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.workflow.write | project_controls.workflow.write | not available |
clutch_complete_execution_run | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write | not available |
clutch_complete_workflow_publish | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.workflow.publish | project_controls.workflow.publish | not available |
clutch_create_active_work_package | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.package.promote | project_controls.package.promote | not available |
clutch_create_external_binding_ref | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write or worker_facade.write (object-bound) | not available |
clutch_create_initiative | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | mcp:bridge.read + mcp:bridge.call |
clutch_create_machine_credential | mcp:bridge.read + mcp:bridge.call (Owner OAuth) | not available | not available | not available |
clutch_create_package_revision | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.package.write | project_controls.package.write | not available |
clutch_create_workflow_agent_binding | mcp:bridge.read + mcp:bridge.call | not available | none | not available |
clutch_create_workflow_agent_binding_node | mcp:bridge.read + mcp:bridge.call | not available | none | not available |
clutch_create_workflow_deployment | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.workflow.write | project_controls.workflow.write | not available |
clutch_create_workflow_draft | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.workflow.write | project_controls.workflow.write | not available |
clutch_describe_operation | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_fail_execution_run | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write | not available |
clutch_finish_worker_instance | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + (worker_facade.write or governed_execution.write (legacy compatibility)) | worker_facade.write (object-bound) or governed_execution.write (legacy compatibility) | not available |
clutch_get_completion_receipt | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read or worker_facade.read (object-bound) | not available |
clutch_get_evidence_summary | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read or worker_facade.read (object-bound) | not available |
clutch_get_execution_run | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read | not available |
clutch_get_initiative | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | mcp:bridge.read + mcp:bridge.call |
clutch_get_owner_worker_brief | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read | not available |
clutch_get_owner_worker_status | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read | not available |
clutch_get_package_review_request | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_get_package_review_worker_brief | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read | not available |
clutch_get_package_type_workflow_binding | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_get_prompt | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_get_skill | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_get_skill_revision_content | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_get_tenant_agent | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_get_timeline | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read or worker_facade.read (object-bound) | not available |
clutch_get_work_menu | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | project_controls.work_menu.read | not available |
clutch_get_work_package | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_get_worker_brief | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read or worker_facade.read (object-bound) | not available |
clutch_get_worker_status | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read or worker_facade.read (object-bound) | not available |
clutch_get_workflow_agent_binding | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_get_workflow_definition_revision | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_get_workflow_draft | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_get_workflow_draft_package_bindings | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_heartbeat_package_review | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write | not available |
clutch_invoke_operation | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_issue_control_baseline | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.package.promote | project_controls.package.promote | not available |
clutch_launch_package | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write | not available |
clutch_library_get_original_content | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_library_retrieve | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_list_initiatives | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | mcp:bridge.read + mcp:bridge.call |
clutch_list_machine_credentials | mcp:bridge.read + mcp:bridge.call (Owner OAuth) | not available | not available | not available |
clutch_list_open_worker_slots | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read or worker_facade.claim | not available |
clutch_list_package_review_requests | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_list_package_review_worker_requests | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read | not available |
clutch_list_package_types | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_list_prompts | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_list_skills | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_list_tenant_agents | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_list_work_packages | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_list_workflow_deployments | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_list_workflow_node_executions | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read | not available |
clutch_list_workflows | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_materialize_package_candidate | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_materialize_work_breakdown_definition | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_materialize_work_order | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_mint_review_permit | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.approval.decide | project_controls.approval.decide | not available |
clutch_move_wbs_node | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_open_review_hold | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.hold.open | project_controls.hold.open | not available |
clutch_open_work_package | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.package.write | project_controls.package.write | not available |
clutch_place_wbs_node_package | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_preview_package_launch | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read | not available |
clutch_preview_package_revision_review_subject | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_preview_workflow_agent_binding_effective | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_preview_workflow_publish | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_promote_package_revision | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.package.promote | project_controls.package.promote | not available |
clutch_promote_wbs_node | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_publish_workflow_definition | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.workflow.publish | project_controls.workflow.publish | not available |
clutch_read_authoring_catalog | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_read_current_controls | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read or worker_facade.read (object-bound) | not available |
clutch_read_owner_worker_controls | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | governed_execution.read | not available |
clutch_reconnect_worker_instance | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write or worker_facade.write (object-bound) | not available |
clutch_record_checkpoint | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + (worker_facade.write or governed_execution.write (legacy compatibility)) | worker_facade.write (object-bound) or governed_execution.write (legacy compatibility) | not available |
clutch_record_handoff | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write or worker_facade.write (object-bound) | not available |
clutch_record_package_review_decision | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.package_review.decide | project_controls.package_review.decide | not available |
clutch_record_package_review_outcome | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write | not available |
clutch_record_source_materialization | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write or worker_facade.write (object-bound) | not available |
clutch_recover_machine_credential | mcp:bridge.read + mcp:bridge.call (Owner OAuth) | not available | not available | not available |
clutch_register_reviewable_artifact | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.package.write | project_controls.package.write | not available |
clutch_release_worker_slot | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + (worker_facade.write or governed_execution.write (legacy compatibility)) | worker_facade.write (object-bound) or governed_execution.write (legacy compatibility) | not available |
clutch_remove_dependency_edge | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_remove_wbs_node_package_placement | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_report_blocked | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write or worker_facade.write (object-bound) | not available |
clutch_report_failed | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + (worker_facade.write or governed_execution.write (legacy compatibility)) | worker_facade.write (object-bound) or governed_execution.write (legacy compatibility) | not available |
clutch_report_progress | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + (worker_facade.write or governed_execution.write (legacy compatibility)) | worker_facade.write (object-bound) or governed_execution.write (legacy compatibility) | not available |
clutch_request_hold | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write or worker_facade.write (object-bound) | not available |
clutch_request_package_closeout | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write | not available |
clutch_request_run_closeout | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write | not available |
clutch_request_workflow_ai_proposal | not available | not available | not available | not available |
clutch_resolve_gate_execution | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write | not available |
clutch_revoke_machine_credential | mcp:bridge.read + mcp:bridge.call (Owner OAuth) | not available | not available | not available |
clutch_search_operations | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | none | not available |
clutch_send_heartbeat | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + (worker_facade.write or governed_execution.write (legacy compatibility)) | worker_facade.write (object-bound) or governed_execution.write (legacy compatibility) | not available |
clutch_set_package_type_workflow_binding | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.package.write | project_controls.package.write | not available |
clutch_set_workflow_node_skills | mcp:bridge.read + mcp:bridge.call | not available | none | not available |
clutch_start | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read | not available |
clutch_start_local_workspace_claim | not available | not available | not available | mcp:bridge.read + mcp:bridge.call |
clutch_submit_package_revision | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.package.write | project_controls.package.write | not available |
clutch_submit_package_revision_for_review | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.package.write | project_controls.package.write | not available |
clutch_update_wbs_node | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.initiative.write | project_controls.initiative.write | not available |
clutch_update_workflow_draft | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + project_controls.workflow.write | project_controls.workflow.write | not available |
clutch_validate_workspace_binding | mcp:bridge.read + mcp:bridge.call | mcp:bridge.read + mcp:bridge.call + governed_execution.write | governed_execution.write or worker_facade.write (object-bound) | not available |
clutch_withdraw_package_review_request | mcp:bridge.read + mcp:bridge.call | not available | none | not available |
13 / error codes
Structured error codes
The bridge serves structured ServerError envelopes. This page names the recovery for codes that a caller can follow from the advertised tool schemas.
| Code | Recovery |
|---|---|
launch_spec_mismatch | Call clutch_preview_package_launch again and retry clutch_launch_package with workflow_definition_revision_id from the preview binding and package_launch_permit_ref from the original request. |
Source-backed MCP bridge reference