OAuth 2.1 client for Model Context Protocol servers (spec 2025-06-18).
Flow — on HTTP 401 from an MCP server, we:
WWW-Authenticate: Bearer resource_metadata="..." (RFC 9728),
fall back to ${origin}/.well-known/oauth-protected-resource;authorization_servers[0];.well-known/oauth-authorization-server (RFC 8414) or
.well-known/openid-configuration for endpoints + capabilities;client_id;provider.flow, the SAME lifecycle used by model-provider authentication.
The initiating client opens the browser and returns directly to its gateway;~/.vis/mcp-tokens/<server>.edn;com.blockether.vis.internal.provider.oauth/make-file-refresher.The returned bearer-fn is a 0/1-arg function: 0-arg yields the current
bearer token; 1-arg (with the token the server just rejected) forces a
refresh. It never opens a browser and never waits — with nothing to refresh
it throws :mcp/oauth-required, which callers turn into sign in.
OAuth 2.1 client for Model Context Protocol servers (spec `2025-06-18`).
Flow — on HTTP 401 from an MCP server, we:
1. read `WWW-Authenticate: Bearer resource_metadata="..."` (RFC 9728),
fall back to `${origin}/.well-known/oauth-protected-resource`;
2. GET the resource-metadata JSON → pick an `authorization_servers[0]`;
3. GET its `.well-known/oauth-authorization-server` (RFC 8414) or
`.well-known/openid-configuration` for endpoints + capabilities;
4. dynamic-client-register (RFC 7591) if the AS supports it, or use
the caller-supplied `client_id`;
5. prepare an allowed callback and register it, then hand the adapter to
`provider.flow`, the SAME lifecycle used by model-provider authentication.
The initiating client opens the browser and returns directly to its gateway;
6. exchange code → access + refresh tokens; persist to
`~/.vis/mcp-tokens/<server>.edn`;
7. on later expiry / 401, refresh the token single-flight through
`com.blockether.vis.internal.provider.oauth/make-file-refresher`.
The returned `bearer-fn` is a 0/1-arg function: 0-arg yields the current
bearer token; 1-arg (with the token the server just rejected) forces a
refresh. It never opens a browser and never waits — with nothing to refresh
it throws `:mcp/oauth-required`, which callers turn into `sign in`.(cancel-authorization! flow-id)Forget an MCP attempt and release its callback worker.
Forget an MCP attempt and release its callback worker.
(complete-authorization! flow-id input)Validate and complete via the shared engine; app flows require the full callback.
Validate and complete via the shared engine; app flows require the full callback.
(forget! server-name)Drop persisted tokens for server-name after sign-out or a revoked refresh grant.
Drop persisted tokens for `server-name` after sign-out or a revoked refresh grant.
(make-bearer-fn server-name server-url www-auth-atom)Build a 0/1-arg fn returning the current Bearer token string for server-name.
0-arg yields the persisted token; 1-arg (the token the server just rejected)
forces a single-flight refresh. server-url is the MCP endpoint and
www-auth-atom is the atom the HTTP transport keeps pointed at the latest
WWW-Authenticate header, so a refusal can carry the LIVE 401 details.
It never runs an interactive authorization. Every caller sits on a path a
human is waiting on - a tool call, a reconcile tick, the health loop - and the
browser dance blocked each of them for up to five minutes on a server nobody
had signed into yet, which looked exactly like a hung daemon. With nothing to
refresh it throws :mcp/oauth-required immediately; authorizing is the
explicit headless flow below, which hands back a URL instead of waiting for
one.
Build a 0/1-arg fn returning the current Bearer token string for `server-name`. 0-arg yields the persisted token; 1-arg (the token the server just rejected) forces a single-flight refresh. `server-url` is the MCP endpoint and `www-auth-atom` is the atom the HTTP transport keeps pointed at the latest `WWW-Authenticate` header, so a refusal can carry the LIVE 401 details. It never runs an interactive authorization. Every caller sits on a path a human is waiting on - a tool call, a reconcile tick, the health loop - and the browser dance blocked each of them for up to five minutes on a server nobody had signed into yet, which looked exactly like a hung daemon. With nothing to refresh it throws `:mcp/oauth-required` immediately; authorizing is the explicit headless flow below, which hands back a URL instead of waiting for one.
(poll-authorization! flow-id)Read the common retained browser/app verdict.
Read the common retained browser/app verdict.
(start-authorization! server-name
server-url
{:keys [www-auth auth-hint callback-mode]})Discover/register the MCP protocol adapter; the shared engine owns its lifecycle. App returns go directly through the paired client, never through a relay.
Discover/register the MCP protocol adapter; the shared engine owns its lifecycle. App returns go directly through the paired client, never through a relay.
(token-status server-name)Non-secret view of the persisted OAuth tokens for server-name. Never returns
the token itself: a client may render it, and a client is never trusted with
credentials.
Non-secret view of the persisted OAuth tokens for `server-name`. Never returns the token itself: a client may render it, and a client is never trusted with credentials.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |