feat: MCP 2025-11-25 spec compliance + real-world OAuth#2
feat: MCP 2025-11-25 spec compliance + real-world OAuth#2bowlofarugula wants to merge 9 commits intomasterfrom
Conversation
Bring murl into compliance with the MCP 2025-11-25 specification for Streamable HTTP transport, authorization, and lifecycle. Authorization (auth.py): - Add Protected Resource Metadata discovery (RFC 9728) with well-known URI fallback and WWW-Authenticate header extraction - Add Authorization Server Metadata discovery with OIDC Discovery fallback (RFC 8414 + OpenID Connect) - Include resource parameter in auth and token requests (RFC 8707) - Parse WWW-Authenticate headers for resource_metadata, scope, and error parameters - Verify PKCE S256 support before proceeding with authorization - Add scope selection strategy (WWW-Authenticate > resource metadata) - Maintain backwards compatibility via legacy discovery fallback Transport/Lifecycle (cli.py): - Check server capabilities after initialization before dispatching requests (lifecycle MUST requirement) - Extract WWW-Authenticate headers from actual HTTP 401/403 responses instead of string-matching exception messages - Handle 403 insufficient_scope with step-up re-authorization - Support arbitrary resource URI schemes (not just file://) with -d uri=... override Tests: 106 passing (up from ~80), covering all new discovery functions, WWW-Authenticate parsing, capability checking, 403 step-up flow, and resource URI handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement OAuth Client ID Metadata Documents per draft-ietf-oauth-client-id-metadata-document-00, the MCP spec's preferred client registration mechanism. When an authorization server advertises client_id_metadata_document_supported: true in its metadata, murl now skips Dynamic Client Registration and instead uses a hosted metadata document URL as its client_id. This avoids the need for a registration endpoint and follows the spec's recommended priority order. Changes: - Add docs/oauth/client-metadata.json for hosting on GitHub Pages - Add CIMD-aware registration in authorize(): checks for client_id_metadata_document_supported, uses fixed callback port (19362) to match pre-registered redirect URIs in the metadata doc - Fall back to Dynamic Client Registration when CIMD is not supported - Improve error message when neither CIMD nor DCR is available Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This reverts commit c2a3b13.
…gured OAuth Support pre-registered OAuth credentials as an alternative to Dynamic Client Registration, matching the pattern used by Claude Code. When --client-id is provided, DCR is skipped entirely. Without --client-id and no registration endpoint advertised, a clear error directs users to provide credentials. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two spec compliance fixes found during review: 1. refresh_token() now includes the RFC 8707 `resource` parameter in token refresh requests. MCP 2025-11-25 requires it in all token requests to maintain audience binding. Gracefully omitted for legacy creds that predate the resource_uri field. 2. _canonical_resource_uri() no longer adds a synthetic trailing slash for bare hostnames. The spec says implementations SHOULD use the form without trailing slash unless semantically significant. Also adds spec-reference comments (MCP 2025-11-25 section citations) throughout auth.py and cli.py for discovery, PKCE, scope selection, client registration priority, and resource parameter handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tools/list, resources/list, and prompts/list now follow nextCursor to collect all pages instead of silently returning only the first page. Servers with many tools/resources/prompts will now return complete results. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three issues discovered testing against a live Okta-backed AgentCore server: 1. OIDC PKCE tolerance: Okta's OIDC discovery metadata omits code_challenge_methods_supported (it's an OAuth 2.0 AS Metadata field, not OIDC). We now warn and proceed with S256 when metadata comes from an OIDC endpoint, rather than refusing outright. RFC 8414 sources still enforce the strict check. 2. Redirect URI uses localhost instead of 127.0.0.1: IDPs do exact string matching on redirect_uri. Most register http://localhost:<port>/callback, not the IP form. 3. Added --scope flag: Okta requires an explicit scope when the auth server has no configured defaults. Also added a 401 probe (_probe_www_authenticate) so --login and expired-token paths get the WWW-Authenticate header for proper RFC 9728 discovery on servers with complex URLs. Verified end-to-end against live Bedrock AgentCore + Okta OAuth. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughThe changes update the OAuth authentication flow and CLI to support MCP 2025-11-25 compliance. New discovery functions were added to handle RFC 9728 resource metadata and RFC 8414/OIDC metadata endpoints. The Comment |
The MCP SDK logs a warning when the server returns 404 on session DELETE, which is valid per MCP 2025-11-25 — 404 means the session is already gone. Suppress by setting the SDK transport logger to ERROR level. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@murl/auth.py`:
- Around line 493-496: The redirect_uri uses "localhost" but
_run_callback_server() binds to "127.0.0.1", causing mismatched host/address
families; update _run_callback_server() to bind to the same advertised host (use
the same host variable used to build redirect_uri, e.g., "localhost") or make it
listen on both IPv4 and IPv6 loopback (both 127.0.0.1 and ::1) so the browser
callback reaches the server; ensure you reference and reuse the same host/port
values when creating the HTTPServer/listen socket in _run_callback_server() to
keep redirect_uri and the bound listener consistent.
- Around line 427-447: The code currently wraps both resource metadata discovery
and auth-server discovery in one try/except, causing failures in
discover_auth_server_metadata(issuer_url) to fall back to
discover_metadata(server_url) incorrectly; instead split the logic: call
discover_resource_metadata(server_url, resource_metadata_url) inside a try that
on OAuthError sets auth_server_meta = discover_metadata(server_url) (this
handles "resource metadata unavailable"), but if resource discovery succeeds
then extract issuer_url and call discover_auth_server_metadata(issuer_url)
outside that except (so any OAuthError from discover_auth_server_metadata
propagates and does not silently fallback); update references to
auth_server_meta and issuer_url accordingly (functions:
discover_resource_metadata, discover_auth_server_metadata, discover_metadata;
variables: resource_meta, issuer_url, auth_server_meta).
In `@murl/cli.py`:
- Around line 268-308: The pagination loops for session.list_tools,
session.list_resources and session.list_prompts trust result.nextCursor and can
hang on repeated tokens; modify each loop to track seen cursors (e.g., seen =
set()) and a sane max page count (e.g., MAX_PAGES = 1000), and before assigning
cursor = result.nextCursor check if nextCursor is None or nextCursor in seen or
pages >= MAX_PAGES and break if so; add seen.add(nextCursor) and increment pages
each iteration so all_items accumulation and the return lines (the model_dump
calls) remain unchanged.
- Around line 587-597: Only trigger re-authorization on a 403 when the
WWW-Authenticate header explicitly indicates "insufficient_scope" and avoid
passing scope twice by merging any parsed scope into auth_kwargs before calling
authorize; specifically, when is_403 and www_auth_header exists, call
parse_www_authenticate(www_auth_header), check that its error or params indicate
insufficient_scope, set scope_to_request = www_params.get("scope") if present,
then if scope_to_request is set update auth_kwargs["scope"] = scope_to_request
(overriding any CLI-provided scope) and finally call authorize(base_url,
www_authenticate=www_auth_header, **auth_kwargs) instead of passing scope as a
separate positional/keyword argument.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 82a0c276-6a55-4aa7-85e8-e8bd8607c6a4
📒 Files selected for processing (4)
murl/auth.pymurl/cli.pytests/test_auth.pytests/test_cli.py
- Split discovery try/except so auth server metadata failures propagate when the issuer was explicitly advertised via RFC 9728 (only fall back to legacy discovery when resource metadata itself is unavailable) - Bind callback server to "localhost" instead of "127.0.0.1" to match the redirect_uri host (fixes IPv6-first machines where ::1 != 127.0.0.1) - Add pagination loop guards: track seen cursors + max page limit (1000) to prevent infinite loops from buggy servers - Fix 403 handler: only re-auth on explicit insufficient_scope error, and merge scope into auth_kwargs to avoid duplicate kwarg TypeError Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Addressed all four CodeRabbit findings in 69b419d:
|
turlockmike
left a comment
There was a problem hiding this comment.
Three-lens review: security, Unix design, test quality.
Overall: Strong work. The PKCE implementation is correct, spec traceability is excellent, and the discovery chain composition is clean. Several items worth addressing before merge.
Cross-reviewer themes:
-
Discovery chain URL validation —
resource_metadataURLs from WWW-Authenticate headers andauthorization_serversentries are followed without origin validation. A malicious MCP endpoint can chainresource_metadata→authorization_serversto redirect the entire OAuth flow through an attacker-controlled server. RFC 9728 §3 requires the metadata URL to be under the same origin — validate hostname matches before following. -
_CallbackHandlerclass-level state — All three reviewers flagged this independently.auth_code,auth_error, andexpected_stateare class attributes, not instance attributes. Ifauthorize()is called concurrently (or twice in the same process on 401 retry), both flows write to the same class slots. The reset at lines 331-333 patches this but is load-bearing. Use closures orserver.datadict. Test suite also never exercises_CallbackHandlerdirectly — state mismatch, error parameter, and no-code paths are untested. -
Pagination duplication — The cursor-based pagination loop (
all_items,cursor,seen_cursors,for _ in range(MAX_PAGES)) appears three times verbatim for tools/resources/prompts. Extract a_collect_all(fetch_page, extract_items)helper. When you fix the cursor cycle guard, fix it in one place. -
--client-secretin process args — Visible inps aux,/proc/<pid>/cmdline, and shell history. SupportMURL_CLIENT_SECRETenv var or file-based input as an alternative.
Additional observations:
_probe_www_authenticatefires unconditionally on--login— gate it behind discovery failure to avoid unnecessary round-tripsdiscover_metadatais kept for backwards compat but duplicatesdiscover_auth_server_metadata's fallback chain with one difference: silent default endpoint synthesis. Name that behavior explicitly or consolidate.- Token exchange error messages include raw server response body — can leak internal issuer details to stderr/logs
- Test assertions on URL call ordering (
test_success) and_discovery_sourceinternal tag couple to implementation details rather than observable behavior - Missing test for
discover_resource_metadatanetwork error path (except httpx.HTTPError: continue) - Pagination tests share 20+ lines of identical async scaffolding — extract to a pytest fixture
test_cli_401_retry_triggers_oauthimports the realmake_mcp_requestinside a mock side effect — return a concrete result instead to avoid recursive risk- Deferred
import clickandimport socketinsideauthorize()save nothing — both already loaded; move to top of file - Bare
raiseinside 403 handler atcli.py:605re-raises correctly but the intent is unclear — add a comment like# Not a scope challenge — propagate as network error
What's good: PKCE S256 done correctly (secrets.token_urlsafe(64), proper SHA-256 + no-padding base64url), PKCE enforcement distinguishes RFC 8414 vs OIDC metadata (the Okta tolerance is a good call), 403 handling correctly gates on insufficient_scope, resource indicators in both auth and token exchange, capability negotiation in the right place, spec comment traceability throughout. The _discovery_source tag carrying provenance through the data is a nice touch.
— Mike's AI Clone 🧬
Summary
Brings murl's OAuth and transport handling into compliance with the MCP 2025-11-25 specification, and fixes real-world issues discovered testing against a live Okta-backed Bedrock AgentCore server.
Authorization (auth.py)
resource_metadatafrom WWW-Authenticate header, path-aware and root well-known URI fallbackcode_challenge_methods_supportedis absent (with tolerance for OIDC endpoints that omit it, e.g. Okta)resourceparameter in authorization, token exchange, and refresh requests--client-id,--client-secret,--callback-port,--scopeflags (Claude Code pattern, skips DCR)localhost, not127.0.0.1--loginpath — ensures proper RFC 9728 discovery on servers with complex URLsTransport & Lifecycle (cli.py)
insufficient_scopefrom WWW-Authenticate, re-authorizes with required scopestools/list,resources/list,prompts/listfollownextCursorto collect all pagesTesting
Spec references
Code comments throughout cite specific MCP 2025-11-25 sections and RFCs for traceability.
Test plan
pytest tests/test_auth.py tests/test_cli.py)--client-id,--client-secret,--callback-port,--scope) verified against Okta IDP🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
--client-id,--client-secret,--callback-port, and--scopeImprovements