The authorization code grant needs a browser to redirect to and a local port to redirect back to. A client with neither, such as a CLI on a server, a CI job, or an agent on a headless machine, uses the device grant instead.
It produces the same credential as the browser grant: a blocklet- prefixed access key, sent as a bearer token. Replace <host> with the blocklet's host.
1. Start a device authorization
curl -s -X POST https://<host>/.well-known/service/oauth/device_authorization \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'client_id=<client_id>&scope=mcp'{
"device_code": "bc0cedc2-899f-49da-b413-213f31c4aae8",
"user_code": "BZVR-ZQZQ",
"verification_uri": "https://<host>/.well-known/service/gen-access-key",
"verification_uri_complete": "https://<host>/.well-known/service/gen-access-key?__token__=bc0cedc2-…",
"expires_in": 300,
"interval": 5
}Obtain the client_id by registering first. See Authorize a client.
| Field | Use |
|---|---|
user_code | Show it to the person. The alphabet excludes vowels and ambiguous characters, so it can be read aloud |
verification_uri | Where the person goes to approve |
verification_uri_complete | The same page with the request pre-filled; use it when you can render a link or a QR code |
expires_in | Seconds until this request expires. 300 above. It does not describe the credential |
interval | Minimum seconds between polls. 5 above |
2. The person approves
Opening verification_uri_complete shows a consent screen naming the requesting application and the account, with an access level selector. It offers owner (the default), admin, member, and guest; the credential carries whichever is chosen.
The role is scoped to this blocklet instance and is an upper bound, not a grant. See Access tiers.
3. Poll for the credential
Poll the token endpoint with the device code grant until the request is approved or expires. Respect interval; polling faster is what the field exists to prevent.
curl -s -X POST https://<host>/.well-known/service/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:device_code' \
--data-urlencode 'device_code=bc0cedc2-899f-49da-b413-213f31c4aae8' \
--data-urlencode 'client_id=<client_id>'Once approved:
{
"access_token": "blocklet-zEJYCdC9awCqxEPLhqreFbwXjqU6Y2BHvKVQAx6kMZfRh",
"token_type": "Bearer",
"scope": "mcp"
}Before approval, and after the five-minute window closes, the same request answers 400:
{"error":"invalid_grant","error_description":"Invalid or expired device_code"}invalid_grant most often means the window expired rather than that the code was wrong. Restart from step 1.
No expires_in, no refresh_token. The authorization server advertises refresh_token as a supported grant, but this flow does not return one. Treat the access key as the credential you keep.
4. Use it
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 credential clears the credential gate. It does not clear the path policy: an authenticated write can still return AFS_FORBIDDEN. See Authorize a client.
Which grant to use
| Situation | Grant |
|---|---|
| Client can open a browser and listen on a local port | authorization_code with PKCE. See Authorize a client |
| Client has no browser, or the person approving is on a different device | Device grant, this page |
MCP clients generally implement the authorization code grant, not the device grant. Use the device grant for your own tooling rather than expecting a third-party MCP client to discover and drive it.