Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Draft
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}}
37 changes: 26 additions & 11 deletions samples/EverythingServer/LoggingUpdateMessageSender.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using Microsoft.Extensions.Hosting;
using ModelContextProtocol;
using ModelContextProtocol;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;

namespace EverythingServer;

public class LoggingUpdateMessageSender(IMcpServer server, Func<LoggingLevel> getMinLevel) : BackgroundService
public class LoggingUpdateMessageSender(IServiceProvider serviceProvider) : BackgroundService
{
readonly Dictionary<LoggingLevel, string> _loggingLevelMap = new()
{
Expand All @@ -21,19 +20,35 @@ public class LoggingUpdateMessageSender(IMcpServer server, Func<LoggingLevel> ge

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Wait for the application to fully start before trying to access the MCP server
await Task.Delay(2000, stoppingToken);

while (!stoppingToken.IsCancellationRequested)
{
var newLevel = (LoggingLevel)Random.Shared.Next(_loggingLevelMap.Count);

var message = new
try
{
// Try to get the server from the service provider
var server = serviceProvider.GetService<IMcpServer>();
if (server != null)
{
Level = newLevel.ToString().ToLower(),
Data = _loggingLevelMap[newLevel],
};
var newLevel = (LoggingLevel)Random.Shared.Next(_loggingLevelMap.Count);

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

if (newLevel > server.LoggingLevel)
{
await server.SendNotificationAsync("notifications/message", message, cancellationToken: stoppingToken);
}
}
}
catch (Exception ex)
{
await server.SendNotificationAsync("notifications/message", message, cancellationToken: stoppingToken);
// Log the exception but don't crash the service
Console.WriteLine($"Error in LoggingUpdateMessageSender: {ex.Message}");
}

await Task.Delay(15000, stoppingToken);
Expand Down
63 changes: 38 additions & 25 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,28 @@
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;
});

HashSet<string> subscriptions = [];
var _minimumLoggingLevel = LoggingLevel.Debug;
var builder = WebApplication.CreateBuilder(args);

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) =>
{
try
{
await mcpServer.RunAsync(token);
}
finally
{
// This code runs when the session ends
SubscriptionManager.RemoveAllSubscriptions(mcpServer);
}
};
})
.WithTools<AddTool>()
.WithTools<AnnotatedMessageTool>()
.WithTools<EchoTool>()
Expand All @@ -40,11 +45,13 @@
.WithResources<SimpleResourceType>()
.WithSubscribeToResourcesHandler(async (ctx, ct) =>
{
var uri = ctx.Params?.Uri;

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

await ctx.Server.SampleAsync([
new ChatMessage(ChatRole.System, "You are a helpful test server"),
Expand All @@ -62,10 +69,13 @@ await ctx.Server.SampleAsync([
})
.WithUnsubscribeFromResourcesHandler(async (ctx, ct) =>
{
var uri = ctx.Params?.Uri;
if (uri is not null)
if (ctx.Server.SessionId == null)
{
subscriptions.Remove(uri);
throw new McpException("Cannot remove subscription for server with null SessionId");
}
if (ctx.Params?.Uri is { } uri)
{
SubscriptionManager.RemoveSubscription(uri, ctx.Server);
}
return new EmptyResult();
})
Expand Down Expand Up @@ -126,13 +136,13 @@ await ctx.Server.SampleAsync([
throw new McpException("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 +155,13 @@ await ctx.Server.SampleAsync([
.WithLogging(b => b.SetResourceBuilder(resource))
.UseOtlpExporter();

builder.Services.AddSingleton(subscriptions);
builder.Services.AddHostedService<SubscriptionMessageSender>();
builder.Services.AddHostedService<LoggingUpdateMessageSender>();

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

app.UseHttpsRedirection();

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",
}
}
}
}
91 changes: 91 additions & 0 deletions samples/EverythingServer/SubscriptionManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using ModelContextProtocol;
using ModelContextProtocol.Server;

// This class manages subscriptions to resources by McpServer instances.
// The subscription information must be accessed in a thread-safe manner since handlers
// can run in parallel even in the context of a single session.
static class SubscriptionManager
{
// Subscriptions tracks resource URIs to bags of McpServer instances (thread-safe via locking)
private static Dictionary<string, List<IMcpServer>> subscriptions = new();

// SessionSubscriptions is a secondary index to subscriptions to allow efficient removal of all
// subscriptions for a given session when it ends. (thread-safe via locking)
private static Dictionary<string /* sessionId */, List<string> /* uris */> sessionSubscriptions = new();

private static readonly object _subscriptionsLock = new();

public static void AddSubscription(string uri, IMcpServer server)
{
if (server.SessionId == null)
{
throw new McpException("Cannot add subscription for server with null SessionId");
}
lock (_subscriptionsLock)
{
subscriptions[uri] ??= new List<IMcpServer>();
subscriptions[uri].Add(server);
sessionSubscriptions[server.SessionId] ??= new List<string>();
sessionSubscriptions[server.SessionId].Add(uri);
}
}

public static void RemoveSubscription(string uri, IMcpServer server)
{
if (server.SessionId == null)
{
throw new McpException("Cannot remove subscription for server with null SessionId");
}
lock (_subscriptionsLock)
{
if (subscriptions.ContainsKey(uri))
{
// Remove the server from the list of subscriptions for the URI
subscriptions[uri] = subscriptions[uri].Where(s => s.SessionId != server.SessionId).ToList();
if (subscriptions[uri]?.Count == 0)
{
subscriptions.Remove(uri);
}
}
// Remove the URI from the list of subscriptions for the session
sessionSubscriptions[server.SessionId]?.Remove(uri);
if (sessionSubscriptions[server.SessionId]?.Count == 0)
{
sessionSubscriptions.Remove(server.SessionId);
}
}
}

public static IDictionary<string, List<IMcpServer>> GetSubscriptions()
{
lock (_subscriptionsLock)
{
// Return a copy of the subscriptions dictionary to avoid external modification
return subscriptions.ToDictionary(entry => entry.Key,
entry => entry.Value.ToList());
}
}

public static void RemoveAllSubscriptions(IMcpServer server)
{
if (server.SessionId is { } sessionId)
{
lock (_subscriptionsLock)
{
// Remove all subscriptions for the session
if (sessionSubscriptions.TryGetValue(sessionId, out var uris))
{
foreach (var uri in uris)
{
subscriptions[uri] = subscriptions[uri].Where(s => s.SessionId != sessionId).ToList();
if (subscriptions[uri]?.Count == 0)
{
subscriptions.Remove(uri);
}
}
sessionSubscriptions.Remove(sessionId);
}
}
}
}
}
Loading
Loading