Authentication
The MCP server accepts a Bearer token in the Authorization header, and
nothing else β no cookie, no client certificate.
:::warning Never in the URL
A token in the query string (?access_token=β¦, or parameters such as token,
bearer, api_key and key) does not authenticate, even next to a valid
header: the request gets 401 with error="invalid_request" in
WWW-Authenticate. A token in the URL leaks into proxy and CDN logs, the
Referer and history β the MCP authorization spec and RFC 6750 forbid it.
:::
That token is an access token issued by the Kobana Authorization Server
(https://account.kobana.com.br), a JWT signed with RS256 and issued for
this resource: its aud is https://mcp.kadu.ia.br (in staging,
https://mcp.staging.kadu.ia.br), and only that β a token with more than one
audience is refused. The token arrives one of two ways β the OAuth flow, run by
the client itself, or a personal token you create in the Kobana Account β
and is verified the same way in both cases, with the same local signature check
as the v1 API.
Authorization: Bearer eyJhbGciOiJSUzI1NiIsβ¦
:::info Each surface has its own audience
A token issued for the MCP is not valid on the API (api.kadu.ia.br), and
an API token is not valid here. That keeps a token handed to an assistant from
being reused as a REST credential β and the reverse.
:::
Two ways to connectβ
| OAuth flow | Personal token | |
|---|---|---|
| How you get it | Paste the URL into the client; it opens the Kobana Account sign-in and you authorize | Create it in the Kobana Account token wizard, with MCP usage, the resources and access levels, and the validity |
| How the client uses it | On its own, after authorization, renewing it when it expires | You paste it into the client as Authorization: Bearer <token> |
| Scope | The resources and access levels you kept checked on the consent screen | The resources and access levels you checked |
| Good for | Claude, ChatGPT, IDEs and any client with OAuth | Clients without OAuth support, scripts and automations |
The personal token must be created with MCP usage. A token created for the
API carries the API audience and is refused here with 401 β and the reverse
holds too.
The OAuth flowβ
You run none of these steps: the MCP client does everything on its own from the URL. They are here for when something fails along the way.
-
Challenge. A request without a token gets
401with the challenge every OAuth 2.1 resource server must send (RFC 9728):WWW-Authenticate: Bearer realm="kadu-mcp",error="invalid_token",resource_metadata="https://mcp.kadu.ia.br/.well-known/oauth-protected-resource" -
Resource metadata. The client reads the document it points to. It is public and carries no secret β only pointers:
curl https://mcp.kadu.ia.br/.well-known/oauth-protected-resource{"resource": "https://mcp.kadu.ia.br","authorization_servers": ["https://account.kobana.com.br"],"bearer_methods_supported": ["header"],"scopes_supported": ["kadu/activities","kadu/environments","kadu/executions","kadu/files","kadu/memberships","kadu/memories","kadu/organizations","kadu/repository","kadu/roles","kadu/schedules","kadu/secrets","kadu/system_events","kadu/tasks","kadu/todos","kadu/usage","kadu/users","kadu/webhook_deliveries","kadu/webhooks","kadu/workspaces","read","write","delete","import","export","all"],"resource_documentation": "https://api.kadu.ia.br/v1/docs"}kadu/allis left out on purpose: the catch-all is not handed to a client that registered itself. -
Authorization Server metadata (RFC 8414), at
https://account.kobana.com.br/.well-known/oauth-authorization-server. -
Dynamic client registration (RFC 7591). The client registers itself.
-
Authorization with PKCE and
resource(RFC 8707). You sign in to the Kobana Account, pick the organization (if you have more than one), review what the client asks for and authorize. -
Token. The client exchanges the code for a short-lived access token and sends it on every request, renewing it when it expires.
The token carries who you are (sub) and the organization you authorized
(org). The org scopes the data β no tool takes an organization as an
argument. A token whose organization does not exist in Kadu gets
403 unknown_organization.
Authorization: the role of who authorizedβ
What a caller sees is narrower than the catalog: tools are filtered by the
role of the token's person in the organization β the same
agent.<resource>.<action> permissions as the console and the API (see
Permissions). A tool requires exactly the
permission of the route it mirrors. The ceiling is always what that person can
do in the console.
| Situation | Result |
|---|---|
| The token's person is not a member of the organization | Denies every tool (not_a_member). Authorize again with an account that belongs to the organization. |
| Member | The role's permissions, applied tool by tool. The wildcards *, agent.* and agent.<resource>.* work as in the console. |
| The permission lookup fails | Denies every tool. Fails closed. |
Unlike the API, the MCP has no integration token without a person: the token
must lead to a person of the organization (by sub or by the token's e-mail),
otherwise no tool runs.
Permissions are re-read at least once a minute, so changing or removing someone's role takes effect within a minute, without waiting for the token to expire.
A tool the caller cannot run is not listed in tools/list. When
tools/call refuses, the code says who fixes it:
| Code | Who fixes it |
|---|---|
insufficient_scope | You, authorizing again with the missing access β or using a personal token that reaches the resource at the required level. |
insufficient_permission | An administrator of the organization, granting the permission (the error names it). |
not_a_member | Authorizing again with an account that is a member of the organization. |
:::tip Recommended posture for agents Authorize the client with an account that has only the permissions the agent needs. A client authorized by someone who can only read exposes only the read tools, whatever the model on the other side does. :::
Token scopeβ
On top of the role, the MCP enforces the scope the token carries, in the
scope claim, with the same grammar as the API (see
API authentication β Scopes). The two
checks add up: a tool appears only if both allow it.
| Tool | Requires |
|---|---|
Read (*_list, *_get, kadu_usage_getβ¦) | The resource scope (kadu/tasks) |
Delete (*_delete) | The resource scope + delete |
| Any other change (create, update, run, cancel, continue, sync, replay, rotate a secret) | The resource scope + write |
kadu_me_get | Any valid token |
The resource is the first segment of the route the tool mirrors:
kadu_executions_logs_list belongs to executions, and kadu_tasks_run
requires kadu/tasks write. Approvals by link (kadu_operation_approvals_*)
are read with kadu/system_events. No level replaces another: write does not
delete. No tool requires import or export.
| Token scope | Result |
|---|---|
kadu/executions kadu/todos read write | Reads and changes executions and pending items β cancels, continues and resolves β and nothing else |
kadu/tasks kadu/executions write | Reads, runs and follows tasks, without deleting anything |
read (no kadu/β¦ at all) | Reads everything, writes nothing |
:::info Clients connected through OAuth ask for every level β you choose which to grant
The /.well-known/oauth-protected-resource document announces Kadu's resources
and the six access levels, and that is what the client asks for on the consent
screen. There you uncheck what you do not want to grant: the issued token
carries only the resources and levels left checked, and the MCP lists only the
tools they reach. A tool outside it returns insufficient_scope, and the way
out is to authorize again granting the missing access β delete for the
delete tools, for example.
:::
Revoking a personal tokenβ
Kadu verifies the token on its own, by its signature, without asking the Kobana Account on every call: revoking a personal token there does not cut access immediately, and it stays accepted until it expires. So:
- pick the shortest validity the use allows;
- check only the resources and access levels the agent needs;
- to cut an agent off immediately, remove the person's membership in the organization or change their role β it takes effect within a minute.
Failure modesβ
| HTTP | code | Cause |
|---|---|---|
| 400 | -32022 (JSON-RPC) | Unsupported protocol version, in the MCP-Protocol-Version header or in _meta; data.supported lists the supported ones. See Transport. |
| 401 | invalid_token | Token missing, malformed, invalid, issued for another resource (the API, for example) or with more than one audience in aud. |
| 401 | token_expired | The token expired. The client renews it and tries again. |
| 401 | token_revoked | The token was revoked in Kadu. |
| 401 | invalid_request | Token in the query string β see the warning at the top of the page. |
| 403 | unknown_organization | The token is valid, but the organization it names does not exist in Kadu. |
| 403 | FORBIDDEN | Origin outside the allowed list (origin_not_allowed). |
| 404 | NOT_FOUND | The request arrived through a host that does not serve MCP. |
| 413 | -32600 (JSON-RPC) | Body above 1 MB. |
| 429 | TOO_MANY_REQUESTS | Rate limit reached, with Retry-After and X-RateLimit-* β see Rate Limiting. |
| 503 | SERVICE_UNAVAILABLE | We could not verify the token against the Authorization Server. It is on our side: try again after Retry-After. |
Every 401 carries the WWW-Authenticate challenge above, which sends the
client back to the OAuth flow. The challenge always says
error="invalid_token" (or invalid_request, for a token in the URL), as RFC
6750 requires; the body code tells an expired token (token_expired) apart
from the rest. Attempts with a refused token count against a per-IP limit, and
past it the answer becomes 429.
No MCP response is cacheable: all of them, refusals included, carry
Cache-Control: no-store and Content-Security-Policy: default-src 'none'.
The exception is the public /.well-known/oauth-protected-resource document,
with Cache-Control: public, max-age=300.
Scope and permission denials are not HTTP errors: they come back as a tool
result marked isError, so the model reads and reacts to them β see
Errors.
Host and edgeβ
The MCP answers only through mcp.kadu.ia.br β never through the console host,
never through the API host, which point at the same application. Through any
other path the answer is 404: the endpoint does not announce that it exists
elsewhere.
The OAuth resource identifier β and therefore the audience the token must
carry β comes from the server configuration, never from the request's
Host, so a forged Host cannot point a client at another identifier.