Skip to content

Commit bcb195e

Browse files
authored
Merge branch 'main' into dotnet-everything
2 parents ddf39ee + 96fa96c commit bcb195e

99 files changed

Lines changed: 3800 additions & 2313 deletions

File tree

Some content is hidden

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

ModelContextProtocol.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickstartClient", "samples
5252
EndProject
5353
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EverythingServer", "samples\EverythingServer\EverythingServer.csproj", "{17B8453F-AB72-99C5-E5EA-D0B065A6AE65}"
5454
EndProject
55+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol.AspNetCore", "src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj", "{37B6A5E0-9995-497D-8B43-3BC6870CC716}"
56+
EndProject
5557
Global
5658
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5759
Debug|Any CPU = Debug|Any CPU
@@ -98,6 +100,10 @@ Global
98100
{17B8453F-AB72-99C5-E5EA-D0B065A6AE65}.Debug|Any CPU.Build.0 = Debug|Any CPU
99101
{17B8453F-AB72-99C5-E5EA-D0B065A6AE65}.Release|Any CPU.ActiveCfg = Release|Any CPU
100102
{17B8453F-AB72-99C5-E5EA-D0B065A6AE65}.Release|Any CPU.Build.0 = Release|Any CPU
103+
{37B6A5E0-9995-497D-8B43-3BC6870CC716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
104+
{37B6A5E0-9995-497D-8B43-3BC6870CC716}.Debug|Any CPU.Build.0 = Debug|Any CPU
105+
{37B6A5E0-9995-497D-8B43-3BC6870CC716}.Release|Any CPU.ActiveCfg = Release|Any CPU
106+
{37B6A5E0-9995-497D-8B43-3BC6870CC716}.Release|Any CPU.Build.0 = Release|Any CPU
101107
EndGlobalSection
102108
GlobalSection(SolutionProperties) = preSolution
103109
HideSolutionNode = FALSE
@@ -114,6 +120,7 @@ Global
114120
{4653EB0C-8FC0-98F4-E9C8-220EDA7A69DF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
115121
{0D1552DC-E6ED-4AAC-5562-12F8352F46AA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
116122
{17B8453F-AB72-99C5-E5EA-D0B065A6AE65} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
123+
{37B6A5E0-9995-497D-8B43-3BC6870CC716} = {A2F1F52A-9107-4BF8-8C3F-2F6670E7D0AD}
117124
EndGlobalSection
118125
GlobalSection(ExtensibilityGlobals) = postSolution
119126
SolutionGuid = {384A3888-751F-4D75-9AE5-587330582D89}

README.md

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ the employed overload of `WithTools` examines the current assembly for classes w
8484
`McpTool` attribute as tools.)
8585

8686
```csharp
87-
using ModelContextProtocol;
88-
using ModelContextProtocol.Server;
87+
using Microsoft.Extensions.DependencyInjection;
8988
using Microsoft.Extensions.Hosting;
89+
using ModelContextProtocol.Server;
9090
using System.ComponentModel;
9191

9292
var builder = Host.CreateEmptyApplicationBuilder(settings: null);
@@ -109,7 +109,7 @@ the connected client. Similarly, arguments may be injected via dependency inject
109109
`IMcpServer` to make sampling requests back to the client in order to summarize content it downloads from the specified url via
110110
an `HttpClient` injected via dependency injection.
111111
```csharp
112-
[McpServerTool("SummarizeContentFromUrl"), Description("Summarizes content downloaded from a specific URI")]
112+
[McpServerTool(Name = "SummarizeContentFromUrl"), Description("Summarizes content downloaded from a specific URI")]
113113
public static async Task<string> SummarizeDownloadedContent(
114114
IMcpServer thisServer,
115115
HttpClient httpClient,
@@ -122,8 +122,8 @@ public static async Task<string> SummarizeDownloadedContent(
122122
[
123123
new(ChatRole.User, "Briefly summarize the following downloaded content:"),
124124
new(ChatRole.User, content),
125-
]
126-
125+
];
126+
127127
ChatOptions options = new()
128128
{
129129
MaxOutputTokens = 256,
@@ -134,13 +134,24 @@ public static async Task<string> SummarizeDownloadedContent(
134134
}
135135
```
136136

137+
Prompts can be exposed in a similar manner, using `[McpServerPrompt]`, e.g.
138+
```csharp
139+
[McpServerPromptType]
140+
public static class MyPrompts
141+
{
142+
[McpServerPrompt, Description("Creates a prompt to summarize the provided message.")]
143+
public static ChatMessage Summarize([Description("The content to summarize")] string content) =>
144+
new(ChatRole.User, $"Please summarize this content into a single sentence: {content}");
145+
}
146+
```
147+
137148
More control is also available, with fine-grained control over configuring the server and how it should handle client requests. For example:
138149

139150
```csharp
140151
using ModelContextProtocol.Protocol.Transport;
141152
using ModelContextProtocol.Protocol.Types;
142153
using ModelContextProtocol.Server;
143-
using Microsoft.Extensions.Logging.Abstractions;
154+
using System.Text.Json;
144155

145156
McpServerOptions options = new()
146157
{
@@ -149,9 +160,8 @@ McpServerOptions options = new()
149160
{
150161
Tools = new()
151162
{
152-
ListToolsHandler = async (request, cancellationToken) =>
153-
{
154-
return new ListToolsResult()
163+
ListToolsHandler = (request, cancellationToken) =>
164+
Task.FromResult(new ListToolsResult()
155165
{
156166
Tools =
157167
[
@@ -173,10 +183,9 @@ McpServerOptions options = new()
173183
"""),
174184
}
175185
]
176-
};
177-
},
186+
}),
178187

179-
CallToolHandler = async (request, cancellationToken) =>
188+
CallToolHandler = (request, cancellationToken) =>
180189
{
181190
if (request.Params?.Name == "echo")
182191
{
@@ -185,10 +194,10 @@ McpServerOptions options = new()
185194
throw new McpServerException("Missing required argument 'message'");
186195
}
187196

188-
return new CallToolResponse()
197+
return Task.FromResult(new CallToolResponse()
189198
{
190199
Content = [new Content() { Text = $"Echo: {message}", Type = "text" }]
191-
};
200+
});
192201
}
193202

194203
throw new McpServerException($"Unknown tool: '{request.Params?.Name}'");

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 & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
using ModelContextProtocol;
2-
using AspNetCoreSseServer;
3-
41
var builder = WebApplication.CreateBuilder(args);
52
builder.Services.AddMcpServer().WithToolsFromAssembly();
63
var app = builder.Build();
74

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

117
app.Run();

samples/QuickstartWeatherServer/Program.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Extensions.Hosting;
3-
using ModelContextProtocol;
43
using System.Net.Http.Headers;
54

65
var builder = Host.CreateEmptyApplicationBuilder(settings: null);

samples/TestServerWithHosting/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using ModelContextProtocol;
1+
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Extensions.Hosting;
33
using Serilog;
44

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+
await using var server = McpServerFactory.Create(transport, mcpServerOptions.Value, loggerFactory, endpoints.ServiceProvider);
51+
52+
try
53+
{
54+
var transportTask = transport.RunAsync(cancellationToken: requestAborted);
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 {sessionId} not found.").ExecuteAsync(context);
89+
return;
90+
}
91+
92+
var message = await context.Request.ReadFromJsonAsync<IJsonRpcMessage>(McpJsonUtilities.DefaultOptions, 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+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
8+
<IsPackable>true</IsPackable>
9+
<PackageId>ModelContextProtocol.AspNetCore</PackageId>
10+
<Description>ASP.NET Core extensions for the C# Model Context Protocol (MCP) SDK.</Description>
11+
<PackageReadmeFile>README.md</PackageReadmeFile>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<None Include="README.md" pack="true" PackagePath="\" />
20+
</ItemGroup>
21+
<ItemGroup>
22+
<ProjectReference Include="..\ModelContextProtocol\ModelContextProtocol.csproj" />
23+
</ItemGroup>
24+
25+
</Project>

0 commit comments

Comments
 (0)