Skip to content

Commit 21dd0ea

Browse files
authored
Merge branch 'main' into dependabot/nuget/Microsoft.Extensions.AI-10.6.0
2 parents 7035aa4 + fd1ac08 commit 21dd0ea

22 files changed

Lines changed: 1521 additions & 31 deletions

File tree

docs/concepts/tasks/tasks.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ uid: tasks
1313
1414
The Model Context Protocol (MCP) supports [task-based execution] for long-running operations. Tasks enable a "call-now, fetch-later" pattern where clients can initiate operations that may take significant time to complete, then poll for status and retrieve results when ready.
1515

16-
[task-based execution]: https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks
16+
[task-based execution]: https://modelcontextprotocol.io/seps/1686-tasks
1717

1818
## Overview
1919

@@ -601,4 +601,4 @@ While this file-based approach demonstrates the pattern, production systems shou
601601
- <xref:ModelContextProtocol.InMemoryMcpTaskStore>
602602
- <xref:ModelContextProtocol.Protocol.McpTask>
603603
- <xref:ModelContextProtocol.Protocol.McpTaskStatus>
604-
- [MCP Tasks Specification](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks)
604+
- [MCP Tasks Specification](https://modelcontextprotocol.io/seps/1686-tasks)

docs/concepts/tools/tools.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,50 @@ Rules and constraints:
340340
- Values containing non-ASCII characters, control characters, or leading/trailing whitespace are Base64-encoded using the `=?base64?{value}?=` wrapper.
341341
- Header names must be case-insensitively unique within the tool's input schema.
342342
- Header validation is enforced only for protocol versions that support the HTTP Standardization feature (currently `DRAFT-2026-v1` and later).
343+
344+
### Pre-loading tool definitions on the client
345+
346+
By default, `Mcp-Param-*` headers are sent only for tools discovered via <xref:ModelContextProtocol.Client.McpClient.ListToolsAsync*>. If a client already has tool schema information (for example, from a previous session, hardcoded configuration, or an out-of-band source), it can pre-load those definitions so that headers are sent immediately—without a round trip to the server.
347+
348+
```csharp
349+
// Build the tool definition with x-mcp-header annotations
350+
var tool = new Tool
351+
{
352+
Name = "execute_sql",
353+
InputSchema = JsonDocument.Parse("""
354+
{
355+
"type": "object",
356+
"properties": {
357+
"region": {
358+
"type": "string",
359+
"x-mcp-header": "Region"
360+
},
361+
"query": {
362+
"type": "string"
363+
}
364+
}
365+
}
366+
""").RootElement.Clone(),
367+
};
368+
369+
// Pre-load the tool definition — no ListToolsAsync needed
370+
client.AddKnownTools([tool]);
371+
372+
// This call now sends an Mcp-Param-Region header automatically
373+
var result = await client.CallToolAsync("execute_sql",
374+
new Dictionary<string, object?> { ["region"] = "us-west-2", ["query"] = "SELECT 1" });
375+
```
376+
377+
Known tools survive <xref:ModelContextProtocol.Client.McpClient.ListToolsAsync*> cache clears—they remain in the cache even when the server's tool list is refreshed. If the server returns a tool with the same name, the server's definition overwrites the cached one, but the tool keeps its known status.
378+
379+
To remove known tools, use <xref:ModelContextProtocol.Client.McpClient.RemoveKnownTools*> for specific tools or <xref:ModelContextProtocol.Client.McpClient.ClearKnownTools*> to remove all:
380+
381+
```csharp
382+
// Remove specific known tools by name
383+
client.RemoveKnownTools(["execute_sql"]);
384+
385+
// Or remove all known tools at once
386+
client.ClearKnownTools();
387+
```
388+
389+
All tools passed to <xref:ModelContextProtocol.Client.McpClient.AddKnownTools*> are validated for correct `x-mcp-header` annotations. If any tool in the batch fails validation, an <xref:System.ArgumentException> is thrown and no tools are added (all-or-nothing).

docs/list-of-diagnostics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ If you use experimental APIs, you will get one of the diagnostics shown below. T
2323

2424
| Diagnostic ID | Description |
2525
| :------------ | :---------- |
26-
| `MCPEXP001` | Experimental APIs for features in the MCP specification itself, including Tasks and Extensions. Tasks provide a mechanism for asynchronous long-running operations that can be polled for status and results (see [MCP Tasks specification](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks)). Extensions provide a framework for extending the Model Context Protocol while maintaining interoperability (see [SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)). |
26+
| `MCPEXP001` | Experimental APIs for features in the MCP specification itself, including Tasks and Extensions. Tasks provide a mechanism for asynchronous long-running operations that can be polled for status and results (see [MCP Tasks specification](https://modelcontextprotocol.io/seps/1686-tasks)). Extensions provide a framework for extending the Model Context Protocol while maintaining interoperability (see [SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)). |
2727
| `MCPEXP002` | Experimental SDK APIs unrelated to the MCP specification itself, including subclassing `McpClient`/`McpServer` (see [#1363](https://github.com/modelcontextprotocol/csharp-sdk/pull/1363)) and `RunSessionHandler`, which may be removed or change signatures in a future release (consider using `ConfigureSessionOptions` instead). |
2828

2929
## Obsolete APIs

src/Common/McpHttpHeaders.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,12 @@ internal static class McpHttpHeaders
7575
/// </summary>
7676
public static bool SupportsStandardHeaders(string? protocolVersion)
7777
=> !string.IsNullOrEmpty(protocolVersion) && s_versionsWithStandardHeaders.Contains(protocolVersion!);
78+
79+
/// <summary>
80+
/// Returns <see langword="true"/> if the negotiated protocol version reports unresolvable
81+
/// resource URIs with the standard JSON-RPC <see cref="McpErrorCode.InvalidParams"/> (-32602)
82+
/// rather than the legacy <see cref="McpErrorCode.ResourceNotFound"/> (-32002).
83+
/// </summary>
84+
internal static bool UseInvalidParamsForMissingResource(string? protocolVersion)
85+
=> string.Equals(protocolVersion, MinVersionForStandardHeaders, StringComparison.Ordinal);
7886
}

src/ModelContextProtocol.Core/Authentication/ClientOAuthOptions.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,40 @@ public sealed class ClientOAuthOptions
3535
public Uri? ClientMetadataDocumentUri { get; set; }
3636

3737
/// <summary>
38-
/// Gets or sets the OAuth scopes to request.
38+
/// Gets or sets the OAuth scopes to request as a fallback.
3939
/// </summary>
4040
/// <remarks>
4141
/// <para>
42-
/// When specified, these scopes will be used instead of the scopes advertised by the protected resource.
43-
/// If not specified, the provider will use the scopes from the protected resource metadata.
42+
/// These scopes are used only when the server does not provide scope information via the
43+
/// WWW-Authenticate header or Protected Resource Metadata (<c>scopes_supported</c>). This
44+
/// matches the MCP scope selection strategy: WWW-Authenticate scope → PRM scopes_supported →
45+
/// client-configured scopes → omit scope parameter.
4446
/// </para>
4547
/// <para>
46-
/// Common OAuth scopes include "openid", "profile", and "email".
48+
/// To filter or customize scopes when the server <em>does</em> provide scope information,
49+
/// use <see cref="ScopeSelector"/> instead.
4750
/// </para>
4851
/// </remarks>
4952
public IEnumerable<string>? Scopes { get; set; }
5053

54+
/// <summary>
55+
/// Gets or sets a delegate that selects or filters the OAuth scopes to request.
56+
/// </summary>
57+
/// <remarks>
58+
/// <para>
59+
/// When set, this delegate is called after the MCP scope selection strategy has determined the
60+
/// candidate scopes (WWW-Authenticate → PRM <c>scopes_supported</c> → <see cref="Scopes"/> fallback)
61+
/// and after <c>offline_access</c> has been automatically appended when advertised by the
62+
/// authorization server. The return value replaces the candidate scopes in the authorization request.
63+
/// </para>
64+
/// <para>
65+
/// Use this to request only a subset of the scopes offered by the server, or to append a custom
66+
/// scope that is not advertised in the server metadata. Return <see langword="null"/> or an empty
67+
/// enumerable to omit the <c>scope</c> parameter entirely.
68+
/// </para>
69+
/// </remarks>
70+
public ScopeSelectorDelegate? ScopeSelector { get; set; }
71+
5172
/// <summary>
5273
/// Gets or sets the authorization redirect delegate for handling the OAuth authorization flow.
5374
/// </summary>

src/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ internal sealed partial class ClientOAuthProvider : McpHttpClient
2828
private readonly Uri _serverUrl;
2929
private readonly Uri _redirectUri;
3030
private readonly string? _configuredScopes;
31+
private readonly ScopeSelectorDelegate? _scopeSelector;
3132
private readonly IDictionary<string, string> _additionalAuthorizationParameters;
3233
private readonly Func<IReadOnlyList<Uri>, Uri?> _authServerSelector;
3334
private readonly AuthorizationRedirectDelegate _authorizationRedirectDelegate;
@@ -76,6 +77,7 @@ public ClientOAuthProvider(
7677
_clientSecret = options.ClientSecret;
7778
_redirectUri = options.RedirectUri ?? throw new ArgumentException("ClientOAuthOptions.RedirectUri must configured.", nameof(options));
7879
_configuredScopes = options.Scopes is null ? null : string.Join(" ", options.Scopes);
80+
_scopeSelector = options.ScopeSelector;
7981
_additionalAuthorizationParameters = options.AdditionalAuthorizationParameters;
8082
_clientMetadataDocumentUri = options.ClientMetadataDocumentUri;
8183

@@ -491,7 +493,7 @@ private Uri BuildAuthorizationUrl(
491493
queryParamsDictionary["resource"] = resourceUri;
492494
}
493495

494-
var scope = GetScopeParameter(protectedResourceMetadata);
496+
var scope = ComputeEffectiveScope(protectedResourceMetadata, authServerMetadata);
495497
if (!string.IsNullOrEmpty(scope))
496498
{
497499
queryParamsDictionary["scope"] = scope!;
@@ -653,7 +655,7 @@ private async Task PerformDynamicClientRegistrationAsync(
653655
TokenEndpointAuthMethod = "client_secret_post",
654656
ClientName = _dcrClientName,
655657
ClientUri = _dcrClientUri?.ToString(),
656-
Scope = GetScopeParameter(protectedResourceMetadata),
658+
Scope = ComputeEffectiveScope(protectedResourceMetadata, authServerMetadata),
657659
};
658660

659661
var requestBytes = JsonSerializer.SerializeToUtf8Bytes(registrationRequest, McpJsonUtilities.JsonContext.Default.DynamicClientRegistrationRequest);
@@ -712,6 +714,20 @@ private async Task PerformDynamicClientRegistrationAsync(
712714
private static string? GetResourceUri(ProtectedResourceMetadata protectedResourceMetadata)
713715
=> protectedResourceMetadata.Resource;
714716

717+
private string? ComputeEffectiveScope(
718+
ProtectedResourceMetadata protectedResourceMetadata,
719+
AuthorizationServerMetadata authServerMetadata)
720+
{
721+
var scope = GetScopeParameter(protectedResourceMetadata);
722+
scope = AugmentScopeWithOfflineAccess(scope, authServerMetadata);
723+
if (_scopeSelector is not null)
724+
{
725+
var selected = _scopeSelector(scope?.Split(' '));
726+
scope = selected is not null ? string.Join(" ", selected) : null;
727+
}
728+
return scope;
729+
}
730+
715731
private string? GetScopeParameter(ProtectedResourceMetadata protectedResourceMetadata)
716732
{
717733
if (!string.IsNullOrEmpty(protectedResourceMetadata.WwwAuthenticateScope))
@@ -726,6 +742,37 @@ private async Task PerformDynamicClientRegistrationAsync(
726742
return _configuredScopes;
727743
}
728744

745+
/// <summary>
746+
/// Augments the scope parameter with <c>offline_access</c> if the authorization server advertises it in
747+
/// <c>scopes_supported</c> and it is not already present. This signals to OIDC-flavored authorization servers
748+
/// that the client desires a refresh token, per SEP-2207.
749+
/// </summary>
750+
private static string? AugmentScopeWithOfflineAccess(string? scope, AuthorizationServerMetadata authServerMetadata)
751+
{
752+
const string OfflineAccess = "offline_access";
753+
754+
if (authServerMetadata.ScopesSupported?.Contains(OfflineAccess) is not true)
755+
{
756+
return scope;
757+
}
758+
759+
if (scope is null)
760+
{
761+
return OfflineAccess;
762+
}
763+
764+
// Check if offline_access is already in the scope string (space-separated tokens).
765+
foreach (var token in scope.Split(' '))
766+
{
767+
if (token == OfflineAccess)
768+
{
769+
return scope;
770+
}
771+
}
772+
773+
return scope + " " + OfflineAccess;
774+
}
775+
729776
/// <summary>
730777
/// Verifies that the resource URI in the metadata exactly matches the original request URL as required by the RFC.
731778
/// Per RFC: The resource value must be identical to the URL that the client used to make the request to the resource server.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
namespace ModelContextProtocol.Authentication;
3+
4+
/// <summary>
5+
/// Represents a method that selects or filters the OAuth scopes to request during authorization.
6+
/// </summary>
7+
/// <param name="scope">
8+
/// The scopes determined by the MCP scope selection strategy (WWW-Authenticate header scope →
9+
/// <c>scopes_supported</c> from Protected Resource Metadata → <see cref="ClientOAuthOptions.Scopes"/>
10+
/// fallback), with <c>offline_access</c> appended when advertised by the authorization server. May be
11+
/// <see langword="null"/> if the server provided no scope information and no fallback scopes are configured.
12+
/// </param>
13+
/// <returns>
14+
/// The scopes to include in the authorization and Dynamic Client Registration requests. Return
15+
/// <see langword="null"/> or an empty enumerable to omit the <c>scope</c> parameter entirely.
16+
/// </returns>
17+
/// <remarks>
18+
/// <para>
19+
/// Use this delegate to filter or customize the proposed scopes before the authorization request is made.
20+
/// Common scenarios include:
21+
/// </para>
22+
/// <list type="bullet">
23+
/// <item><description>Requesting only a subset of the scopes offered by the server.</description></item>
24+
/// <item><description>Appending a custom scope not advertised in the server metadata.</description></item>
25+
/// </list>
26+
/// <para>
27+
/// The MCP specification defines the following scope selection priority (highest to lowest):
28+
/// WWW-Authenticate header scope → PRM <c>scopes_supported</c> → omit scope parameter. The
29+
/// <paramref name="scope"/> parameter already reflects this priority. The delegate runs after
30+
/// <c>offline_access</c> has been auto-appended, so it can also remove that scope if desired.
31+
/// </para>
32+
/// <para>
33+
/// The resolved scope is applied consistently to both the authorization URL and the Dynamic Client
34+
/// Registration (DCR) request, so the registered client scope matches what is actually requested.
35+
/// </para>
36+
/// </remarks>
37+
public delegate IEnumerable<string>? ScopeSelectorDelegate(IReadOnlyCollection<string>? scope);

src/ModelContextProtocol.Core/Client/McpClient.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,84 @@ protected McpClient()
7070
/// </para>
7171
/// </remarks>
7272
public abstract Task<ClientCompletionDetails> Completion { get; }
73+
74+
/// <summary>
75+
/// Registers one or more tool definitions in the client's tool cache, enabling the transport
76+
/// to send <c>Mcp-Param-*</c> headers for those tools without requiring a prior <see cref="McpClient.ListToolsAsync(RequestOptions?, CancellationToken)"/> call.
77+
/// </summary>
78+
/// <param name="tools">The tool definitions to register.</param>
79+
/// <remarks>
80+
/// <para>
81+
/// This method allows callers who already have tool schema information (e.g., from a previous session,
82+
/// hardcoded configuration, or an out-of-band source) to provide it directly to the client. Once registered,
83+
/// any <see cref="McpClient.CallToolAsync(string, IReadOnlyDictionary{string, object?}?, IProgress{ProgressNotificationValue}?, RequestOptions?, CancellationToken)"/>
84+
/// call for a registered tool will automatically include <c>Mcp-Param-*</c> HTTP headers based on
85+
/// the tool's <c>x-mcp-header</c> schema annotations, exactly as if the tool had been discovered
86+
/// via <see cref="McpClient.ListToolsAsync(RequestOptions?, CancellationToken)"/>.
87+
/// </para>
88+
/// <para>
89+
/// <b>Cache interaction behavior:</b>
90+
/// <list type="bullet">
91+
/// <item>Registered tools are added to the same internal tool cache used by <see cref="McpClient.ListToolsAsync(RequestOptions?, CancellationToken)"/>.</item>
92+
/// <item>Calling <see cref="McpClient.ListToolsAsync(RequestOptions?, CancellationToken)"/> after <see cref="AddKnownTools"/> preserves
93+
/// manually registered tools — only server-discovered tools are cleared and repopulated.</item>
94+
/// <item>If the server returns a tool with the same name as a manually registered tool, the server's
95+
/// definition overwrites the registered one in the cache, but the tool retains its known status
96+
/// and will survive subsequent cache clears. This registration is sticky for the lifetime of the
97+
/// <see cref="McpClient"/>; use <see cref="RemoveKnownTools"/> or <see cref="ClearKnownTools"/> to
98+
/// explicitly drop known tools that are no longer needed.</item>
99+
/// <item>Tools can be registered at any time — before or after <see cref="McpClient.ListToolsAsync(RequestOptions?, CancellationToken)"/>,
100+
/// and across multiple calls.</item>
101+
/// <item>Re-registering a tool with the same name overwrites the previous definition in the cache (last write wins).</item>
102+
/// </list>
103+
/// </para>
104+
/// <para>
105+
/// Tools with invalid <c>x-mcp-header</c> annotations cause an <see cref="ArgumentException"/> to be thrown.
106+
/// No tools are added to the cache if any tool in the batch fails validation (all-or-nothing).
107+
/// </para>
108+
/// </remarks>
109+
/// <exception cref="ArgumentNullException"><paramref name="tools"/> is <see langword="null"/>.</exception>
110+
/// <exception cref="ArgumentException">One or more tools have invalid <c>x-mcp-header</c> annotations.</exception>
111+
public virtual void AddKnownTools(IEnumerable<Tool> tools)
112+
{
113+
Throw.IfNull(tools);
114+
throw new NotSupportedException($"{GetType().Name} does not support adding known tools.");
115+
}
116+
117+
/// <summary>
118+
/// Removes one or more previously registered tool definitions from the client's tool cache by name.
119+
/// </summary>
120+
/// <param name="toolNames">The names of the tools to remove.</param>
121+
/// <remarks>
122+
/// <para>
123+
/// This removes the specified tools from both the known-tools set and the internal tool cache.
124+
/// After removal, those tools will no longer survive <see cref="McpClient.ListToolsAsync(RequestOptions?, CancellationToken)"/>
125+
/// cache clears, and <c>Mcp-Param-*</c> headers will no longer be sent for them unless the server
126+
/// re-discovers them via <see cref="McpClient.ListToolsAsync(RequestOptions?, CancellationToken)"/>.
127+
/// </para>
128+
/// <para>
129+
/// Removing a tool name that was not previously added via <see cref="AddKnownTools"/> is a no-op.
130+
/// </para>
131+
/// </remarks>
132+
/// <exception cref="ArgumentNullException"><paramref name="toolNames"/> is <see langword="null"/>.</exception>
133+
public virtual void RemoveKnownTools(IEnumerable<string> toolNames)
134+
{
135+
Throw.IfNull(toolNames);
136+
throw new NotSupportedException($"{GetType().Name} does not support removing known tools.");
137+
}
138+
139+
/// <summary>
140+
/// Removes all previously registered tool definitions from the client's tool cache.
141+
/// </summary>
142+
/// <remarks>
143+
/// <para>
144+
/// This clears all tools that were added via <see cref="AddKnownTools"/> from both the known-tools
145+
/// set and the internal tool cache. Server-discovered tools that are not also known tools are not affected
146+
/// and will remain in the cache until the next <see cref="McpClient.ListToolsAsync(RequestOptions?, CancellationToken)"/> call.
147+
/// </para>
148+
/// </remarks>
149+
public virtual void ClearKnownTools()
150+
{
151+
throw new NotSupportedException($"{GetType().Name} does not support clearing known tools.");
152+
}
73153
}

0 commit comments

Comments
 (0)