ClientOAuthProvider.Scopes have priority again (#1236)#1238
Conversation
|
I think we should update the doc comments to indicate that The November version of the MCP spec added a "Scope Selection Strategy" we now implement.
Do we know what the other SDK's do here? |
|
I asked Copilot what the TS and Python SDK do, and it appears the client-developer-provided scopes are only used as a fallback.
@halllo Can you explain the scenario where you need to override the scopes explicitly requested by the MCP server? Is this a scenario that works with any clients that do not use the MCP C# SDK? |
|
That is very interesting, because I find it counter-intuitive that the values I explicitly provide are not used. Alternatively additional integration points (e.g. custom scope selection) would also help me to have more control over what scopes are actually used. |
|
What do you think about this? private string? GetScopeParameter(ProtectedResourceMetadata protectedResourceMetadata)
{
var scopeParameter = default(string?);
if (!string.IsNullOrEmpty(protectedResourceMetadata.WwwAuthenticateScope))
{
scopeParameter = protectedResourceMetadata.WwwAuthenticateScope;
}
else if (protectedResourceMetadata.ScopesSupported.Count > 0)
{
scopeParameter = string.Join(" ", protectedResourceMetadata.ScopesSupported);
}
else
{
scopeParameter = _configuredScopes;
}
if (_customScopeSelector is not null)
{
scopeParameter = _customScopeSelector.Invoke(scopeParameter);
}
return scopeParameter;
}Then |
|
As a workaround I manipulate the scope query parameter of the authorization URL before I redirect the user. var httpClientTransport = new HttpClientTransport(new()
{
...
OAuth = new()
{
...
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
},
}, http);
static async Task<string?> HandleAuthorizationUrlAsync(Uri authUrl, Uri redirectUri, CancellationToken cancellationToken)
{
static Uri changeScopes(Uri url, Func<string[], string[]> adjustScopes)
{
return new Uri(Regex.Replace(url.ToString(), @"(?<=&scope=)(?<scopes>[^&]+)", m =>
{
var scopes = m.Groups["scopes"].Value;
return string.Join('+', adjustScopes(scopes.Split('+', StringSplitOptions.RemoveEmptyEntries)));
}));
}
var newAuthUrl = changeScopes(authUrl, scopes => [.. scopes, "offline_access"]);
Console.WriteLine($"Starting OAuth authorization flow at {newAuthUrl}");
...
}This works as long as the library does not use pushed authorization requests. |
|
I just merged #1479, which implements SEP-2207's That leaves the "my client only supports a subset of PRM scopes" case. I don't think a priority inversion is the right fix for it: The MCP Scope Selection Strategy says priority is WWW-Authenticate → PRM scopes_supported → omit. The TS and Python SDKs both treat client-configured scopes as fallback only. Having C# silently disagree would be a real interop hazard. A general-purpose MCP client usually shouldn't be narrowing PRM-advertised scopes — the spec's guidance is that requesting all advertised scopes lets the AS and end user decide via consent. If a specific client genuinely needs to filter scopes, your My preference is to:
Does that work for you? I'd be happy to take a small doc-only PR from you but definitely don't feel obligated. I can do that myself too. |
|
I think always automatically adding the I agree with the interoperability argument. My workaround with |
|
Thanks! I see #1596 is already up — appreciate you getting on it quickly. On the |
I adjusted the priority of scope determination. Scopes specified via
ClientOAuthOptions.Scopeshave priority over scopes from PRM.Motivation and Context
When the client specifies Scopes via the ClientOAuthOptions, these scopes are used before the scopes of the PRM, just as the xml comment states
For example this is needed if a client only supports a subset of PRM scopes or wants to add the
offline_accessscope.How Has This Been Tested?
I added test method.
Breaking Changes
No. Unless they unexpectedly rely on
ClientOAuthOptions.Scopesnot having an effect in the presence of PRM scopes.Types of changes
Checklist
Additional context
This addresses #1236