Skip to content

Commit 3677281

Browse files
authored
Merge branch 'main' into custom_mcp_client_tool_values
2 parents f1fd4bb + 4b38d3e commit 3677281

File tree

76 files changed

+2114
-1908
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+2114
-1908
lines changed

Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsVersion)" />
3434
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
3535
<PackageVersion Include="Moq" Version="4.20.72" />
36+
<PackageVersion Include="OpenTelemetry" Version="1.11.2" />
37+
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.11.2" />
3638
<PackageVersion Include="Serilog.Extensions.Hosting" Version="9.0.0" />
3739
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />
3840
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />

ModelContextProtocol.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickstartWeatherServer", "
5050
EndProject
5151
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickstartClient", "samples\QuickstartClient\QuickstartClient.csproj", "{0D1552DC-E6ED-4AAC-5562-12F8352F46AA}"
5252
EndProject
53+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol.AspNetCore", "src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj", "{37B6A5E0-9995-497D-8B43-3BC6870CC716}"
54+
EndProject
5355
Global
5456
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5557
Debug|Any CPU = Debug|Any CPU
@@ -92,6 +94,10 @@ Global
9294
{0D1552DC-E6ED-4AAC-5562-12F8352F46AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
9395
{0D1552DC-E6ED-4AAC-5562-12F8352F46AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
9496
{0D1552DC-E6ED-4AAC-5562-12F8352F46AA}.Release|Any CPU.Build.0 = Release|Any CPU
97+
{37B6A5E0-9995-497D-8B43-3BC6870CC716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
98+
{37B6A5E0-9995-497D-8B43-3BC6870CC716}.Debug|Any CPU.Build.0 = Debug|Any CPU
99+
{37B6A5E0-9995-497D-8B43-3BC6870CC716}.Release|Any CPU.ActiveCfg = Release|Any CPU
100+
{37B6A5E0-9995-497D-8B43-3BC6870CC716}.Release|Any CPU.Build.0 = Release|Any CPU
95101
EndGlobalSection
96102
GlobalSection(SolutionProperties) = preSolution
97103
HideSolutionNode = FALSE
@@ -107,6 +113,7 @@ Global
107113
{0C6D0512-D26D-63D3-5019-C5F7A657B28C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
108114
{4653EB0C-8FC0-98F4-E9C8-220EDA7A69DF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
109115
{0D1552DC-E6ED-4AAC-5562-12F8352F46AA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
116+
{37B6A5E0-9995-497D-8B43-3BC6870CC716} = {A2F1F52A-9107-4BF8-8C3F-2F6670E7D0AD}
110117
EndGlobalSection
111118
GlobalSection(ExtensibilityGlobals) = postSolution
112119
SolutionGuid = {384A3888-751F-4D75-9AE5-587330582D89}

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,24 @@ It includes a simple echo tool as an example (this is included in the same file
8383
the employed overload of `WithTools` examines the current assembly for classes with the `McpServerToolType` attribute, and registers all methods with the
8484
`McpTool` attribute as tools.)
8585

86+
```
87+
dotnet add package ModelContextProtocol --prerelease
88+
dotnet add package Microsoft.Extensions.Hosting
89+
```
90+
8691
```csharp
8792
using Microsoft.Extensions.DependencyInjection;
8893
using Microsoft.Extensions.Hosting;
94+
using Microsoft.Extensions.Logging;
8995
using ModelContextProtocol.Server;
9096
using System.ComponentModel;
9197

92-
var builder = Host.CreateEmptyApplicationBuilder(settings: null);
98+
var builder = Host.CreateApplicationBuilder(args);
99+
builder.Logging.AddConsole(consoleLogOptions =>
100+
{
101+
// Configure all logs to go to stderr
102+
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
103+
});
93104
builder.Services
94105
.AddMcpServer()
95106
.WithStdioServerTransport()

samples/AspNetCoreSseServer/AspNetCoreSseServer.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
<ItemGroup>
1010
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" />
11+
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
1112
</ItemGroup>
1213

1314
</Project>

samples/AspNetCoreSseServer/McpEndpointRouteBuilderExtensions.cs

Lines changed: 0 additions & 62 deletions
This file was deleted.
Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
using AspNetCoreSseServer;
2-
31
var builder = WebApplication.CreateBuilder(args);
42
builder.Services.AddMcpServer().WithToolsFromAssembly();
53
var app = builder.Build();
64

7-
app.MapGet("/", () => "Hello World!");
8-
app.MapMcpSse();
5+
app.MapMcp();
96

107
app.Run();

samples/QuickstartClient/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
using ModelContextProtocol.Client;
66
using ModelContextProtocol.Protocol.Transport;
77

8-
var builder = Host.CreateEmptyApplicationBuilder(settings: null);
8+
var builder = Host.CreateApplicationBuilder(args);
99

1010
builder.Configuration
1111
.AddEnvironmentVariables()

samples/QuickstartWeatherServer/Program.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Extensions.Hosting;
3+
using Microsoft.Extensions.Logging;
34
using System.Net.Http.Headers;
45

5-
var builder = Host.CreateEmptyApplicationBuilder(settings: null);
6+
var builder = Host.CreateApplicationBuilder(args);
67

78
builder.Services.AddMcpServer()
89
.WithStdioServerTransport()
910
.WithToolsFromAssembly();
1011

12+
builder.Logging.AddConsole(options =>
13+
{
14+
options.LogToStandardErrorThreshold = LogLevel.Trace;
15+
});
16+
1117
builder.Services.AddSingleton(_ =>
1218
{
1319
var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") };

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<RepositoryUrl>https://github.com/modelcontextprotocol/csharp-sdk</RepositoryUrl>
77
<RepositoryType>git</RepositoryType>
88
<VersionPrefix>0.1.0</VersionPrefix>
9-
<VersionSuffix>preview.2</VersionSuffix>
9+
<VersionSuffix>preview.5</VersionSuffix>
1010
<Authors>ModelContextProtocolOfficial</Authors>
1111
<Copyright>© Anthropic and Contributors.</Copyright>
1212
<PackageTags>ModelContextProtocol;mcp;ai;llm</PackageTags>
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Microsoft.AspNetCore.Routing;
3+
using Microsoft.AspNetCore.WebUtilities;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Logging;
6+
using Microsoft.Extensions.Options;
7+
using ModelContextProtocol.Protocol.Messages;
8+
using ModelContextProtocol.Protocol.Transport;
9+
using ModelContextProtocol.Server;
10+
using ModelContextProtocol.Utils.Json;
11+
using System.Collections.Concurrent;
12+
using System.Security.Cryptography;
13+
14+
namespace Microsoft.AspNetCore.Builder;
15+
16+
/// <summary>
17+
/// Extension methods for <see cref="IEndpointRouteBuilder"/> to add MCP endpoints.
18+
/// </summary>
19+
public static class McpEndpointRouteBuilderExtensions
20+
{
21+
/// <summary>
22+
/// Sets up endpoints for handling MCP HTTP Streaming transport.
23+
/// </summary>
24+
/// <param name="endpoints">The web application to attach MCP HTTP endpoints.</param>
25+
/// <param name="runSession">Provides an optional asynchronous callback for handling new MCP sessions.</param>
26+
/// <returns>Returns a builder for configuring additional endpoint conventions like authorization policies.</returns>
27+
public static IEndpointConventionBuilder MapMcp(this IEndpointRouteBuilder endpoints, Func<HttpContext, IMcpServer, CancellationToken, Task>? runSession = null)
28+
{
29+
ConcurrentDictionary<string, SseResponseStreamTransport> _sessions = new(StringComparer.Ordinal);
30+
31+
var loggerFactory = endpoints.ServiceProvider.GetRequiredService<ILoggerFactory>();
32+
var mcpServerOptions = endpoints.ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>();
33+
34+
var routeGroup = endpoints.MapGroup("");
35+
36+
routeGroup.MapGet("/sse", async context =>
37+
{
38+
var response = context.Response;
39+
var requestAborted = context.RequestAborted;
40+
41+
response.Headers.ContentType = "text/event-stream";
42+
response.Headers.CacheControl = "no-store";
43+
44+
var sessionId = MakeNewSessionId();
45+
await using var transport = new SseResponseStreamTransport(response.Body, $"/message?sessionId={sessionId}");
46+
if (!_sessions.TryAdd(sessionId, transport))
47+
{
48+
throw new Exception($"Unreachable given good entropy! Session with ID '{sessionId}' has already been created.");
49+
}
50+
51+
try
52+
{
53+
var transportTask = transport.RunAsync(cancellationToken: requestAborted);
54+
await using var server = McpServerFactory.Create(transport, mcpServerOptions.Value, loggerFactory, endpoints.ServiceProvider);
55+
56+
try
57+
{
58+
runSession ??= RunSession;
59+
await runSession(context, server, requestAborted);
60+
}
61+
finally
62+
{
63+
await transport.DisposeAsync();
64+
await transportTask;
65+
}
66+
}
67+
catch (OperationCanceledException) when (requestAborted.IsCancellationRequested)
68+
{
69+
// RequestAborted always triggers when the client disconnects before a complete response body is written,
70+
// but this is how SSE connections are typically closed.
71+
}
72+
finally
73+
{
74+
_sessions.TryRemove(sessionId, out _);
75+
}
76+
});
77+
78+
routeGroup.MapPost("/message", async context =>
79+
{
80+
if (!context.Request.Query.TryGetValue("sessionId", out var sessionId))
81+
{
82+
await Results.BadRequest("Missing sessionId query parameter.").ExecuteAsync(context);
83+
return;
84+
}
85+
86+
if (!_sessions.TryGetValue(sessionId.ToString(), out var transport))
87+
{
88+
await Results.BadRequest($"Session ID not found.").ExecuteAsync(context);
89+
return;
90+
}
91+
92+
var message = (IJsonRpcMessage?)await context.Request.ReadFromJsonAsync(McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(IJsonRpcMessage)), context.RequestAborted);
93+
if (message is null)
94+
{
95+
await Results.BadRequest("No message in request body.").ExecuteAsync(context);
96+
return;
97+
}
98+
99+
await transport.OnMessageReceivedAsync(message, context.RequestAborted);
100+
context.Response.StatusCode = StatusCodes.Status202Accepted;
101+
await context.Response.WriteAsync("Accepted");
102+
});
103+
104+
return routeGroup;
105+
}
106+
107+
private static Task RunSession(HttpContext httpContext, IMcpServer session, CancellationToken requestAborted)
108+
=> session.RunAsync(requestAborted);
109+
110+
private static string MakeNewSessionId()
111+
{
112+
// 128 bits
113+
Span<byte> buffer = stackalloc byte[16];
114+
RandomNumberGenerator.Fill(buffer);
115+
return WebEncoders.Base64UrlEncode(buffer);
116+
}
117+
}

0 commit comments

Comments
 (0)