{"protocol":"clank-doc/1","frameworkVersion":"0.9.0","slug":"agent-protocol","title":"Agent protocol","description":"Every Clank backend is an MCP server. Define normal typed queries and mutations, open the backend, and an agent can discover and invoke the same server actions without a second API layer, generated OpenAPI file, SDK, or adapter process.","group":{"id":"agents","title":"Agents and generation"},"url":"https://docs.clank.run/docs/agent-protocol","source":"docs/agent-protocol.md","headings":["Why MCP","Clank documentation MCP","Define agent ready actions","Application identity","Discovery","Authentication","Scopes","Transport behavior","Security properties"],"tableOfContents":[{"id":"why-mcp","title":"Why MCP","level":2},{"id":"clank-documentation-mcp","title":"Clank documentation MCP","level":2},{"id":"define-agent-ready-actions","title":"Define agent-ready actions","level":2},{"id":"application-identity","title":"Application identity","level":2},{"id":"discovery","title":"Discovery","level":2},{"id":"authentication","title":"Authentication","level":2},{"id":"scopes","title":"Scopes","level":2},{"id":"transport-behavior","title":"Transport behavior","level":2},{"id":"security-properties","title":"Security properties","level":2}],"markdown":"# Agent protocol\n\nEvery Clank backend is an MCP server. Define normal typed queries and mutations, open the backend,\nand an agent can discover and invoke the same server actions without a second API layer, generated\nOpenAPI file, SDK, or adapter process.\n\nThe MCP endpoint is:\n\n```text\nhttps://your-app.example.com/__clank/mcp\n```\n\nGive that URL to any remote MCP client that supports Streamable HTTP and OAuth. Clank implements\nthe stable `2025-11-25` protocol revision and accepts compatible `2025-06-18` and `2025-03-26`\nclients.\n\n## Why MCP\n\nMCP tools are the closest match for application actions: each tool has a programmatic name,\nhuman description, JSON Schema input and output, authorization scope, and side-effect annotations.\nMCP also specifies Streamable HTTP and how a protected server discovers its OAuth authorization\nserver.\n\nAgent2Agent (A2A) solves a different problem. It lets autonomous agents exchange messages and\nlong-running tasks through Agent Cards. A Clank application may add A2A later when the application\nitself behaves as an autonomous agent, but A2A is not a replacement for its typed query and\nmutation API.\n\n## Clank documentation MCP\n\nThe canonical documentation site is itself an MCP server:\n\n```text\nhttps://docs.clank.run/__clank/mcp\n```\n\nIt exposes three public, read-only tools:\n\n- `docs.list` lists the canonical guide catalog, optionally by documentation group;\n- `docs.search` searches guide titles, headings, and text with a bounded result count;\n- `docs.read` returns one canonical guide as Markdown with its navigation metadata.\n\nStart at `https://docs.clank.run/.well-known/clank` when a client supports discovery. Documentation\nis public, so this particular server does not require OAuth. Its tools are annotated read-only,\nnon-destructive, idempotent, and closed-world. Authenticated application backends still use the\nOAuth flow described below.\n\n## Define agent-ready actions\n\nBackend functions are exposed by default:\n\n```ts\nexport const backend = defineBackend({ schema, auth }).functions(\n  ({ query, mutation }) => ({\n    todos: {\n      list: query({\n        description: \"List the signed-in user's todos.\",\n        args: {},\n        handler: ({ db }) => db.table(\"todos\").collect(),\n      }),\n\n      add: mutation({\n        description: \"Create a todo for the signed-in user.\",\n        args: {\n          title: s.string({\n            min: 1,\n            max: 160,\n            description: \"Todo title\",\n          }),\n        },\n        agent: { destructive: false },\n        handler: ({ db }, { title }) =>\n          db.table(\"todos\").insert({ title, done: false }),\n      }),\n\n      remove: mutation({\n        description: \"Permanently remove one todo.\",\n        args: {\n          id: s.id(\"todos\"),\n          version: s.number({ integer: true, min: 1 }),\n        },\n        agent: { destructive: true },\n        handler: ({ db }, { id, version }) =>\n          db.table(\"todos\").delete(id, { ifVersion: version }),\n      }),\n    },\n  }),\n);\n```\n\nClank derives MCP tool names from function paths: `todos.list`, `todos.add`, and `todos.remove`.\nThe normal runtime schemas become JSON Schema 2020-12 tool contracts. Query results and mutation\nresults include the committed Clank database revision.\n\nThe optional `agent` contract supports:\n\n- `title`: human-readable tool name;\n- `description`: agent-specific description override;\n- `destructive`: whether a mutation can remove or irreversibly change data;\n- `idempotent`: whether repeating the exact call has no additional effect;\n- `openWorld`: whether the action can communicate outside this application;\n- `enabled: false`, or `agent: false`, to omit an internal function.\n\nQueries are always marked read-only and idempotent. Mutations default to destructive as a\nconservative safety hint; mark additive or reversible writes with `destructive: false`.\nAnnotations help the MCP client decide when to ask for confirmation, but server authorization\nnever trusts an annotation.\n\n## Application identity\n\nCustomize the generated server card and OAuth consent screen when opening the backend:\n\n```ts\nconst runtime = await openBackend(backend, {\n  path: databasePath,\n  agent: {\n    name: \"private-todo\",\n    title: \"Private Todo\",\n    version: \"1.0.0\",\n    description: \"Manage private todos.\",\n    instructions: \"Read the current revision before changing an existing todo.\",\n  },\n});\n```\n\nSet `agent: false` on `openBackend()` only when the entire application must not expose an agent\nprotocol. Endpoint paths can be changed with `mcpPath` and `oauthPrefix`.\n\n## Discovery\n\nClank publishes only non-sensitive connection metadata before authentication:\n\n| URL | Purpose |\n|---|---|\n| `/.well-known/clank` | Stable Clank discovery document with the absolute MCP URL |\n| `/.well-known/mcp/server-card.json` | Forward-compatible MCP Server Card; tools stay dynamic |\n| `/.well-known/oauth-protected-resource/__clank/mcp` | RFC 9728 protected-resource metadata |\n| `/.well-known/oauth-authorization-server` | RFC 8414 authorization-server metadata |\n\nDetailed action descriptions and schemas are returned after connection through `tools/list`, or\nas the authenticated `clank://actions` MCP resource. The public documents do not contain action\narguments, user data, credentials, private routes, or implementation paths.\n\nThe Server Card endpoint follows the current MCP Server Card proposal. Stable clients can ignore\nit and connect directly to `/__clank/mcp`.\n\n## Authentication\n\nWhen `defineBackend()` receives `auth`, Clank automatically protects MCP with an OAuth 2.1-style\nauthorization-code flow:\n\n1. The MCP endpoint returns `401` with an RFC 9728 `resource_metadata` challenge.\n2. The client discovers Clank's authorization and token endpoints.\n3. Unknown public clients register through RFC 7591 dynamic client registration.\n4. The client starts authorization with PKCE `S256` and the exact MCP resource indicator.\n5. The user signs into the application in the browser and approves the displayed scopes.\n6. Clank returns a short-lived, resource-bound bearer token and a rotating refresh token.\n7. The client sends the bearer token in the `Authorization` header on every MCP request.\n\nIf the authorization page opens while signed out, open the application from that page, sign in\nnormally, return to the authorization tab, and continue. The agent never receives the password,\nbrowser cookie, CSRF token, or application session.\n\nOAuth access tokens work only at the exact MCP resource that issued them. They do not authenticate\nordinary `__clank/query`, `__clank/mutation`, browser-auth, or another domain's endpoints.\n\n## Scopes\n\nClank uses two deliberately small scopes:\n\n- `agent:read`: initialize MCP, list and read tool documentation, and call queries;\n- `agent:write`: call mutations. A write grant also includes `agent:read`.\n\nA read-only token does not merely fail mutation calls: mutation tools are omitted from\n`tools/list` and the action manifest. An attempted mutation still receives `403\ninsufficient_scope`.\n\nApplications keep their normal authorization rules. Required authentication, verified-email\nrequirements, roles, owned-table isolation, validation, optimistic concurrency, and transaction\nboundaries apply identically to browser and MCP calls.\n\n## Transport behavior\n\nClank uses stateless JSON responses over MCP Streamable HTTP. It supports `initialize`, `ping`,\n`tools/list`, `tools/call`, `resources/list`, `resources/read`, and standard notifications. It\ndoes not open a server-initiated SSE stream, so `GET` on the MCP endpoint returns `405` as permitted\nby the protocol.\n\nTool results include both MCP text content and structured content:\n\n```json\n{\n  \"content\": [\n    {\n      \"type\": \"text\",\n      \"text\": \"{\\\"value\\\":[],\\\"version\\\":12}\"\n    }\n  ],\n  \"structuredContent\": {\n    \"value\": [],\n    \"version\": 12\n  },\n  \"isError\": false\n}\n```\n\nApplication-level validation and conflict errors are returned as `isError: true` tool results.\nUnexpected exceptions are reported privately and become a generic `TOOL_FAILED` or\n`BACKEND_ERROR`; stack traces and exception text are never sent to the agent.\n\n## Security properties\n\n- The HTTP `Origin` header is checked when present to prevent DNS-rebinding and browser-origin\n  attacks.\n- Request and response bodies are bounded before execution.\n- Tool input and backend output use the same runtime schemas as the application.\n- Authorization codes are single-use, expire after five minutes, and require PKCE `S256`.\n- Redirect URIs must be exact registered HTTPS URLs or HTTP loopback URLs; fragments and embedded\n  credentials are rejected.\n- Access tokens are stored only as SHA-256 digests, expire after one hour, and are bound to the\n  exact MCP resource.\n- Refresh tokens rotate, expire after 30 days, and reuse revokes the entire token family.\n- OAuth client registration is bounded and never fetches caller-controlled metadata URLs, avoiding\n  an authorization-server SSRF surface.\n- Disabling an account immediately invalidates its agent tokens.\n- Public discovery is cacheable but contains no detailed tools or user-specific information.\n\nTreat agent input as untrusted even after OAuth. Authorization identifies the user; it does not\nmake model-generated arguments safe.\n"}