Skip to main content

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 flowPersonal token
How you get itPaste the URL into the client; it opens the Kobana Account sign-in and you authorizeCreate it in the Kobana Account token wizard, with MCP usage, the resources and access levels, and the validity
How the client uses itOn its own, after authorization, renewing it when it expiresYou paste it into the client as Authorization: Bearer <token>
ScopeThe resources and access levels you kept checked on the consent screenThe resources and access levels you checked
Good forClaude, ChatGPT, IDEs and any client with OAuthClients 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.

  1. Challenge. A request without a token gets 401 with 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"
  2. 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/all is left out on purpose: the catch-all is not handed to a client that registered itself.

  3. Authorization Server metadata (RFC 8414), at https://account.kobana.com.br/.well-known/oauth-authorization-server.

  4. Dynamic client registration (RFC 7591). The client registers itself.

  5. 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.

  6. 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.

SituationResult
The token's person is not a member of the organizationDenies every tool (not_a_member). Authorize again with an account that belongs to the organization.
MemberThe role's permissions, applied tool by tool. The wildcards *, agent.* and agent.<resource>.* work as in the console.
The permission lookup failsDenies 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:

CodeWho fixes it
insufficient_scopeYou, authorizing again with the missing access β€” or using a personal token that reaches the resource at the required level.
insufficient_permissionAn administrator of the organization, granting the permission (the error names it).
not_a_memberAuthorizing 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.

ToolRequires
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_getAny 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 scopeResult
kadu/executions kadu/todos read writeReads and changes executions and pending items β€” cancels, continues and resolves β€” and nothing else
kadu/tasks kadu/executions writeReads, 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​

HTTPcodeCause
400-32022 (JSON-RPC)Unsupported protocol version, in the MCP-Protocol-Version header or in _meta; data.supported lists the supported ones. See Transport.
401invalid_tokenToken missing, malformed, invalid, issued for another resource (the API, for example) or with more than one audience in aud.
401token_expiredThe token expired. The client renews it and tries again.
401token_revokedThe token was revoked in Kadu.
401invalid_requestToken in the query string β€” see the warning at the top of the page.
403unknown_organizationThe token is valid, but the organization it names does not exist in Kadu.
403FORBIDDENOrigin outside the allowed list (origin_not_allowed).
404NOT_FOUNDThe request arrived through a host that does not serve MCP.
413-32600 (JSON-RPC)Body above 1 MB.
429TOO_MANY_REQUESTSRate limit reached, with Retry-After and X-RateLimit-* β€” see Rate Limiting.
503SERVICE_UNAVAILABLEWe 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.