Agent integration
This page is the contract for agents (and agent developers) integrating with a capsol registry without a human walking them through it. Everything here is discoverable from the registry root.
Discovery: /.well-known/capsol-agent.json
Section titled “Discovery: /.well-known/capsol-agent.json”GET /.well-known/capsol-agent.json (unauthenticated) describes the whole integration surface:
| Field | Meaning |
|---|---|
name, version | Always "capsol" plus the registry version |
mcp.url | URL template — https://host/mcp/:capsuleId |
mcp.auth.type | "oauth_required" — there is no anonymous MCP access |
mcp.auth.oauth.protected_resource | RFC 9728 protected-resource metadata URL |
mcp.auth.oauth.protected_resource_template | Per-capsule variant with :capsuleId |
mcp.auth.oauth.authorization_server | RFC 8414 authorization-server metadata URL |
mcp.auth.oauth.registration_endpoint | Dynamic Client Registration (POST) |
mcp.auth.oauth.dcr / cimd | Whether DCR / client-id-metadata-documents are enabled |
mcp.auth.oauth.redirect_policy | Allowed redirect hosts / native schemes (empty lists = any safe redirect) |
mcp.auth.oauth.pkce | Always true; only S256 is accepted |
mcp.auth.token_in_url | Always false — tokens in URLs are rejected with 410 |
enrollment.endpoint | POST /v1/agent-enrollments |
enrollment.approval | Current approval mode: human, supervisor, or policy |
enrollment.idempotency_key | What makes a repeated enrollment idempotent |
enrollment.flow | Step-by-step instructions (also reproduced below) |
grants.* | Where grant requests are listed and decided |
cli.* | Copy-paste CLI equivalents |
docs.llms / docs.full | The llms.txt contract |
scopes | All grantable scopes |
Two auth paths exist. OAuth (DCR → authorize → PKCE token) suits clients with a browser-capable user. Enrollment suits headless autonomous agents and is specified next.
Enrollment state machine
Section titled “Enrollment state machine”An enrollment is a request: “client X wants role R on capsule C”. Create one with no credentials at all:
curl -X POST https://host/v1/agent-enrollments \ -H "Content-Type: application/json" \ -d '{ "client_id": "my-agent-stable-id", "capsule_id": "<capsule-uuid>", "agent_label": "Build agent", "requested_role": "writer", "human_email": "owner@example.com" }'States
Section titled “States” create │ ▼ ┌────────┐ operator/supervisor/policy approves ┌──────────┐ │pending │ ───────────────────────────────────────▶│ approved │ (terminal) └────────┘ └──────────┘ │ reject token is now an ▼ MCP credential ┌────────┐ │rejected│ (terminal) └────────┘ │ 30 minutes elapse while pending ▼ ┌────────┐ │expired │ (terminal — create a new enrollment) └────────┘- The create response returns
enrollment_tokenonce. Save it. It is the polling credential now and the MCP credential after approval. repeated: true(HTTP 200 instead of 201) means an identical pending enrollment already exists — sameclient_id+capsule_id+ requested role/content scope. The original token stays valid; the repeated response does not re-issue it.- Pending enrollments expire after 30 minutes. Terminal states never transition.
- Under
approval: "policy"the create response can come back alreadyapprovedwithconnection_idandmcp_urlinline.
Polling
Section titled “Polling”curl https://host/v1/agent-enrollments/<enrollment_id> \ -H "Authorization: Bearer <enrollment_token>"- Poll with the token; unauthenticated or wrong-token polls get 401.
- Sensible cadence: every 5–15 s for the first minutes, then back off. The lookup is rate-limited to 10/min per IP — a 429 with
Retry-Aftertells you to slow down. status: "pending"responses never include connection details. After approval the same poll returnscapsule_id,mcp_url, andconnection_id.- Once approved, send the enrollment token as the MCP bearer:
Authorization: Bearer <enrollment_token>againstmcp_url.
Error codes and recovery
Section titled “Error codes and recovery”Every failed REST/MCP-transport request carries { error, error_code, recovery }. The agent-relevant ones:
error_code | You should |
|---|---|
invalid_token | Re-run discovery and OAuth/DCR, or create a fresh enrollment. Do not retry the same credential. |
token_expired | POST /oauth/token with grant_type=refresh_token and your saved refresh token; fall back to re-authorization. |
token_revoked | Stop. Access was withdrawn. File a new access request and wait for approval. |
grant_revoked | Same as token_revoked, but explicitly an operator decision on the grant. |
connection_paused | Back off and retry later; credentials stay valid. Tell your human. |
unknown_capsule | The capsule id in your URL is wrong or gone. Re-check with your operator. |
rate_limited | Wait the Retry-After seconds, then resume. |
payload_too_large | The body has limit_bytes and actual_bytes. Split content or use the upload endpoint. |
invalid_uri | Use scheme://path (example: docs://readme). No .., no absolute paths. |
Inside MCP tool results, errors come as status: "error" payloads with code and a hint — notably version_conflict (re-read, retry with the current version), item_exists, missing_if_version, string_not_found, and insufficient_scope.
The llms.txt contract
Section titled “The llms.txt contract”GET /llms.txt— one screen of plain text: URL shape, “credentials never in URLs”, the four tools, and where enrollment lives. Stable, cacheable, safe to inline into a prompt.GET /llms-full.txt— the full agent guide: OAuth discovery steps in order, enrollment instructions, tool semantics, artifact views, and signal audience filters.
Both are unauthenticated and intended to be fetched by agents at connect time. The contract: anything stated there matches the running registry version, and breaking changes to it are versioned with the registry (URL shapes are stable forever — Invariant G3 of the project).
Hosted-connection limits worth knowing
Section titled “Hosted-connection limits worth knowing”capsol_read(output_path=…)andcapsol_write(file_path=…)are disabled for hosted MCP connections — content travels in the tool response, not via server-side file paths.- Replacing or patching an existing item over a hosted connection requires
if_version(read first, then write). - Signals are capped at 140 characters and delivered via per-connection unread cursors on your next tool call.