Skip to content

feat(mcp): MCP OAuth 2.1 client for tool servers#1196

Open
thotam wants to merge 1 commit into
nextlevelbuilder:devfrom
thotam:feat/mcp-oauth-v3
Open

feat(mcp): MCP OAuth 2.1 client for tool servers#1196
thotam wants to merge 1 commit into
nextlevelbuilder:devfrom
thotam:feat/mcp-oauth-v3

Conversation

@thotam

@thotam thotam commented Jun 9, 2026

Copy link
Copy Markdown

Summary

Adds a complete MCP OAuth 2.1 authorization flow so agents can call MCP tool servers that require user-delegated access (RFC 9728 discovery → 8414/OIDC metadata → 7591 DCR → PKCE S256 / refresh / client_credentials), with encrypted token storage, per-user vs global isolation, SSRF-safe outbound calls, and automatic cleanup of stale tokens when a server's URL or OAuth config changes.

Type

  • Feature
  • Bug fix
  • Hotfix (targeting main)
  • Refactor
  • Docs
  • CI/CD

Target Branch

dev

Checklist

  • go build ./... passes
  • go build -tags sqliteonly ./... passes (if Go changes)
  • go vet ./... passes
  • Tests pass: go test -race ./...-race unavailable locally (no cgo/gcc on Windows dev box). Ran without -race: internal/mcp/oauth, internal/http, internal/gateway, internal/agent all pass. (Two pre-existing store/pg TestBuildSkillInfo* failures are Windows path-separator only, unrelated to this PR; CI runs -race on Linux.)
  • Web UI builds: cd ui/web && pnpm build
  • No hardcoded secrets or credentials (tokens AES-256-GCM encrypted at rest)
  • SQL queries use parameterized $1, $2 (PG) / ? (SQLite) — no string concat
  • New user-facing strings added to all 3 locales (en/vi/zh) — internal/i18n catalogs + ui/web/src/i18n/locales/{en,vi,zh}
  • Migration version bumped in internal/upgrade/version.go (RequiredSchemaVersion → 74; new migration 000074_mcp_oauth_tokens)

Test Plan

Automated

  • internal/mcp/oauth/*_test.go — discovery fallback + 5-min cache, PKCE S256 math, RFC 7591 DCR, refresher (cache/expiry/refresh, per-user vs global, InvalidateServer).
  • internal/http/mcp_oauth_test.go — all 5 endpoints, admin gating, WS mcp.oauth_complete event, callback HTML.
  • internal/http/mcp_update_oauth_purge_test.go — tokens purged on URL change & OAuth-config change; NOT purged on unrelated update.
  • tests/integration/v3_mcp_oauth_* — store CRUD + AES-256-GCM round-trip + tenant isolation, DeleteServerOAuthTokens, E2E start → callback → DB via httptest.
  • internal/gateway/event_filter_test.gomcp.oauth_complete routed only to initiating user / in-tenant admins, fail-closed across tenants.
  • internal/agent/loop_mcp_user_test.go — Bearer token injection + graceful expiry + 401 cache purge.

Manual

  • Configure an OAuth-protected MCP server (DCR and manual client_id), authorize via popup → token stored encrypted, agent calls succeed.
  • client_credentials grant completes server-side (no popup) and reports authorized.
  • Change server URL / client_id → status flips to "not authorized", old token removed from mcp_oauth_tokens, pool reconnects.
  • goclaw migrate up applies 000074 cleanly on PostgreSQL; SQLite desktop build starts.

Notes for reviewers

  • Single squashed commit (605d1e69); 64 files, +6712/−126.
  • Security surface: all outbound HTTP (discovery/DCR/token/refresh) goes through the SSRF-safe client with pinned IPs + response size limits; OAuth status/revoke are admin-gated; callback page builds its postMessage payload via json.Marshal (no reflected XSS from error_description).

@thotam thotam force-pushed the feat/mcp-oauth-v3 branch 2 times, most recently from b736263 to c836588 Compare June 9, 2026 08:51
Implements a complete MCP OAuth 2.1 authorization flow for tool servers that
require user-delegated access, covering all layers from DB to UI.

## Core OAuth package (internal/mcp/oauth/)
- discovery.go: RFC 9728 protected-resource → RFC 8414 AS metadata → OIDC
  fallback chain with 5-min in-memory cache and InvalidateCache()
- dcr.go: RFC 7591 Dynamic Client Registration with response size guard
- flow.go: PKCE (S256) authorization code flow — StartFlow(), ExchangeCode(),
  ClientCredentials(), auto-cleanup of expired flows; carries AS issuer through
  PendingFlow for status display
- refresher.go: OAuthTokenProvider with in-memory token cache, automatic refresh
  on expiry, per-user vs global slot isolation, InvalidateCache/InvalidateServer

## Database
- migrations/000074 + SQLite schema: mcp_oauth_tokens with AES-256-GCM encrypted
  access/refresh tokens, partial unique index for global vs per-user rows,
  ON DELETE CASCADE from mcp_servers

## Store layer
- store.MCPOAuthTokenStore: Upsert, Get/GetUser, Delete/DeleteUser, and
  DeleteServerOAuthTokens (purge all rows for a server)
- PostgreSQL + SQLite implementations

## HTTP handler (internal/http/mcp_oauth.go) — 5 endpoints
- POST   /v1/mcp/oauth/start      — discovery + optional DCR + PKCE redirect URL;
  client_credentials completes server-side (no redirect) and returns completed=true
- GET    /v1/mcp/oauth/callback   — exchange code, persist token, publish WS event;
  payload built via json.Marshal (no reflected XSS via error_description)
- GET    /v1/mcp/oauth/status/{id}, DELETE /v1/mcp/oauth/token/{id} — admin-gated
- POST   /v1/mcp/oauth/discover/{id} — on-demand discovery probe
- All outbound calls go through the SSRF-safe client with pinned IPs

## Gateway / WebSocket
- pkg/protocol/mcp_events.go: EventMCPOAuthComplete routed only to the initiating
  user (admins in-tenant included); fail-closed across tenants

## Agent loop
- getUserMCPTools() injects Authorization: Bearer from OAuthTokenProvider; on a
  401 for OAuth servers it purges the cached token so the next turn re-resolves

## Stale-token cleanup on reconfigure
- handleUpdateServer purges all OAuth tokens (global + per-user), drops the
  refresher cache, and evicts the pool when a server's URL or OAuth config
  (client_id / endpoints / grant_type / scope / auth_type) changes — so the
  status UI and agent never use a token minted for the old resource/AS

## Web UI (ui/web/)
- MCPOAuthDialog (WS-driven), unified user-credentials dialog, OAuth settings
  fields; handles the no-redirect client_credentials completion

## Tests
- internal/mcp/oauth/*_test.go: discovery cache, PKCE, DCR, refresher
- internal/http/mcp_oauth_test.go + mcp_update_oauth_purge_test.go: routes, auth
  gating, WS event, purge-on-URL/OAuth-config-change
- tests/integration: store + encryption + tenant isolation, E2E start→callback,
  DeleteServerOAuthTokens
- internal/gateway/event_filter_test.go, internal/agent/loop_mcp_user_test.go
@thotam thotam force-pushed the feat/mcp-oauth-v3 branch from c836588 to b49b268 Compare June 9, 2026 10:22
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