Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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> and <c>RunSessionHandler</c>.
Comment thread
halter73 marked this conversation as resolved.
Outdated
/// </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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ public class HttpServerTransportOptions
/// </summary>
/// <remarks>
/// This callback is useful for running logic before a session starts and after it completes.
/// <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