Skip to content

Add ScopeSelectorDelegate to enhance OAuth options for scope filtering#1596

Open
halllo wants to merge 3 commits into
modelcontextprotocol:mainfrom
halllo:scope_selector
Open

Add ScopeSelectorDelegate to enhance OAuth options for scope filtering#1596
halllo wants to merge 3 commits into
modelcontextprotocol:mainfrom
halllo:scope_selector

Conversation

@halllo
Copy link
Copy Markdown
Contributor

@halllo halllo commented May 22, 2026

Add a ScopeSelector hook to ClientOAuthOptions for customizing OAuth scopes

Motivation and Context

The MCP authorization spec defines a strict scope selection priority: WWW-Authenticate header scope → PRM scopes_supported → omit scope parameter. Clients sometimes need to request only a subset of the scopes a server advertises (e.g. a client that only supports mcp:tools but the server lists ten scopes), or to append a scope not in the server metadata (e.g. a non-standard scope required by a specific deployment).

The existing Scopes property on ClientOAuthOptions was documented as an override but only ever acted as a last-resort fallback — consistent with the TypeScript and Python SDKs. That misleading documentation has been corrected. The new ScopeSelector delegate is the intended extension point for clients that need to influence scope selection when the server does provide scope information.

This closes out the use-case from #1238 and #1236 without violating the spec's priority order.

How Has This Been Tested?

Five integration tests were added to AuthTests.cs, covering all input/output combinations of the delegate:

  • Non-null input → filtered subset (client keeps only mcp:tools from a two-scope server)
  • Non-null input → superset (client appends a custom scope not advertised by the server)
  • Null input — server advertises no scopes and AS does not include offline_access; delegate receives null
  • Return nullscope query parameter is absent from the authorization URL
  • Return empty enumerablescope query parameter is absent from the authorization URL

All tests run end-to-end against the in-process test OAuth server on net8.0, net9.0, and net10.0.

Breaking Changes

None. The behavior of Scopes (last-resort fallback) is unchanged; only its documentation was corrected to match the actual behavior and the spec.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Delegate signature:

public delegate IEnumerable<string>? ScopeSelectorDelegate(IEnumerable<string>? scope);

The delegate receives the scope list after the full MCP priority logic and after offline_access has been auto-appended (per #1479), so it can also strip that scope if the client opts out. Returning null or an empty enumerable omits the scope parameter from the authorization request entirely.

The selector is intentionally not applied to the scope hint sent during Dynamic Client Registration — DCR scope is advisory and the authorization request scope is what matters for token issuance.

Comment thread src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs
Comment thread src/ModelContextProtocol.Core/Authentication/ScopeSelectorDelegate.cs Outdated
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.

2 participants