A client that receives 401 with a WWW-Authenticate challenge has everything it needs to reach the authorization server. Every step is a plain GET or POST against the same host. Replace <host> with the blocklet's host.
| Step | Request | Result |
|---|---|---|
| 1 | tools/call a write tool, no credential | 401 + WWW-Authenticate carrying the metadata URL |
| 2 | GET /.well-known/oauth-protected-resource | Which authorization server protects this resource |
| 3 | GET /.well-known/oauth-authorization-server | Endpoints, grant types, PKCE method |
| 4 | POST /.well-known/service/oauth/register | A client_id for this client |
| 5 | Authorize, then exchange for a credential | Authorization: Bearer blocklet-… |
Step 2: protected resource
curl -s https://<host>/.well-known/oauth-protected-resource{
"resource": "https://<host>/mcp",
"authorization_servers": ["https://<host>"]
}The authorization server is the same host. A blocklet protects only its own resource and issues only its own credentials, so a client that talks to several blocklets registers and authorizes with each one separately.
Step 3: authorization server metadata
curl -s https://<host>/.well-known/oauth-authorization-server{
"issuer": "https://<host>",
"authorization_endpoint": "https://<host>/.well-known/service/oauth/authorize",
"registration_endpoint": "https://<host>/.well-known/service/oauth/register",
"device_authorization_endpoint": "https://<host>/.well-known/service/oauth/device_authorization",
"token_endpoint": "https://<host>/.well-known/service/oauth/token",
"response_types_supported": ["code"],
"grant_types_supported": [
"urn:ietf:params:oauth:grant-type:device_code",
"authorization_code",
"refresh_token"
],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"scopes_supported": ["mcp"]
}token_endpoint_auth_methods_supported is none, so a client authenticates the token request with its PKCE verifier rather than a client secret. scopes_supported has one entry, so there is no narrower or broader scope to choose between.
Step 4: register the client
A client obtains its client_id by registering itself. No pre-shared identifier is handed out.
curl -s -X POST https://<host>/.well-known/service/oauth/register \
-H 'Content-Type: application/json' \
-d '{
"client_name": "my agent",
"redirect_uris": ["http://127.0.0.1:3118/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'{
"client_id": "b665f46e-8fb5-4bf9-bd32-53df5b35f7f5",
"redirect_uris": ["http://127.0.0.1:3118/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}The response echoes the registration, not just the identifier. Clients validate this and reject a reply that omits fields they sent.
Register once and keep the client_id. Registration is rate limited per source address, and a registration never used in a completed authorization is swept after seven days.
Redirect URIs
A client running on the user's machine listens on a port chosen at run time, so register the exact redirect_uris this session will use rather than expecting a fixed value to be pre-approved. Loopback addresses, https URLs, and private-use schemes are accepted; anything else is rejected. See Errors.
Step 5: obtain the credential
Two grants lead to the same credential. Your tool usually picks one for you, see Connect your tool.
| Your client | Grant |
|---|---|
| Can open a browser and listen on a local port, as most MCP clients do | authorization_code with PKCE, using authorization_endpoint then token_endpoint |
| Cannot: a CLI, CI job, or headless agent | The device grant, see Credentials without a browser |
Both end at the same token_endpoint and return the same credential shape.
What the credential is
The token endpoint returns a bearer token that is also an access key:
{
"access_token": "blocklet-zEJYCdC9awCqxEPLhqreFbwXjqU6Y2BHvKVQAx6kMZfRh",
"token_type": "Bearer",
"scope": "mcp"
}Three things follow from that response, and each matters:
- The
blocklet-prefix is how the runtime recognises it. OAuth token and access key are not two mechanisms; they are one credential with one prefix. - It carries the role the person picked on the consent screen (
owner,admin,member, orguest), scoped to this blocklet instance. See Access tiers. - The device grant returns no
expires_inand norefresh_token. The authorization server advertisesrefresh_tokenas a supported grant, but the device flow does not hand one back.
Use it
Send it as a bearer token on /mcp:
curl -s -X POST https://<host>/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Authorization: Bearer blocklet-…' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'The 401 is gone: the same tools/call that was refused before now reaches the tool.
What the credential leaves closed
Reaching the tool is not the same as the operation succeeding. An authenticated write can still come back like this, 200 at the transport layer but refused inside:
{
"result": {
"content": [{
"type": "text",
"text": "AFS_FORBIDDEN: Forbidden at /instance/notes.txt: network clients cannot write base paths; use a resolver overlay or internal code"
}],
"isError": true
},
"jsonrpc": "2.0",
"id": 3
}An owner-role credential gets the same refusal. Writing from the network is not something a credential turns on. The blocklet must have declared that path open to network clients, via a resolver overlay or code running inside it.
If your agent must write, that is a decision for whoever owns the blocklet. Read Access tiers before designing around it.