Authentication
The MCP server is an OAuth 2.1 Resource Server. It accepts a Bearer token in the Authorization header and nothing else — no cookies, no client certificate, and never a token in the URL: ?access_token= is ignored, and such a request is answered as unauthenticated (401 with the challenge). A URL ends up in access logs and Referer headers; a bearer token must not.
:::caution No mTLS here
The REST API requires a client certificate (/api/v1 authentication); the MCP host deliberately does not. No MCP client presents a client certificate, so the transport layer is replaced by OAuth, not dropped: the audience check, the scope check and the per-person permission check all still run.
:::
Two ways to connect
The token is an OAuth 2.1 access token issued by the Kobana authorization server, an RS256 JWT issued for this resource: its aud is the MCP server URL (https://mcp.inbox.kobana.com.br). It reaches the server one of two ways, and is verified the same way in both.
| OAuth flow | Personal token | |
|---|---|---|
| How you get it | Paste the URL into the client; it opens the Kobana login and you authorize | Create it in Kobana Account → Tokens and Access → New Personal Token: app Autonomous Inbox, use MCP, resources and access, validity |
| How the client uses it | On its own, after authorization | You paste it into the client as Authorization: Bearer <token> |
| Validity | Short, renewed by the client | 7 days to 1 year, your choice |
| Scope | The resources you consented to, read-only | The resources and access you ticked |
| Good for | Claude, ChatGPT, IDEs and any client with OAuth | Clients without OAuth, scripts, and agents that need to write |
:::info Each surface has its own audience
A token issued for the MCP server is not valid on the REST API (api.inbox.*), and an API token is not valid here — a token handed to an assistant cannot be replayed against the API. A personal token has to be created with the MCP use for the same reason. A token naming more than one audience is refused.
:::
The OAuth flow
You run none of these steps: the MCP client does everything from the URL. They are here for when something fails midway.
-
Challenge. A request with no token is answered
401with the challenge every OAuth 2.1 resource server has to send (RFC 9728):WWW-Authenticate: Bearer realm="kobana-inbox-mcp", error="invalid_token", resource_metadata="https://mcp.inbox.kobana.com.br/.well-known/oauth-protected-resource" -
Resource metadata. The client reads the document it points at. It is public and carries only pointers; the authorization server is named by its issuer identifier:
curl https://mcp.inbox.kobana.com.br/.well-known/oauth-protected-resource{"resource": "https://mcp.inbox.kobana.com.br","authorization_servers": ["https://account.kobana.com.br"],"bearer_methods_supported": ["header"],"scopes_supported": ["inbox/agent_runs", "inbox/agents", "inbox/emails", "inbox/inboxes","inbox/secrets", "inbox/system_events", "inbox/usage", "inbox/webhooks","inbox/workflow_runs", "inbox/workspaces", "read"],"resource_documentation": "https://docs.inbox.kobana.com.br"} -
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; the consent screen says it is not a verified app.
-
Authorization with PKCE and
resource(RFC 8707). You log in to Kobana, pick the organization (if you have more than one) 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), the organization you authorized (org) and the scopes you consented to (scope). It is org that scopes the data — no tool takes an organization as an argument.
Two levels of authorization
Authorization is the conjunction of two independent checks — one about the client, one about the person.
| Layer | Answers | Decided by | Effect on MCP |
|---|---|---|---|
Scope (inbox/X + the global write verb, or the access level) | what this client may ask for on the person's behalf | the person, on the consent screen or when creating a personal token | filters tools/list, blocks tools/call with insufficient_scope |
Permission (resource.action) | what this person may do in this organization | the organization's administrator, in the dashboard | filters tools/list, blocks tools/call with insufficient_permission |
The two error codes are distinct because different people fix them: a missing scope is fixed by whoever owns the connected client, a missing permission by the organization's administrator.
Every MCP token is a person's. The authorization server only issues a resource-bound token to a person, so the MCP server always applies that person's permissions — whatever src the token carries. A person who is not a member of the organization reaches nothing (403, no_membership). Permissions are read on every call, so revoking a role in the dashboard takes effect on the next tool call, without waiting for the token to expire.
Scopes
Scopes have two orthogonal axes: the resource, always prefixed by the app, and the verb, a global scope with no prefix. A resource without a verb means read. Writing requires both.
| Scope | Grants |
|---|---|
inbox/workspaces | Read workspace tools |
inbox/inboxes | Read inbox tools and allowed recipients |
inbox/emails | Read e-mail tools, including the .eml download |
inbox/agents | Read agent tools, prompt versions and the model catalog |
inbox/agent_runs | Read agent-run tools |
inbox/usage | Read the usage and cost tools |
inbox/webhooks | Read webhook tools and their deliveries |
inbox/system_events | Read system-event tools |
inbox/secrets | Read secret tools — key, notes and metadata. The value is never returned, by any tool or endpoint |
inbox/workflow_runs | Read workflow-run tools, including every step of a run |
inbox/all | Resource catch-all — reads every resource above, grants no verb |
write | Global verb: every write tool on the resources the token carries |
A read-only token is the recommended default for an agent: with inbox/all alone, the connected client physically cannot write, even if the person behind it could. A client registered through the OAuth flow gets resource scopes and no verb — read-only by construction.
The access level. Every token the authorization server issues also carries the access level the person granted — read, write, delete, import, export, all. It is honoured next to the grammar above:
- With
inbox/…scopes in the token, the resources define the reach, andread,writeandallact only within them:inbox/emails read writereads and writes e-mails and nothing else;inbox/all readreads everything and writes nothing. - Without
inbox/…scopes, the access level alone decides: reads withread,writeorall; writes withwriteorall.
delete, import and export on their own do not count as a write.
Each tool page in Resources states the scope and the permission it requires.
Choosing the workspace
A person-bound token does not pin a workspace — an organization may have several. Pick one per connection with:
X-Workspace-Id: <workspace-id-or-external-id>
When the organization has exactly one workspace, the header is optional. With more than one and no header, the server answers 400 workspace_required. A header naming a workspace outside the token's organization is 403 forbidden with message: unknown_workspace — indistinguishable from a nonexistent one. No tool accepts a workspace argument.
Personal token revocation
Revoking a personal token in your Kobana account does not cut access immediately: the Autonomous Inbox verifies the token on its own, without asking Kobana on every call, and it stays valid until it expires. So:
- pick the shortest validity the use allows;
- tick only the resources and access the agent needs;
- to cut an agent off immediately, suspend or remove the person's membership in the organization — permissions are read on every call and take effect at once.
Failure modes
| HTTP | code | Cause |
|---|---|---|
| 400 | workspace_required | Organization has several workspaces and none was selected |
| 400 | -32022 (JSON-RPC) | Protocol version not supported, in MCP-Protocol-Version or in _meta; data.supported lists the supported ones. See Transport |
| 401 | invalid_token | Token missing, malformed, with an invalid signature, or issued for another resource (the REST API, say) |
| 401 | invalid_issuer | iss is not the Kobana issuer |
| 401 | token_expired | exp in the past |
| 401 | token_revoked | jti revoked |
| 403 | forbidden | Request did not come through the MCP edge, or the token resolves to no tenant — message says which: unknown_organization (the org claim names an account this server does not know), no_membership (the person has no live membership in that organization) or unknown_workspace (X-Workspace-Id outside the organization) |
| 403 | origin_not_allowed | Origin header outside the allow-list |
| 404 | not_found | Request arrived on a host that does not serve MCP, or asked for a path that is not an MCP endpoint — always this JSON body, for every method |
| 429 | rate_limited | Rate limit exceeded — see Rate limiting. Rejected tokens are also throttled per client IP |
| 503 | as_unavailable | The signing keys could not be fetched from the authorization server. Not a problem with your token: retry after Retry-After. Never a 401, which would send the client through the whole OAuth flow again for nothing |
Every 401 carries the WWW-Authenticate challenge above, which sends the client back into the OAuth flow. Scope and permission denials are not HTTP errors: they come back as tool results flagged isError, so the model can read and react to them — see Errors.