One site, or the whole workspace if an Owner made it
Agent token
bz_mcp_live_
/mcp/v1
A person, in Developer › MCP server
Until revoked
One site
OAuth access token
bz_mcp_live_
/mcp/v1
Consent page, through an MCP client
1 hour; its refresh token lasts 30 days
One site
A token is its prefix and 43 URL-safe characters. Brenzuri keeps only a SHA-256 hash, so a token is shown once, when it is made, and cannot be shown again.
Every request carries the token as a bearer credential.
The prefix is tied to the path. An agent token on a REST route, or a key on /mcp, is refused as if it were unknown. The header name Bearer is matched case-insensitively. A bearer token wins over a session cookie when both are sent.
An unknown, revoked or wrongly prefixed token gets 401 invalid_key with WWW-Authenticate: Bearer error="invalid_token". On /mcp the challenge also carries a resource_metadata address, which is how an OAuth client finds the sign-in.
More than 20 refused tokens a minute from one client address are answered 429 rate_limited, whatever the token.
A credential whose creator has left the workspace, or whose site was deleted, is refused as unknown.
A deleted workspace answers 410 workspace_deleted, with deletedAt and restoreUntil.
A route not on the allowlist answers 403 key_not_allowed; a missing scope answers 403 scope_missing. See Roles and scopes.
POST /api/v1/developer/mcp/agents makes an agent token from { name, siteId, scopes[], dailyCap }. An agent is always bound to one site. The workspace plan limits how many agents are connected; a start over the limit is 409 plan_limit. Agents made by OAuth appear in the same list.
A client that speaks OAuth gets an agent without anyone pasting a token. The server is a public-client, authorization-code-with-PKCE server and nothing else.
All on the Brenzuri host. Both metadata documents are served from the same host.
Document or route
Address
Protected resource metadata
/.well-known/oauth-protected-resource and /.well-known/oauth-protected-resource/mcp/v1
Either register one, or use a client id that is an https address.
Dynamic registration.POST /api/v1/oauth/register with { client_name?, redirect_uris[] }. One to five redirect URIs, each https or an http loopback address (localhost, 127.0.0.1, [::1]), at most 512 characters, no fragment. The answer is 201 with client_id. A loopback redirect may use any port.
Client ID metadata document. A client_id that is an https URL with a path. Brenzuri fetches it (16 KB at most, 5 seconds, no redirects), and it must repeat the client_id and list one to ten valid redirect_uris. It is cached for an hour.
Shell
curl -X POST "$BRENZURI_URL/api/v1/oauth/register" \
-H "Content-Type: application/json" \
-d '{ "client_name": "My agent", "redirect_uris": ["http://127.0.0.1:8765/callback"] }'
Build an authorization address with a PKCE S256 challenge and open it in a browser. The challenge is the base64url of the SHA-256 of a random verifier of 43 to 128 characters.
The 43-character challenge and S256. No other method is accepted.
state
Optional. Returned unchanged.
resource
Optional. If sent it must be the MCP server address, https://{your Brenzuri host}/mcp/v1.
scope
Optional, space-separated. Without it, or with unknown scopes, all five are offered.
The person signs in if they are not, and the page asks them for four things: one site, the scopes they allow (never more than were asked for), a daily credit cap (5 if left alone, 0 to 1000), and a name. They need a role that can edit. Pressing Allow redirects to your redirect_uri with code=bz_code_…, state and iss; pressing Deny redirects with error=access_denied. A code lasts 10 minutes and works once.
The access token is an agent token and lasts one hour. Send it to /mcp/v1 as a bearer token.
To renew, POST /api/v1/oauth/token with grant_type=refresh_token, refresh_token and client_id. Refresh tokens rotate: each use returns a new one, and presenting an old one again revokes the agent. A refresh checks that the person may still hold that site.
POST /api/v1/oauth/revoke with token (and optionally client_id) always answers 200.
Token and revocation bodies are form-encoded or JSON. Errors have the OAuth shape { "error", "error_description" }: invalid_request, invalid_client (401), invalid_grant, unsupported_grant_type, invalid_target, invalid_redirect_uri.