Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Comment thread
jeffhandley marked this conversation as resolved.

## Obsolete APIs

Expand Down
2 changes: 2 additions & 0 deletions samples/EverythingServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -76,6 +77,7 @@
subscriptions.TryRemove(mcpServer.SessionId, out _);
}
};
#pragma warning restore MCPEXP002
})
.WithTools<AddTool>()
.WithTools<AnnotatedMessageTool>()
Expand Down
43 changes: 41 additions & 2 deletions src/Common/Experimentals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,24 @@ namespace ModelContextProtocol;
/// Defines diagnostic IDs, messages, and URLs for APIs annotated with <see cref="ExperimentalAttribute"/>.
/// </summary>
/// <remarks>
/// Experimental diagnostic IDs are grouped by category:
/// <list type="bullet">
/// <item><description>
/// <c>MCPEXP001</c> covers APIs related to experimental features in the MCP specification itself,
/// such as Tasks and Extensions. These APIs may change as the specification evolves.
/// </description></item>
/// <item><description>
/// <c>MCPEXP002</c> 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.
/// </description></item>
/// </list>
/// <para>
/// 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.
/// </para>
/// <para>
/// Experimental diagnostic IDs are in the format MCPEXP###.
/// </para>
Expand Down Expand Up @@ -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";

/// <summary>
/// Diagnostic ID for experimental subclassing of McpClient and McpServer.
/// Diagnostic ID for experimental SDK APIs unrelated to the MCP specification,
/// such as subclassing <c>McpClient</c>/<c>McpServer</c> or referencing <c>RunSessionHandler</c>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public const string Subclassing_DiagnosticId = "MCPEXP002";

/// <summary>
Expand All @@ -70,5 +90,24 @@ internal static class Experimentals
/// <summary>
/// URL for experimental subclassing of McpClient and McpServer.
/// </summary>
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";

/// <summary>
/// Diagnostic ID for the experimental <c>RunSessionHandler</c> API.
/// </summary>
/// <remarks>
/// This uses the same diagnostic ID as <see cref="Subclassing_DiagnosticId"/> because
/// both are experimental SDK APIs unrelated to the MCP specification.
/// </remarks>
public const string RunSessionHandler_DiagnosticId = "MCPEXP002";

/// <summary>
/// Message for the experimental <c>RunSessionHandler</c> API.
/// </summary>
public const string RunSessionHandler_Message = "RunSessionHandler is experimental and may be removed or changed in a future release. Consider using ConfigureSessionOptions instead.";

/// <summary>
/// URL for the experimental <c>RunSessionHandler</c> API.
/// </summary>
public const string RunSessionHandler_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp002";
}
13 changes: 13 additions & 0 deletions src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,20 @@ public class HttpServerTransportOptions
/// </summary>
/// <remarks>
/// This callback is useful for running logic before a session starts and after it completes.
/// <para>
/// The <see cref="HttpContext"/> parameter comes from the request that initiated the session (e.g., the
/// initialize request) and may not be usable after <see cref="McpServer.RunAsync"/> starts, since that
/// request will have already completed.
/// </para>
/// <para>
/// Consider using <see cref="ConfigureSessionOptions"/> instead, which provides access to the
/// <see cref="HttpContext"/> of the initializing request with fewer known issues.
/// </para>
/// <para>
/// This API is experimental and may be removed or change signatures in a future release.
/// </para>
Comment thread
halter73 marked this conversation as resolved.
/// </remarks>
[System.Diagnostics.CodeAnalysis.Experimental(Experimentals.RunSessionHandler_DiagnosticId, UrlFormat = Experimentals.RunSessionHandler_Url)]
public Func<HttpContext, McpServer, CancellationToken, Task>? RunSessionHandler { get; set; }

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
<None Include="README.md" pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Common\Experimentals.cs" Link="Experimentals.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ModelContextProtocol\ModelContextProtocol.csproj" />
</ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/ModelContextProtocol.AspNetCore/SseHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,9 @@ private async ValueTask<StreamableHttpSession> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<EchoHttpContextUserTools>();

await using var app = Builder.Build();
Expand Down Expand Up @@ -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<ClaimsPrincipalTools>();

await using var app = Builder.Build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -93,6 +94,7 @@ public async Task ConnectAndReceiveNotification_InMemoryServer()
});
return mcpServer.RunAsync(cancellationToken);
};
#pragma warning restore MCPEXP002
});

await using var app = Builder.Build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,15 @@ 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<InvalidOperationException>(
() => server.SendNotificationAsync(NotificationMethods.PromptListChangedNotification, TestContext.Current.CancellationToken));

await server.RunAsync(cancellationToken);
};
#pragma warning restore MCPEXP002
});

await StartAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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));
Expand Down
Loading