Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 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
48 changes: 25 additions & 23 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,18 @@
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;
// Subscriptions tracks resource URIs to McpServer instances
// Use thread-safe data structures since handlers can run in parallel
// even in the context of a single session.
ConcurrentDictionary<string, ConcurrentBag<IMcpServer>> subscriptions = new();

builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithHttpTransport()
.WithTools<AddTool>()
.WithTools<AnnotatedMessageTool>()
.WithTools<EchoTool>()
Expand All @@ -40,11 +35,10 @@
.WithResources<SimpleResourceType>()
.WithSubscribeToResourcesHandler(async (ctx, ct) =>
{
var uri = ctx.Params?.Uri;

if (uri is not null)
if (ctx.Params?.Uri is { } uri)
{
subscriptions.Add(uri);
var bag = subscriptions.GetOrAdd(uri, _ => new ConcurrentBag<IMcpServer>());
bag.Add(ctx.Server);

await ctx.Server.SampleAsync([
new ChatMessage(ChatRole.System, "You are a helpful test server"),
Expand All @@ -62,10 +56,14 @@ await ctx.Server.SampleAsync([
})
.WithUnsubscribeFromResourcesHandler(async (ctx, ct) =>
{
var uri = ctx.Params?.Uri;
if (uri is not null)
if (ctx.Params?.Uri is { } uri)
{
subscriptions.Remove(uri);
if (subscriptions.TryGetValue(uri, out var bag))
{
// Remove ctx.Server from the subscription bag (ConcurrentBag does not support removal, so recreate)
var newBag = new ConcurrentBag<IMcpServer>(bag.Where(s => s != ctx.Server));
subscriptions[uri] = newBag;
}
}
return new EmptyResult();
})
Expand Down Expand Up @@ -126,13 +124,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 @@ -149,6 +147,10 @@ await ctx.Server.SampleAsync([
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",
}
}
}
}
30 changes: 22 additions & 8 deletions samples/EverythingServer/SubscriptionMessageSender.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
using Microsoft.Extensions.Hosting;
using ModelContextProtocol;
using ModelContextProtocol;
using ModelContextProtocol.Server;

internal class SubscriptionMessageSender(IMcpServer server, HashSet<string> subscriptions) : BackgroundService
using System.Collections.Concurrent;
internal class SubscriptionMessageSender(ConcurrentDictionary<string, ConcurrentBag<IMcpServer>> subscriptions) : BackgroundService
{
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)
{
foreach (var uri in subscriptions)
try
{
await server.SendNotificationAsync("notifications/resource/updated",
new
foreach (var (uri, servers) in subscriptions)
{
foreach (var server in servers)
{
Uri = uri,
}, cancellationToken: stoppingToken);
await server.SendNotificationAsync("notifications/resource/updated",
new
{
Uri = uri,
}, cancellationToken: stoppingToken);
}
}
}
catch (Exception ex)
{
// Log the exception but don't crash the service
Console.WriteLine($"Error in SubscriptionMessageSender: {ex.Message}");
}

await Task.Delay(5000, stoppingToken);
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": "*"
}
Loading