diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index ae4cab4fb..ede10d127 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -23,8 +23,8 @@ If you use experimental APIs, you will get one of the diagnostics shown below. T | Diagnostic ID | Description | | :------------ | :---------- | -| `MCPEXP001` | MCP experimental APIs 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)). | -| `MCPEXP002` | Subclassing `McpClient` and `McpServer` is experimental and subject to change (see [#1363](https://github.com/modelcontextprotocol/csharp-sdk/pull/1363)). | +| `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)). | +| `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). | ## Obsolete APIs diff --git a/samples/EverythingServer/Program.cs b/samples/EverythingServer/Program.cs index c508db99f..99d7759bd 100644 --- a/samples/EverythingServer/Program.cs +++ b/samples/EverythingServer/Program.cs @@ -51,6 +51,7 @@ .WithHttpTransport(options => { // Add a RunSessionHandler to remove all subscriptions for the session when it ends +#pragma warning disable MCPEXP002 // RunSessionHandler is experimental options.RunSessionHandler = async (httpContext, mcpServer, token) => { if (mcpServer.SessionId == null) @@ -76,6 +77,7 @@ subscriptions.TryRemove(mcpServer.SessionId, out _); } }; +#pragma warning restore MCPEXP002 }) .WithTools() .WithTools() diff --git a/src/Common/Experimentals.cs b/src/Common/Experimentals.cs index d9cfe42e8..7e7e969bb 100644 --- a/src/Common/Experimentals.cs +++ b/src/Common/Experimentals.cs @@ -6,10 +6,24 @@ namespace ModelContextProtocol; /// Defines diagnostic IDs, messages, and URLs for APIs annotated with . /// /// +/// Experimental diagnostic IDs are grouped by category: +/// +/// +/// MCPEXP001 covers APIs related to experimental features in the MCP specification itself, +/// such as Tasks and Extensions. These APIs may change as the specification evolves. +/// +/// +/// MCPEXP002 covers experimental SDK APIs that are unrelated to the MCP specification, +/// such as subclassing internal types or SDK-specific extensibility hooks. These APIs may +/// change or be removed based on SDK design feedback. +/// +/// +/// /// When an experimental API is associated with an experimental specification, the message /// should refer to the specification version that introduces the feature and the SEP /// when available. If there is a SEP associated with the experimental API, the Url should /// point to the SEP issue. +/// /// /// Experimental diagnostic IDs are in the format MCPEXP###. /// @@ -58,8 +72,14 @@ internal static class Experimentals public const string Extensions_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001"; /// - /// Diagnostic ID for experimental subclassing of McpClient and McpServer. + /// Diagnostic ID for experimental SDK APIs unrelated to the MCP specification, + /// such as subclassing McpClient/McpServer or referencing RunSessionHandler. /// + /// + /// This diagnostic ID covers experimental SDK-level extensibility APIs. All constants + /// in this group share the same diagnostic ID so users need only one suppression point + /// for SDK design preview features. + /// public const string Subclassing_DiagnosticId = "MCPEXP002"; /// @@ -70,5 +90,24 @@ internal static class Experimentals /// /// URL for experimental subclassing of McpClient and McpServer. /// - public const string Subclassing_Url = "https://github.com/modelcontextprotocol/csharp-sdk/pull/1363"; + public const string Subclassing_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp002"; + + /// + /// Diagnostic ID for the experimental RunSessionHandler API. + /// + /// + /// This uses the same diagnostic ID as because + /// both are experimental SDK APIs unrelated to the MCP specification. + /// + public const string RunSessionHandler_DiagnosticId = "MCPEXP002"; + + /// + /// Message for the experimental RunSessionHandler API. + /// + public const string RunSessionHandler_Message = "RunSessionHandler is experimental and may be removed or changed in a future release. Consider using ConfigureSessionOptions instead."; + + /// + /// URL for the experimental RunSessionHandler API. + /// + public const string RunSessionHandler_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp002"; } diff --git a/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs b/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs index 1b2bec0eb..0338911d3 100644 --- a/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs +++ b/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs @@ -24,7 +24,20 @@ public class HttpServerTransportOptions /// /// /// This callback is useful for running logic before a session starts and after it completes. + /// + /// The parameter comes from the request that initiated the session (e.g., the + /// initialize request) and may not be usable after starts, since that + /// request will have already completed. + /// + /// + /// Consider using instead, which provides access to the + /// of the initializing request with fewer known issues. + /// + /// + /// This API is experimental and may be removed or change signatures in a future release. + /// /// + [System.Diagnostics.CodeAnalysis.Experimental(Experimentals.RunSessionHandler_DiagnosticId, UrlFormat = Experimentals.RunSessionHandler_Url)] public Func? RunSessionHandler { get; set; } /// diff --git a/src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj b/src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj index a957bd969..6fe243695 100644 --- a/src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj +++ b/src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj @@ -20,6 +20,10 @@ + + + + diff --git a/src/ModelContextProtocol.AspNetCore/SseHandler.cs b/src/ModelContextProtocol.AspNetCore/SseHandler.cs index eefe0d29e..472ba08c4 100644 --- a/src/ModelContextProtocol.AspNetCore/SseHandler.cs +++ b/src/ModelContextProtocol.AspNetCore/SseHandler.cs @@ -56,7 +56,9 @@ public async Task HandleSseRequestAsync(HttpContext context) await using var mcpServer = McpServer.Create(transport, mcpServerOptions, loggerFactory, context.RequestServices); context.Features.Set(mcpServer); +#pragma warning disable MCPEXP002 // RunSessionHandler is experimental var runSessionAsync = httpMcpServerOptions.Value.RunSessionHandler ?? StreamableHttpHandler.RunSessionAsync; +#pragma warning restore MCPEXP002 await runSessionAsync(context, mcpServer, cancellationToken); } finally diff --git a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs index b02333ae8..290eca4cc 100644 --- a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs +++ b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs @@ -390,7 +390,9 @@ private async ValueTask CreateSessionAsync( var userIdClaim = GetUserIdClaim(context.User); var session = new StreamableHttpSession(sessionId, transport, server, userIdClaim, sessionManager); +#pragma warning disable MCPEXP002 // RunSessionHandler is experimental var runSessionAsync = HttpServerTransportOptions.RunSessionHandler ?? RunSessionAsync; +#pragma warning restore MCPEXP002 session.ServerRunTask = runSessionAsync(context, server, session.SessionClosed); return session; diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs index aa619d9ba..53099ad27 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpStreamableHttpTests.cs @@ -213,12 +213,14 @@ public async Task CanResumeSessionWithMapMcpAndRunSessionHandler() }).WithHttpTransport(opts => { ConfigureStateless(opts); +#pragma warning disable MCPEXP002 // RunSessionHandler is experimental opts.RunSessionHandler = async (context, server, cancellationToken) => { Interlocked.Increment(ref runSessionCount); serverTcs.TrySetResult(server); await server.RunAsync(cancellationToken); }; +#pragma warning restore MCPEXP002 }).WithTools(); await using var app = Builder.Build(); @@ -481,11 +483,13 @@ public async Task DisposeAsync_DoesNotHang_WhenOwnsSessionIsFalse_WithUnsolicite Builder.Services.AddMcpServer().WithHttpTransport(opts => { ConfigureStateless(opts); +#pragma warning disable MCPEXP002 // RunSessionHandler is experimental opts.RunSessionHandler = async (context, server, cancellationToken) => { serverTcs.TrySetResult(server); await server.RunAsync(cancellationToken); }; +#pragma warning restore MCPEXP002 }).WithTools(); await using var app = Builder.Build(); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ResumabilityIntegrationTestsBase.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ResumabilityIntegrationTestsBase.cs index 703697f1a..9738ffda3 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ResumabilityIntegrationTestsBase.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ResumabilityIntegrationTestsBase.cs @@ -253,11 +253,13 @@ public virtual async Task Client_CanResumeUnsolicitedMessageStream_AfterDisconne await using var app = await CreateServerAsync(configureTransport: options => { +#pragma warning disable MCPEXP002 // RunSessionHandler is experimental options.RunSessionHandler = (httpContext, mcpServer, cancellationToken) => { serverTcs.TrySetResult(mcpServer); return mcpServer.RunAsync(cancellationToken); }; +#pragma warning restore MCPEXP002 }); await using var client = await ConnectClientAsync(); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs index 11df047fb..35adccef9 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs @@ -83,6 +83,7 @@ public async Task ConnectAndReceiveNotification_InMemoryServer() Builder.Services.AddMcpServer() .WithHttpTransport(httpTransportOptions => { +#pragma warning disable MCPEXP002 // RunSessionHandler is experimental httpTransportOptions.RunSessionHandler = (httpContext, mcpServer, cancellationToken) => { // We could also use ServerCapabilities.NotificationHandlers, but it's good to have some test coverage of RunSessionHandler. @@ -93,6 +94,7 @@ public async Task ConnectAndReceiveNotification_InMemoryServer() }); return mcpServer.RunAsync(cancellationToken); }; +#pragma warning restore MCPEXP002 }); await using var app = Builder.Build(); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs index 87aa46566..0f7c7e7e0 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs @@ -158,6 +158,7 @@ public async Task UnsolicitedNotification_Fails_WithInvalidOperationException() Builder.Services.AddMcpServer() .WithHttpTransport(options => { +#pragma warning disable MCPEXP002 // RunSessionHandler is experimental options.RunSessionHandler = async (context, server, cancellationToken) => { unsolicitedNotificationException = await Assert.ThrowsAsync( @@ -165,6 +166,7 @@ public async Task UnsolicitedNotification_Fails_WithInvalidOperationException() await server.RunAsync(cancellationToken); }; +#pragma warning restore MCPEXP002 }); await StartAsync(); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs index 548348dc2..ff566f533 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs @@ -325,11 +325,13 @@ public async Task GetRequest_Receives_UnsolicitedNotifications() Builder.Services.AddMcpServer() .WithHttpTransport(options => { +#pragma warning disable MCPEXP002 // RunSessionHandler is experimental options.RunSessionHandler = (httpContext, mcpServer, cancellationToken) => { server = mcpServer; return mcpServer.RunAsync(cancellationToken); }; +#pragma warning restore MCPEXP002 }); await StartAsync(); @@ -365,11 +367,13 @@ public async Task SendNotificationAsync_DoesNotThrow_WhenNoGetRequestHasBeenMade Builder.Services.AddMcpServer() .WithHttpTransport(options => { +#pragma warning disable MCPEXP002 // RunSessionHandler is experimental options.RunSessionHandler = (httpContext, mcpServer, cancellationToken) => { server = mcpServer; return mcpServer.RunAsync(cancellationToken); }; +#pragma warning restore MCPEXP002 }); await StartAsync(); @@ -502,11 +506,13 @@ public async Task AsyncLocalSetInRunSessionHandlerCallback_Flows_ToAllToolCalls_ .WithHttpTransport(options => { options.PerSessionExecutionContext = true; +#pragma warning disable MCPEXP002 // RunSessionHandler is experimental options.RunSessionHandler = async (httpContext, mcpServer, cancellationToken) => { asyncLocal.Value = $"RunSessionHandler ({totalSessionCount++})"; await mcpServer.RunAsync(cancellationToken); }; +#pragma warning restore MCPEXP002 }); Builder.Services.AddSingleton(McpServerTool.Create([McpServerTool(Name = "async-local-session")] () => asyncLocal.Value));