Skip to content

Commit f8f235c

Browse files
Merge branch 'main' into feature/sep-2106-output-schema-relaxation
2 parents 614f19f + 712a06b commit f8f235c

5 files changed

Lines changed: 152 additions & 4 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/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/ModelContextProtocol.Core/Authentication/ClientOAuthProvider.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@ private Uri BuildAuthorizationUrl(
492492
}
493493

494494
var scope = GetScopeParameter(protectedResourceMetadata);
495+
scope = AugmentScopeWithOfflineAccess(scope, authServerMetadata);
495496
if (!string.IsNullOrEmpty(scope))
496497
{
497498
queryParamsDictionary["scope"] = scope!;
@@ -726,6 +727,37 @@ private async Task PerformDynamicClientRegistrationAsync(
726727
return _configuredScopes;
727728
}
728729

730+
/// <summary>
731+
/// Augments the scope parameter with <c>offline_access</c> if the authorization server advertises it in
732+
/// <c>scopes_supported</c> and it is not already present. This signals to OIDC-flavored authorization servers
733+
/// that the client desires a refresh token, per SEP-2207.
734+
/// </summary>
735+
private static string? AugmentScopeWithOfflineAccess(string? scope, AuthorizationServerMetadata authServerMetadata)
736+
{
737+
const string OfflineAccess = "offline_access";
738+
739+
if (authServerMetadata.ScopesSupported?.Contains(OfflineAccess) is not true)
740+
{
741+
return scope;
742+
}
743+
744+
if (scope is null)
745+
{
746+
return OfflineAccess;
747+
}
748+
749+
// Check if offline_access is already in the scope string (space-separated tokens).
750+
foreach (var token in scope.Split(' '))
751+
{
752+
if (token == OfflineAccess)
753+
{
754+
return scope;
755+
}
756+
}
757+
758+
return scope + " " + OfflineAccess;
759+
}
760+
729761
/// <summary>
730762
/// Verifies that the resource URI in the metadata exactly matches the original request URL as required by the RFC.
731763
/// Per RFC: The resource value must be identical to the URL that the client used to make the request to the resource server.

tests/ModelContextProtocol.AspNetCore.Tests/OAuth/AuthTests.cs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,4 +1261,108 @@ public async Task CanAuthenticate_WithLegacyServerUsingDefaultEndpointFallback()
12611261
await using var client = await McpClient.CreateAsync(
12621262
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
12631263
}
1264+
1265+
[Fact]
1266+
public async Task AuthorizationFlow_AppendsOfflineAccess_WhenServerAdvertisesIt()
1267+
{
1268+
TestOAuthServer.IncludeOfflineAccessInMetadata = true;
1269+
await using var app = await StartMcpServerAsync();
1270+
1271+
string? requestedScope = null;
1272+
1273+
await using var transport = new HttpClientTransport(new()
1274+
{
1275+
Endpoint = new(McpServerUrl),
1276+
OAuth = new()
1277+
{
1278+
ClientId = "demo-client",
1279+
ClientSecret = "demo-secret",
1280+
RedirectUri = new Uri("http://localhost:1179/callback"),
1281+
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
1282+
{
1283+
var query = QueryHelpers.ParseQuery(uri.Query);
1284+
requestedScope = query["scope"].ToString();
1285+
return HandleAuthorizationUrlAsync(uri, redirect, ct);
1286+
},
1287+
},
1288+
}, HttpClient, LoggerFactory);
1289+
1290+
await using var client = await McpClient.CreateAsync(
1291+
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
1292+
1293+
Assert.NotNull(requestedScope);
1294+
Assert.Contains("offline_access", requestedScope!.Split(' '));
1295+
}
1296+
1297+
[Fact]
1298+
public async Task AuthorizationFlow_DoesNotAppendOfflineAccess_WhenServerDoesNotAdvertiseIt()
1299+
{
1300+
// IncludeOfflineAccessInMetadata defaults to false, so the AS will not advertise offline_access.
1301+
await using var app = await StartMcpServerAsync();
1302+
1303+
string? requestedScope = null;
1304+
1305+
await using var transport = new HttpClientTransport(new()
1306+
{
1307+
Endpoint = new(McpServerUrl),
1308+
OAuth = new()
1309+
{
1310+
ClientId = "demo-client",
1311+
ClientSecret = "demo-secret",
1312+
RedirectUri = new Uri("http://localhost:1179/callback"),
1313+
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
1314+
{
1315+
var query = QueryHelpers.ParseQuery(uri.Query);
1316+
requestedScope = query["scope"].ToString();
1317+
return HandleAuthorizationUrlAsync(uri, redirect, ct);
1318+
},
1319+
},
1320+
}, HttpClient, LoggerFactory);
1321+
1322+
await using var client = await McpClient.CreateAsync(
1323+
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
1324+
1325+
Assert.NotNull(requestedScope);
1326+
Assert.DoesNotContain("offline_access", requestedScope!.Split(' '));
1327+
}
1328+
1329+
[Fact]
1330+
public async Task AuthorizationFlow_DoesNotDuplicateOfflineAccess_WhenAlreadyPresent()
1331+
{
1332+
TestOAuthServer.IncludeOfflineAccessInMetadata = true;
1333+
1334+
// Configure the PRM to already include offline_access in its scopes.
1335+
Builder.Services.Configure<McpAuthenticationOptions>(McpAuthenticationDefaults.AuthenticationScheme, options =>
1336+
{
1337+
options.ResourceMetadata!.ScopesSupported = ["mcp:tools", "offline_access"];
1338+
});
1339+
1340+
await using var app = await StartMcpServerAsync();
1341+
1342+
string? requestedScope = null;
1343+
1344+
await using var transport = new HttpClientTransport(new()
1345+
{
1346+
Endpoint = new(McpServerUrl),
1347+
OAuth = new()
1348+
{
1349+
ClientId = "demo-client",
1350+
ClientSecret = "demo-secret",
1351+
RedirectUri = new Uri("http://localhost:1179/callback"),
1352+
AuthorizationRedirectDelegate = (uri, redirect, ct) =>
1353+
{
1354+
var query = QueryHelpers.ParseQuery(uri.Query);
1355+
requestedScope = query["scope"].ToString();
1356+
return HandleAuthorizationUrlAsync(uri, redirect, ct);
1357+
},
1358+
},
1359+
}, HttpClient, LoggerFactory);
1360+
1361+
await using var client = await McpClient.CreateAsync(
1362+
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
1363+
1364+
Assert.NotNull(requestedScope);
1365+
var scopeTokens = requestedScope!.Split(' ');
1366+
Assert.Single(scopeTokens, t => t == "offline_access");
1367+
}
12641368
}

tests/ModelContextProtocol.TestOAuthServer/Program.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ public Program(ILoggerProvider? loggerProvider = null, IConnectionListenerFactor
7878
/// </remarks>
7979
public bool ExpectResource { get; set; } = true;
8080

81+
/// <summary>
82+
/// Gets or sets a value indicating whether the authorization server advertises support for
83+
/// <c>offline_access</c> in its <c>scopes_supported</c> metadata. This simulates an OIDC-flavored
84+
/// authorization server that issues refresh tokens when the client requests the <c>offline_access</c> scope.
85+
/// </summary>
86+
/// <remarks>
87+
/// The default value is <c>false</c>.
88+
/// </remarks>
89+
public bool IncludeOfflineAccessInMetadata { get; set; }
90+
8191
public HashSet<string> DisabledMetadataPaths { get; } = new(StringComparer.OrdinalIgnoreCase);
8292
public IReadOnlyCollection<string> MetadataRequests => _metadataRequests.ToArray();
8393

@@ -188,7 +198,9 @@ IResult HandleMetadataRequest(HttpContext context, string? issuerPath = null)
188198
ResponseTypesSupported = ["code"],
189199
SubjectTypesSupported = ["public"],
190200
IdTokenSigningAlgValuesSupported = ["RS256"],
191-
ScopesSupported = ["openid", "profile", "email", "mcp:tools"],
201+
ScopesSupported = IncludeOfflineAccessInMetadata
202+
? ["openid", "profile", "email", "mcp:tools", "offline_access"]
203+
: ["openid", "profile", "email", "mcp:tools"],
192204
TokenEndpointAuthMethodsSupported = ["client_secret_post"],
193205
ClaimsSupported = ["sub", "iss", "name", "email", "aud"],
194206
CodeChallengeMethodsSupported = ["S256"],

0 commit comments

Comments
 (0)