YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.

Interface▾


Observability▾POST /mcp. There is no second API and nothing to add to a signal file: the same endpoint runs through the same guards, middleware and service. The chat inside your own pages is a different surface, the In-Page Agent.akan:// URI, which a client can attach as context.akan:// resource URI.| Refused when |
|---|
| ↳ Why, and what to do |
It declares mcp: false |
| It was curated off the shelf on purpose; its guards and HTTP stay exactly as they were. |
A guard declares static agents = false, like Person |
| It is an act reserved for a person, so no model is ever offered it. |
It declares no guards, or an empty list |
Nobody decided who may call it; write guards: [Public] if anonymous access is the intent. |
It is the generated light<Model> read |
It reads the same document as <model> in a smaller shape, so the agent calls <model> instead. |
It is a pubsub or a message |
| It rides the websocket, and its arguments read a socket an MCP request does not have. |
The deployment is read-only and it is not a query |
The readOnly valve drops every mutation, whatever its guards allow. |
It returns Any, Upload or Binary |
| A model cannot be told what comes back, and raw bytes only fill its context window. |
| It takes a file upload |
| A file upload has no MCP representation. |
It is a mutation whose only guard is Public |
[Public] on a write is having no guard, spelled out; add a real one. |
A required argument is typed Any |
Any is left out of the schema, so expose a named filter slice instead. |


Unknown tool, and a guard's refusal always reads You are not permitted to perform this action. without naming the guard. Never make either message more helpful: the difference would let a caller enumerate your private surface./mcp is mounted by default, so a new app already serves it. Change its settings in lib/option.ts with setMcp(), not in main.ts:option.ts is read in mount order and the app's last, so the app has the final word.AKAN_MCP_* env of the same name, but writing undefined does not erase the env's value. AKAN_MCP=false keeps /mcp off whatever the code says.setMcp(false) in code, or set AKAN_MCP=false in the env.setMcp((options) => ({ … })) receives the server options from env.server.*, for a value decided at boot. libs/shared builds its auth this way./mcp is mounted; false or 0 in the env turns it off whatever the code says.true or 1.aud a token needs, follows it.serverInfo.version, the same placeholder the OpenAPI document uses.nextCursor for the rest.shallow names nested models, full inlines, none omits.tools/call, resources/read and prompts/get, counted per process.429 with Retry-After. Listings are not counted, and N replicas grant N budgets.outputSchema: "none" keeps the text block whatever legacyTextBlock says: a client reads structuredContent only against a declared schema.query or mutation with a real guard is published as a tool:startTask..param(), .search() and .body() argument in one object; .search() ones are optional..desc().readOnlyHint on a query, destructiveHint on a remove… or delete… mutation..desc() is a broken tool, not an untidy one..arg(): each description rides in the input schema.slice() guards map. A named slice does not inherit that map: write its own guards, or it is not published.guards.root, opted out with mcp: { root: false }.guards.get and mcp: { get: false }; lightTask is never published.guards.cru and mcp: { cru: false }, or a per-verb key such as create.init({ guards, mcp }) counts.mcp: false. On slice() it is a map keyed like guards:mcp: false is curation, not authorization. It takes an entry off the shelf; its guards and HTTP stay the same.slice() map mirrors guards key for key (root, get, cru, create, update, remove) and reaches exactly as far: the root slice and generated CRUD.mcp: false turns them all off. It expands to root, get and cru, and create, update and remove inherit cru.<model> and the <model>List… reads have one. An insight is an aggregate with nothing to point at, and a custom endpoint keeps its tool but gets no template.…/list. The third segment belongs to the slice key, so the root list has none.lightTask gets neither a tool nor a URI.

queryKey takes one of the model's filter names, but args is typed Any, so it is left out of the schema and a value sent for it is refused. Declare a named filter slice when an agent should pass a filter's arguments..prompt(name, description): the user runs it as a slash command, and the model receives what the page loads:<Agent.Guide> text never reaches MCP..param() is required, .search() is optional, and desc becomes the argument's description.Comma-separated list. appended, and an ID, Int or enum value is checked by the page's own declaration.^[A-Za-z0-9_-]{1,64}$ and is unique across pages; prompts/list lists every page with .prompt().prompt() builder, and Msg is not a public API.prompts/get runs the page body (root layouts, layouts, then render) in the RSC worker under the caller's bearer token. Nothing is rendered and no client component runs; each fetch.* query the page makes becomes part of the answer:akan:// URI the tool answers to; a custom read gets akan://<toolKey>?args.project and a page's lightProject, is attached once, as the larger.401 credential challenge, so the client signs in instead of giving up.router.notFound(), or a document it reads is missingpromptBudget (60,000 characters by default), largest first, with a note: Attached the first N of M rows of <key>; call it for the rest. A single document is never cut.mcp: false and Person still apply.web: false runs none.report does nothing, so the same service runs unchanged over HTTP, a websocket and in tests:Accept: text/event-stream and a _meta.progressToken. Only tools/call streams.exec already running; it finishes with nobody waiting.McpProgress.streaming is true while a caller is reading, so build an expensive message only then.Self and the account middleware behave as they do for a browser. One difference: the cookie header is dropped at the door, so the Authorization header is the only credential.static scope, with no default. It decides whether the guard can hide an entry from a caller's listing:/.well-known/oauth-authorization-server/mcp at it with the three env vars below. Naming an issuer is what makes /mcp demand a token.AKAN_MCP_AUTH_SERVERS401 with WWW-Authenticate; before that, only a call a guard refuses does. Either way the client signs in instead of concluding the tool does not exist.insufficient_scope is enforced only once AKAN_MCP_SCOPES is set. Tokens an app mounting libs/shared issues itself carry no scope claim, so setting it there refuses every one of them with 403.aud is refused once an issuer is named and accepted while none is. An aud naming another resource is always refused.auth.verify through setMcp() so a forged token is refused instead of read as an anonymous caller; libs/shared does this for its own tokens.libs/shared, step by step: OAuth For Agents.MCP catalogue: tools=… is followed by one line per refused endpoint with its reason, and both sit below the default level, so set AKAN_PUBLIC_LOG_LEVEL=verbose..desc() in its dictionary .of(). Generated CRUD tools append it to "Get X", and the root list and insight use the .of() label and description; those entries have no other text.$ref across entries, so every entry inlines the schema of every model it mentions, and the whole listing is re-sent to every agent that connects. The per-signal MCP catalogue cost: line says where the bytes went; mcp: { cru: false } is the usual lever.Unknown argument "x". and a missing document No <model> found for the arguments given. Only a genuine failure says the server failed.field.visual. It is stripped from every MCP result and from the readable schema, so the two agree.