Skip to content

Authentication

BlenderMCP servers implement the MCP-spec authorization profile: OAuth 2.1 with PKCE (S256), Dynamic Client Registration, and .well-known/ discovery. Consumers should use a library that handles this flowfastmcp.Client and any MCP-spec-compliant client do — but the wire details are documented here for anyone implementing a client from scratch or debugging an unexpected token failure.

Every interaction starts with a 401 that points at the resource-metadata endpoint:

Terminal window
$ curl -i https://mcp.blender.bet/
HTTP/2 401
www-authenticate: Bearer error="invalid_token",
error_description="...",
resource_metadata="https://mcp.blender.bet/.well-known/oauth-protected-resource"

Follow the resource_metadata URL to find the authorization server:

Terminal window
$ curl https://mcp.blender.bet/.well-known/oauth-protected-resource
{
"resource": "https://mcp.blender.bet/",
"authorization_servers": ["https://mcp.blender.bet"],
"scopes_supported": [],
"bearer_methods_supported": ["header"]
}

Follow the authorization_servers[0] URL to find the endpoints:

Terminal window
$ curl https://mcp.blender.bet/.well-known/oauth-authorization-server
{
"issuer": "https://mcp.blender.bet",
"authorization_endpoint": "https://mcp.blender.bet/authorize",
"token_endpoint": "https://mcp.blender.bet/token",
"registration_endpoint": "https://mcp.blender.bet/register",
"revocation_endpoint": "https://mcp.blender.bet/revoke",
"scopes_supported": [],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
"revocation_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
"code_challenge_methods_supported": ["S256"],
"client_id_metadata_document_supported": true
}

Three endpoints matter to a client: registration_endpoint, authorization_endpoint, token_endpoint.

Step 1 — Dynamic Client Registration (DCR)

Section titled “Step 1 — Dynamic Client Registration (DCR)”

Public clients register themselves at runtime. No advance coordination with the operator needed:

Terminal window
$ curl -X POST https://mcp.blender.bet/register \
-H 'content-type: application/json' \
-d '{
"client_name": "my-llm-agent",
"redirect_uris": ["http://127.0.0.1:51234/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"]
}'

Returns:

{
"client_id": "mcp-xxxxxxxx",
"client_name": "my-llm-agent",
"redirect_uris": ["http://127.0.0.1:51234/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"client_id_issued_at": 1716_000_000
}

token_endpoint_auth_method: "none" marks this as a public client — no client_secret, PKCE substitutes for it. Loopback redirect URIs (http://127.0.0.1:<port>/) are allowed per RFC 8252 for installed apps.

Generate a verifier and S256 challenge:

import secrets, hashlib, base64
verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).rstrip(b"=").decode()
state = secrets.token_urlsafe(16) # CSRF protection

Open the user’s browser to:

https://mcp.blender.bet/authorize?
response_type=code&
client_id=mcp-xxxxxxxx&
redirect_uri=http%3A%2F%2F127.0.0.1%3A51234%2Fcallback&
state=<state>&
code_challenge=<challenge>&
code_challenge_method=S256

The user authenticates against the upstream identity provider (Authentik in production, or the server’s in-memory provider in dev mode). On success, the browser is redirected back to your loopback redirect_uri:

http://127.0.0.1:51234/callback?code=<authorization_code>&state=<state>

Verify state matches what you sent. The code is single-use and short-lived (~60s).

Terminal window
$ curl -X POST https://mcp.blender.bet/token \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code' \
-d 'code=<authorization_code>' \
-d 'redirect_uri=http://127.0.0.1:51234/callback' \
-d 'client_id=mcp-xxxxxxxx' \
-d 'code_verifier=<verifier>'

Returns:

{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "<opaque>",
"token_type": "Bearer",
"expires_in": 28800,
"scope": ""
}

Use access_token as Authorization: Bearer <access_token> on every request to the MCP endpoint (https://mcp.blender.bet/).

When expires_in is close to elapsed, exchange the refresh token:

Terminal window
$ curl -X POST https://mcp.blender.bet/token \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'grant_type=refresh_token' \
-d 'refresh_token=<refresh_token>' \
-d 'client_id=mcp-xxxxxxxx'

Returns a new access_token (and usually a rotated refresh_token). Treat the new refresh token as canonical and discard the old one.

The server runs one of two providers, selected at boot via AUTH_BACKEND:

BackendWhat it doesWhen
authentikBridges to an upstream Authentik OIDC provider via OIDCProxy. Users authenticate with their Authentik credentials (SSO, federated identity, etc.).Production.
inmemoryBlenderMCPOAuthProvider — a complete MCP-spec OAuth 2.1 implementation backed by an in-process USERS dict. Useful for local dev and CI.Development / no Authentik.

Discovery URLs and endpoint shapes are identical between backends — the choice is transparent to consumers. The token format differs (Authentik issues RS256 JWTs with rich claims; inmemory issues opaque tokens), but consumers should treat both as opaque Bearer strings.

The server resolves a user_id from the access token and uses it as the bus partition key. All bus tools — register_client, send_message, list_available_clients, job_update, and the dispatch tools — operate exclusively on the bus owned by the resolved user. Users can’t see each other’s clients or job state. See OAuth and per-user buses for the isolation guarantees.

Status / ErrorCause
401 invalid_tokenToken expired, revoked, or never valid. Re-run the PKCE flow (or refresh if you still have a refresh token).
400 invalid_grantcode_verifier doesn’t match the code_challenge from step 2, OR the code was already used, OR redirect_uri mismatch.
400 invalid_clientclient_id not registered. Re-run DCR.
400 invalid_request (missing PKCE)Server requires PKCE; you sent response_type=code without code_challenge.