Repository navigation
v10.4.0 — MCP v4: OAuth 2.1 + GraphRAG Stability
[10.4.0] - 2026-06-22
Closes the v10.4 milestone — MCP v4: OAuth 2.1 + GraphRAG Stability (7 phases, 64–70; 16/16 requirements; milestone audit passed). Agent Brain can now run remotely behind OAuth 2.1 on the Streamable HTTP transport — in both a co-located AS/RS single-binary shape and a split AS/RS shape backed by an external IdP (validated against Keycloak-in-CI). Bugs were fixed first: the kuzu SIGSEGV (#178) and graph under-reporting / stale-snapshot issues (#184). Auth is off by default (AGENT_BRAIN_AUTH=none); nothing changes for existing local/loopback users unless they opt in.
Security
- OAuth 2.1 for the remote MCP (Streamable HTTP) transport (
agent-brain-mcp/agent_brain_mcp/oauth/*,oauth_metadata.py,http.py,config.py,server.py; closes #188). Gated byAGENT_BRAIN_AUTH(nonedefault /basicLAN shared-secret bridge /oauth), exclusive by construction with a boot-time startup gate (exit 2 on an invalid mode or an empty/scheme-less/fragmentedAGENT_BRAIN_OAUTH_RESOURCE). The design was filed and passed an independent adversarial security review before any implementation code landed (docs/plans/2026-06-14-mcp-v4-oauth-design.md), which found and closed 7 gaps (DNS-rebinding SSRF post-resolution IP check, empty-resource startup gate, import-time scope drift guard, PKCE-plainrejection,0o600token file, all-mode token-termination contract,/mcp/subscriptionsexemption audit) at design time.- Co-located Authorization Server + Resource Server. Authorization-code flow with PKCE S256-only (
plain/missing challenge actively rejected with HTTP 400 at/authorize), RS256 JWT minting (PyJWT[crypto], 15-min access / rotating 30-day refresh) + a/.well-known/jwks.jsonendpoint, wired through the SDKOAuthAuthorizationServerProvider. The RS verifies signature,exp/nbf(30 s leeway),iss, andaudand returns 401 +WWW-Authenticateon any failure. - Public discovery root, reachable with no token. Protected Resource Metadata (RFC 9728) at
/.well-known/oauth-protected-resource(+ path-suffixed variant) and Authorization Server Metadata (RFC 8414, advertisingcode_challenge_methods_supported: ["S256"]) at/.well-known/oauth-authorization-server, mounted above the/mcpmount so Starlette first-match keeps them served (HTTP 200) in every mode even withRequireAuthMiddlewarewired. - Per-tool scope enforcement. Four scopes (
agent-brain:read|index|admin|subscribe) mapped to all 16 MCP tools in a single source of truth co-located with_tool_matrix.py, guarded by a fail-loud import-time drift check so the server refuses to start if a tool is added without a scope. A pre-dispatchScopeEnforcementMiddlewarereturns a real HTTP 403insufficient_scope(distinct from a 401 missing/invalid token) for under-scoped calls. - Resource Indicators (RFC 8707) + confused-deputy prevention. The client sends
resourcein authorize + token requests; the AS bindsaudto the canonical resource URI and the RS validates it. The client's OAuth token never reaches the MCP→REST leg, which continues to useX-API-Keyexclusively — asserted by a dedicatedtest_oauth_confused_deputy.pythat checks the token string is absent from every upstream header. - Client registration. CIMD (Client ID Metadata Documents) + static pre-registration, with a full SSRF stack on metadata fetches (domain allowlist + unconditional private/loopback/link-local block + DNS-rebinding post-resolution
getaddrinfore-check + 5 s timeout). - Split AS/RS (external IdP).
build_verifier()selectsJwksTokenVerifier(remote JWKS viaPyJWKClient,kid-miss refresh + TTL,asyncio.to_thread) →IntrospectionTokenVerifier(RFC 7662,active:false→ 401) →LocalRs256Verifierbehind one stableverify_token() -> AccessToken | Noneseam, so topology is a config swap. Validated end-to-end against a live Keycloak ≥22 container in CI. Token revocation (RFC 7009) is supported via introspection plus ajtidenylist on the co-located store.
- Co-located Authorization Server + Resource Server. Authorization-code flow with PKCE S256-only (
Added
McpHttpBackendclient-side OAuth dance (agent-brain-mcp/agent_brain_mcp/oauth/oauth_client.py,oauth_handlers.py,token_storage.py,client.py; OAUTH-07). The CLI transparently handles a 401 +WWW-Authenticatechallenge → PRM/OASM discovery → PKCE dance via the SDKOAuthClientProvider, opening a browser once and persisting tokens in aFileTokenStorage(state_dir/mcp-oauth-tokens.json, chmod0o600). Subsequent Pattern A per-call invocations reuse the cached token (silent refresh on expiry, no re-prompt). Opt-in viaAGENT_BRAIN_MCP_AUTH=oauth(default OFF → byte-identical no-auth path).GET /mcp/subscriptionsdebug endpoint (agent-brain-mcp/agent_brain_mcp/http.py,server.py; HOUSE-01, closes #194). Exposes live subscription state (session IDs, subscribed URIs, uptime) for operator diagnosis, closing the v10.2 VAL-02 deferral where disconnect-cleanup tests fell back to stderr-scraping.agent-brain graph restore-from-snapshot [--snapshot PATH] [--dry-run]+agent-brain doctorstale-graph detection (server + CLI; GSTAB-02). Replays the latest kuzu snapshot from disk after anAGENT_BRAIN_JOB_TIMEOUTrollback, anddoctornow surfaces the stale-graph condition as a WARN instead of reportingOK.
Fixed
- Sustained GraphRAG indexing with
graphrag.store_type: kuzuno longer crashes the server with SIGSEGV (agent-brain-server/agent_brain_server/indexing/graph_index.py,storage/graph_errors.py,services/indexing_service.py; GSTAB-01, closes #178). Graph writes now run in an out-of-processmultiprocessingspawn subprocess (build_from_documents_isolated()); a kuzu-native crash surfaces as a structuredGraphBuildFailedErrorand the indexing job degrades gracefully (leaves the jobCOMPLETEDwith vector + BM25 intact) instead of taking down the server process. Thesimplestore remains the documented fallback. /health/statusgraphentity_count/relationship_countnow match kuzu's actual contents (agent-brain-server/agent_brain_server/api/routers/health.py,indexing/graph_index.py; GSTAB-03, closes #184). Counts are derived from a live kuzuCOUNT(*)at query time (TTL-cached, with a stale fallback) — the0 / 100vs real5677 / 4366under-reporting from a drifting stored counter is gone.
Known issues
- Keycloak-in-CI E2E (
agent-brain-mcp/tests/test_oauth_keycloak_e2e.py) is currently red — the realm/client bootstrap returns400at the Keycloak token endpoint, failing all 8 opt-in external-IdP E2E tests at setup. This is a CI test-harness bootstrap issue, not a defect in the shipped OAuth code: the unit + contract suite (1021 tests) passes andagent_brain_mcp/oauth/holds ≥90 % coverage (90.53 %) behind a binding CI gate. Tracked as follow-up tech debt; theMCP Keycloak Integrationcheck is non-blocking.
Deferred
- Public RFC 7009
/revokeroute → v10.4.1 (revocation currently rides on introspection + thejtidenylist; the OAUTH-12 DoD is met). DPoP (RFC 9449) token binding is forced-deferred — no production-grade Python library exists as of June 2026; it is optional in the MCP core spec. Structured audit logging of authorized calls is its own future milestone.