OAuth on mcp.ggui.ai
mcp.ggui.ai implements OAuth 2.1 with PKCE, Dynamic Client Registration (RFC 7591), and the Protected Resource Metadata discovery profile (RFC 9728) so MCP-Apps-aware hosts can connect without a manually-issued client_id.
This page describes the wire format. If you’re a user trying to connect a client (Claude Desktop, claude.ai, Goose, VS Code Copilot), you don’t need any of this — the host handles everything. Read on if you’re building a host, debugging a custom client, or operating an OSS deployment.
The flow at a glance
Section titled “The flow at a glance”Client Server User │ │ │ │── GET /mcp (no auth) ─────────→│ │ │← 401 + WWW-Authenticate ──────│ │ │ │ │ │── GET /.well-known/ │ │ │ oauth-protected-resource ───→│ │ │← { authorization_servers } ───│ │ │ │ │ │── GET /.well-known/ │ │ │ oauth-authorization-server ─→│ │ │← { authorize_endpoint, ... } ─│ │ │ │ │ │── POST /oauth/register ───────→│ (RFC 7591 DCR — no client_secret) │← { client_id } ───────────────│ │ │ │ │ │── open /oauth/authorize ──────→│── 302 → console.ggui.ai ──────→│ │ │ │── sign in + Approve │ │← form POST {api_key, params} ─│ │← 302 → redirect_uri?code=… ───│ │ │ │ │ │── POST /oauth/token │ │ │ {code, code_verifier} ──────→│ │ │← { access_token: ggui_user_* }│ │ │ │ │ │── GET /mcp │ │ │ Authorization: Bearer ─→│ │ │← MCP session ─────────────────│ │The access token IS the API key
Section titled “The access token IS the API key”The simplest possible bridge between MCP’s OAuth-required client UX and ggui’s existing API-key model:
- No parallel token table. The
access_tokenreturned at/oauth/tokenis verbatim aggui_user_*key minted byconsole.ggui.aiduring consent. - No translation in the request hot path. Every authenticated
/mcprequest looks identical to a CLI request —Authorization: Bearer ggui_user_*. - No refresh dance. The access token TTL equals the API key TTL. Revoke the key in the console → the client’s next request returns
401. Re-auth means re-running the OAuth ceremony, which is one paste. - One audit surface. Every connected client shows up as one row in your keys list, labelled with the client’s
client_namefrom DCR.
The trade-off: there’s no separate “this token came from OAuth” flag. The key works identically whether minted via console UI, the ggui CLI, or the OAuth flow. If that distinction matters for your operator policy, plug in your own OAuthStorage + AuthAdapter (see Custom storage below).
Endpoints
Section titled “Endpoints”GET /.well-known/oauth-protected-resource (RFC 9728)
Section titled “GET /.well-known/oauth-protected-resource (RFC 9728)”Tells the client where to find the authorization server. Same origin in our case — mcp.ggui.ai is both the resource and the auth server.
{ "resource": "https://mcp.ggui.ai/mcp", "authorization_servers": ["https://mcp.ggui.ai"], "bearer_methods_supported": ["header"], "resource_documentation": "https://modelcontextprotocol.io/extensions/apps/overview"}GET /.well-known/oauth-authorization-server (RFC 8414)
Section titled “GET /.well-known/oauth-authorization-server (RFC 8414)”Authorization-server metadata.
{ "issuer": "https://mcp.ggui.ai", "authorization_endpoint": "https://mcp.ggui.ai/oauth/authorize", "token_endpoint": "https://mcp.ggui.ai/oauth/token", "registration_endpoint": "https://mcp.ggui.ai/oauth/register", "response_types_supported": ["code"], "grant_types_supported": ["authorization_code"], "code_challenge_methods_supported": ["S256"], "token_endpoint_auth_methods_supported": ["none"], "scopes_supported": ["mcp"]}token_endpoint_auth_methods_supported: ["none"] is intentional — PKCE is the only supported client authentication, no client_secret is ever issued.
POST /oauth/register (RFC 7591)
Section titled “POST /oauth/register (RFC 7591)”Dynamic Client Registration. Issues a random client_id, no secret. Accepts arbitrary redirect_uris from the client without an allowlist — the trade-off matches the MCP spec’s pragmatism: any client willing to do PKCE + paste-key gets registered.
curl -X POST https://mcp.ggui.ai/oauth/register \ -H "Content-Type: application/json" \ -d '{ "redirect_uris": ["http://localhost:33418/callback"], "client_name": "Claude Desktop" }'{ "client_id": "mcp_client_AbCdEf...", "client_name": "Claude Desktop", "redirect_uris": ["http://localhost:33418/callback"], "token_endpoint_auth_method": "none"}GET /oauth/authorize
Section titled “GET /oauth/authorize”The user-facing approval entry point. Required query params:
| Param | Value |
|---|---|
response_type | code |
client_id | from DCR |
redirect_uri | one of the URIs registered with DCR |
code_challenge | base64url(SHA256(verifier)) |
code_challenge_method | S256 |
state | opaque, echoed back on redirect (CSRF defense) |
scope | mcp (only supported scope today) |
When the operator has configured a consentUrl (true on mcp.ggui.ai, where it points at console.ggui.ai/oauth/consent), the server 302s the browser there with every OAuth param forwarded plus an mcp_origin param the consent page uses to know where to POST back. When no consentUrl is configured (typical OSS posture), the server itself renders an unbranded paste-key HTML form.
POST /oauth/authorize (form-encoded)
Section titled “POST /oauth/authorize (form-encoded)”Called by the consent UI after the user approves. Same OAuth params echoed as hidden inputs, plus an api_key field carrying the freshly-minted (or pasted) ggui_user_* key. The server validates the key against the same AuthAdapter that gates /mcp, mints a 5-minute auth code, and 302s to the client’s redirect_uri?code=…&state=….
POST /oauth/token
Section titled “POST /oauth/token”Exchange the auth code for an access token.
curl -X POST https://mcp.ggui.ai/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code&code=AUTH_CODE&redirect_uri=http://localhost:33418/callback&client_id=mcp_client_…&code_verifier=ORIGINAL_VERIFIER"{ "access_token": "ggui_user_AbCdEf...", "token_type": "Bearer", "scope": "mcp"}No refresh_token, no expires_in — the access token’s lifetime is the underlying API key’s lifetime. Re-auth to get a new one.
OSS deployment notes
Section titled “OSS deployment notes”If you’re running @ggui-ai/mcp-server yourself and want to mount the OAuth surface, pass oauth: true to the server factory:
import { createGguiServer } from "@ggui-ai/mcp-server";
const server = createGguiServer({ // ... oauth: { issuerUrl: "https://your-mcp.example.com", consentUrl: "https://your-console.example.com/oauth/consent", // optional storage: new InMemoryOAuthStorage(), // or your own }, auth: yourAuthAdapter,});Storage seam
Section titled “Storage seam”Auth codes and DCR clients live in OAuthStorage. The default InMemoryOAuthStorage works fine for single-replica dev and any deployment with sticky sessions on the load balancer. For multi-replica deployments without sticky sessions, plug in a Redis or DynamoDB backend — the interface is two collections (auth codes, DCR clients) with put / consume / get methods. See packages/mcp-server/src/oauth.ts in the public repo for the full type.
Consent UI
Section titled “Consent UI”consentUrl delegates the user-facing approval step to a separate origin. The consent UI sees both the user’s session (whatever auth model it uses) and the freshly-minted API key in plaintext — same trust boundary as the MCP server itself. Validate the mcp_origin param against an allowlist before posting back, otherwise an attacker can craft a URL that exfiltrates a key to a third-party origin.
Without consentUrl, the server renders an in-process paste-key HTML form. Functional but unbranded — fine for OSS deployers who don’t want to stand up a separate origin.
Disable OAuth entirely
Section titled “Disable OAuth entirely”oauth: false (or simply omitting the option) skips the OAuth surface — /oauth/* and the well-known endpoints return 404, and /mcp falls back to the bearer-only AuthAdapter model. Hosts that don’t speak OAuth-DCR can still authenticate by setting a static Authorization: Bearer … header.
What this is NOT
Section titled “What this is NOT”- Not refresh-token compatible. The OAuth ceremony is one-time per credential. Add a refresh-token grant only if you also stop using the API key as the access token (separate token store + lifetime).
- Not OIDC. No ID tokens, no userinfo endpoint, no nonce. The MCP spec doesn’t ask for them, and the access-token-IS-the-API-key model has no need.
- Not a replacement for static API keys. If your client can hold a static
Authorization: Bearer ggui_user_*header (CLI agents, server-side runtimes), keep doing that — OAuth exists for hosts that need DCR + browser-mediated approval.