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
5 changes: 2 additions & 3 deletions samples/EverythingServer/EverythingServer.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
Expand All @@ -8,14 +8,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\ModelContextProtocol\ModelContextProtocol.csproj" />
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
</ItemGroup>

</Project>
77 changes: 77 additions & 0 deletions samples/EverythingServer/EverythingServer.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
@HostAddress = http://localhost:3001

POST {{HostAddress}}/
Accept: application/json, text/event-stream
Content-Type: application/json

{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"clientInfo": {
"name": "RestClient",
"version": "0.1.0"
},
"capabilities": {},
"protocolVersion": "2025-06-18"
}
}

###

@SessionId = ZwwM0VFEtKNOMBsP8D2VzQ

POST {{HostAddress}}/
Accept: application/json, text/event-stream
Content-Type: application/json
MCP-Protocol-Version: 2025-06-18
Mcp-Session-Id: {{SessionId}}

{
"jsonrpc": "2.0",
"id": 2,
"method": "resources/list"
}

###

@resource_uri = test://direct/text/resource

POST {{HostAddress}}/
Accept: application/json, text/event-stream
Content-Type: application/json
MCP-Protocol-Version: 2025-06-18
Mcp-Session-Id: {{SessionId}}

{
"jsonrpc": "2.0",
"id": 3,
"method": "resources/subscribe",
"params": {
"uri": "{{resource_uri}}"
}
}

###

POST {{HostAddress}}/
Accept: application/json, text/event-stream
Content-Type: application/json
MCP-Protocol-Version: 2025-06-18
Mcp-Session-Id: {{SessionId}}

{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/unsubscribe",
"params": {
"uri": "{{resource_uri}}"
}
}

###

DELETE {{HostAddress}}/
MCP-Protocol-Version: 2025-06-18
Mcp-Session-Id: {{SessionId}}
10 changes: 5 additions & 5 deletions samples/EverythingServer/LoggingUpdateMessageSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace EverythingServer;

public class LoggingUpdateMessageSender(McpServer server, Func<LoggingLevel> getMinLevel) : BackgroundService
public class LoggingUpdateMessageSender(McpServer server) : BackgroundService
{
readonly Dictionary<LoggingLevel, string> _loggingLevelMap = new()
{
Expand All @@ -23,15 +23,15 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var newLevel = (LoggingLevel)Random.Shared.Next(_loggingLevelMap.Count);
var msgLevel = (LoggingLevel)Random.Shared.Next(_loggingLevelMap.Count);

var message = new
{
Level = newLevel.ToString().ToLower(),
Data = _loggingLevelMap[newLevel],
Level = msgLevel.ToString().ToLower(),
Data = _loggingLevelMap[msgLevel],
};

if (newLevel > getMinLevel())
if (msgLevel > server.LoggingLevel)
{
await server.SendNotificationAsync("notifications/message", message, cancellationToken: stoppingToken);
}
Expand Down
78 changes: 52 additions & 26 deletions samples/EverythingServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
using EverythingServer.Resources;
using EverythingServer.Tools;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
Expand All @@ -14,20 +11,46 @@
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Collections.Concurrent;

var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(consoleLogOptions =>
{
// Configure all logs to go to stderr
consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
var builder = WebApplication.CreateBuilder(args);

HashSet<string> subscriptions = [];
var _minimumLoggingLevel = LoggingLevel.Debug;
// Dictionary of session IDs to a set of resource URIs they are subscribed to
// The value is a ConcurrentDictionary used as a thread-safe HashSet
// because .NET does not have a built-in concurrent HashSet
ConcurrentDictionary<string, ConcurrentDictionary<string, byte>> subscriptions = new();

builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithHttpTransport(options =>
{
// Add a RunSessionHandler to remove all subscriptions for the session when it ends
options.RunSessionHandler = async (httpContext, mcpServer, token) =>
{
if (mcpServer.SessionId == null)
{
// There is no sessionId if the serverOptions.Stateless is true
await mcpServer.RunAsync(token);
return;
}
try
{
subscriptions[mcpServer.SessionId] = new ConcurrentDictionary<string, byte>();
// Start an instance of SubscriptionMessageSender for this session
using var subscriptionSender = new SubscriptionMessageSender(mcpServer, subscriptions[mcpServer.SessionId]);
await subscriptionSender.StartAsync(token);
// Start an instance of LoggingUpdateMessageSender for this session
using var loggingSender = new LoggingUpdateMessageSender(mcpServer);
await loggingSender.StartAsync(token);
await mcpServer.RunAsync(token);
}
finally
{
// This code runs when the session ends
subscriptions.TryRemove(mcpServer.SessionId, out _);
}
};
})
.WithTools<AddTool>()
.WithTools<AnnotatedMessageTool>()
.WithTools<EchoTool>()
Expand All @@ -40,11 +63,13 @@
.WithResources<SimpleResourceType>()
.WithSubscribeToResourcesHandler(async (ctx, ct) =>
{
var uri = ctx.Params?.Uri;

if (uri is not null)
if (ctx.Server.SessionId == null)
{
subscriptions.Add(uri);
throw new McpException("Cannot add subscription for server with null SessionId");
}
if (ctx.Params?.Uri is { } uri)
{
subscriptions[ctx.Server.SessionId].TryAdd(uri, 0);

await ctx.Server.SampleAsync([
new ChatMessage(ChatRole.System, "You are a helpful test server"),
Expand All @@ -62,10 +87,13 @@ await ctx.Server.SampleAsync([
})
.WithUnsubscribeFromResourcesHandler(async (ctx, ct) =>
{
var uri = ctx.Params?.Uri;
if (uri is not null)
if (ctx.Server.SessionId == null)
{
throw new McpException("Cannot remove subscription for server with null SessionId");
}
if (ctx.Params?.Uri is { } uri)
{
subscriptions.Remove(uri);
subscriptions[ctx.Server.SessionId].TryRemove(uri, out _);
}
return new EmptyResult();
})
Expand Down Expand Up @@ -126,13 +154,13 @@ await ctx.Server.SampleAsync([
throw new McpProtocolException("Missing required argument 'level'", McpErrorCode.InvalidParams);
}

_minimumLoggingLevel = ctx.Params.Level;
// The SDK updates the LoggingLevel field of the IMcpServer

await ctx.Server.SendNotificationAsync("notifications/message", new
{
Level = "debug",
Logger = "test-server",
Data = $"Logging level set to {_minimumLoggingLevel}",
Data = $"Logging level set to {ctx.Params.Level}",
}, cancellationToken: ct);

return new EmptyResult();
Expand All @@ -145,10 +173,8 @@ await ctx.Server.SampleAsync([
.WithLogging(b => b.SetResourceBuilder(resource))
.UseOtlpExporter();

builder.Services.AddSingleton(subscriptions);
builder.Services.AddHostedService<SubscriptionMessageSender>();
builder.Services.AddHostedService<LoggingUpdateMessageSender>();
var app = builder.Build();

builder.Services.AddSingleton<Func<LoggingLevel>>(_ => () => _minimumLoggingLevel);
app.MapMcp();

await builder.Build().RunAsync();
app.Run();
21 changes: 21 additions & 0 deletions samples/EverythingServer/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:3001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7133;http://localhost:3001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
}
}
}
}
6 changes: 3 additions & 3 deletions samples/EverythingServer/SubscriptionMessageSender.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
using Microsoft.Extensions.Hosting;
using System.Collections.Concurrent;
using ModelContextProtocol;
using ModelContextProtocol.Server;

internal class SubscriptionMessageSender(McpServer server, HashSet<string> subscriptions) : BackgroundService
internal class SubscriptionMessageSender(McpServer server, ConcurrentDictionary<string, byte> subscriptions) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
foreach (var uri in subscriptions)
foreach (var uri in subscriptions.Keys)
{
await server.SendNotificationAsync("notifications/resource/updated",
new
Expand Down
8 changes: 8 additions & 0 deletions samples/EverythingServer/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions samples/EverythingServer/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}