Skip to content

MCP: sign in from claude.ai with OAuth - #200

Merged
Maselkov merged 7 commits into
mainfrom
mcp-oauth
Nov 4, 2026
Merged

Maselkov merged 7 commits into
mainfrom
mcp-oauth

Conversation

@Maselkov

@Maselkov Maselkov commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Stacked on #199, which is stacked on #198. Merge #198, then #199, then this. Until they're in, this diff also shows their commits. The base is main on purpose, so this can't end up merged into a stacked branch.

claude.ai, the Claude apps and Claude Code can now connect to /mcp by URL and sign in with OAuth, without anyone handing them a token. The server is its own authorization server, built on the access tokens users already have.

The flow

  1. /mcp without a token gets a 401 with WWW-Authenticate: Bearer resource_metadata=".../.well-known/oauth-protected-resource". claude.ai only starts signing in on a 401.
  2. Discovery: the protected resource metadata (RFC 9728, also served at /.well-known/oauth-protected-resource/mcp) names this server, and /.well-known/oauth-authorization-server (RFC 8414) lists the endpoints. These and the token and registration endpoints send Access-Control-Allow-Origin: * for browser-based clients.
  3. Registration (/oauth/register, RFC 7591): public clients only. Redirect URIs must be https, or http to a loopback address. Claude registers on every new connection, so registrations never used are dropped after a day, with at most 200 an hour. CIMD isn't supported; the metadata doesn't advertise it, so claude.ai falls back to registering.
  4. Consent (/oauth/authorize): a plain HTML page in the site's style.
    • A signed-out user first signs in with their access token.
    • It names the app, the user and their role, and where it will send them back. A loopback callback gets a warning that it's an app on this computer.
    • Allow and Cancel are guarded by a value tied to the session, on top of the existing same-origin check.
    • The page has its own CSP: form-action must include the app's callback origin, because Chrome applies form-action to the redirect after a form POST.
    • Unknown clients and unregistered redirect URIs get an error page, never a redirect.
  5. Tokens (/oauth/token, form-encoded):
    • The code works once, lasts 60 s, and needs the PKCE S256 verifier, the same redirect URI and the same client.
    • Access tokens (gcmat_…) last 1 h; refresh tokens (gcmrt_…) last 30 days.
    • Refresh tokens rotate, and one reused revokes the grant.
    • Every failure answers invalid_grant, as claude.ai expects.
    • The authorization response includes iss (RFC 9207).

Loopback redirect URIs match on any port, which Claude Code needs.

Revocation

A grant remembers the login token the user consented with. Revoking that token, New token, or deleting the user disconnects the app. Admins see each user's apps in Manage users ("App: Claude · connected … · used …") with a Disconnect button. The new route is POST /api/admin/apps/<id>/revoke.

Storage

A new app.db migration adds oauth_clients, oauth_codes, oauth_grants and oauth_tokens. Codes and tokens are stored as SHA-256 hashes. Grants and codes cascade when their user is deleted.

Consent page

Dark and light at phone width, checked in headless Chrome:

Connect E2E Assistant?
E2E Assistant wants to act as Administrator (admin) on this server. It will be able to read the base: items, crafts, power and stock rules.
You'll be sent back to an app on this computer (127.0.0.1). Only allow it if you started connecting it just now. An admin can disconnect it in Manage users.
[Cancel] [Allow]

Testing

  • New tests/test_oauth.py (40 tests): discovery and PUBLIC_URL; registration, including bad redirect URIs, confidential clients, the hourly cap and dropping unused clients; consent (signing in, allow, deny, a forged form, a cross-origin POST, unknown client or redirect, the four errors sent back to the client, loopback any-port); tokens (the full flow onto /mcp, a code used once, wrong verifier, wrong redirect or client, expiry, refresh rotation and replay revoking the grant); revocation by login token, New token, user deletion and admin Disconnect.
  • New e2e step: register an app, click Allow on the consent page in Chrome, trade the code, call /mcp, see the app in Manage users, Disconnect it, and check the token stops working.
  • The full checklist passes: pytest (987), node --test, e2e (55), node --check, Lua (on 5.4), ruff, luacheck and shellcheck.
  • Not tested: claude.ai itself. It needs a public HTTPS address. The plan is to add a gtnh.network staging instance as a custom connector once this is deployed there.

🤖 Generated with Claude Code

Maselkov and others added 3 commits November 4, 2026 21:48
The MCP endpoint coming next must check what the page's routes check,
so the logic moves into functions both can call:

- commands.request_cancel(): the busy-CPU and same-job checks. The job
  identity helpers (output_identity, same_output) move with it from
  tracking, which imports commands, not the other way round.
- commands.craft_request_error(): the craft request's input checks.
- stock.item_from(), set_alert(), set_target() and rules(): stock rule
  validation, the craftable check and the rules listing.
- planner.rules_for_user(): the stock rules a plan warns about.

No change to what the routes accept or answer, except that a craft
request's amount can no longer be true (which counted as 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/mcp is a Model Context Protocol endpoint, so Claude Code or another MCP
client can answer questions about the base. Eleven read-only tools:
item search, item details and history, inventory changes, current
crafts, a job's held-up steps, craft history and stats, the crafting
plan, stock rules and power.

- Hand-written JSON-RPC over Streamable HTTP (gcm/mcp/protocol.py): one
  JSON answer per POST, no sessions. It serves both protocol eras: the
  2026-07-28 stateless requests (version in _meta, mirrored headers
  checked) and initialize-based 2025-03-26..2025-11-25 clients.
- Signed in with the user's personal access token as a bearer token
  (auth.bearer_required), never a session cookie; another site's Origin
  is refused.
- Tools take item keys from search_items, never names, and compact their
  output for a model's context.
- docs: an AI assistants section in using.md, plus README, server.md,
  how-it-works.md and CONTEXT.md. dev.py prints the MCP address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude.ai's connectors only sign in with OAuth, so the server is now its
own authorization server for /mcp:

- /mcp's 401 points at the protected resource metadata (RFC 9728),
  which names this server's authorization server metadata (RFC 8414).
- Clients register themselves (RFC 7591, public clients only, unused
  ones dropped after a day, capped per hour).
- /oauth/authorize is a consent page: sign in with your access token,
  see which app asks and where it sends you back, Allow or Cancel. Its
  own CSP lets the form's redirect reach the app's callback.
- /oauth/token trades the code (PKCE S256 only) for an hour's access
  token and a 30-day refresh token. Refresh tokens rotate; one used
  twice revokes the grant.
- Loopback redirect URIs match on any port, for Claude Code.
- Manage users lists each user's connected apps with Disconnect.
  Revoking the login token they consented with, New token or deleting
  the user disconnects them too.
- docs: using.md (claude.ai and Claude Code setup), server.md
  (PUBLIC_URL, proxy paths), CONTEXT.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Maselkov and others added 4 commits November 4, 2026 22:30
- A tool that raises anything but ToolError answers as a tool error
  (and is logged) instead of a 500 the client can't read, and a KeyError
  inside a tool no longer reads as "Unknown tool".
- Numbers in arguments must be finite: JSON's 1e400 is infinity to
  Python, which int() turned into a 500.
- inventory_changes builds the snapshot's lookup once instead of
  scanning it for every changed item.
- search_items ranks by the name without Minecraft's colour codes, and
  tool output drops them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The consent page only takes POSTs from its own page (Origin and
  Sec-Fetch-Site): another site could post a sign-in with its own token
  and sign the visitor in as someone else (login CSRF).
- A refresh token used again within 30 s of its first use gets tokens
  instead of revoking the grant: that's two requests refreshing at once
  or a retry after a lost answer, not a copied token.
- Registrations are capped per address (30 an hour) as well as in all
  (2000), so one script can't stop everyone else connecting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Maselkov
Maselkov merged commit f870f19 into main Nov 4, 2026
8 checks passed
Maselkov added a commit that referenced this pull request Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant