diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx index 9020d2fbe..69cfbbda6 100644 --- a/ModelContextProtocol.slnx +++ b/ModelContextProtocol.slnx @@ -43,7 +43,9 @@ - + + + diff --git a/samples/EverythingServer.Core/EverythingServer.Core.csproj b/samples/EverythingServer.Core/EverythingServer.Core.csproj new file mode 100644 index 000000000..7ef15aa00 --- /dev/null +++ b/samples/EverythingServer.Core/EverythingServer.Core.csproj @@ -0,0 +1,16 @@ + + + + net9.0 + enable + enable + + $(NoWarn);MCP9005 + + + + + + + diff --git a/samples/EverythingServer.Core/EverythingServerExtensions.cs b/samples/EverythingServer.Core/EverythingServerExtensions.cs new file mode 100644 index 000000000..05ba3698c --- /dev/null +++ b/samples/EverythingServer.Core/EverythingServerExtensions.cs @@ -0,0 +1,149 @@ +using EverythingServer.Core.Prompts; +using EverythingServer.Core.Resources; +using EverythingServer.Core.Tools; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using System.Collections.Concurrent; + +namespace EverythingServer.Core; + +/// +/// Extension methods for configuring the EverythingServer MCP handlers. +/// +public static class EverythingServerExtensions +{ + /// + /// Adds all the tools, prompts, resources, and handlers for the EverythingServer. + /// + /// The MCP server builder. + /// The subscriptions dictionary to use for managing resource subscriptions. + /// The MCP server builder for chaining. + public static IMcpServerBuilder AddEverythingMcpHandlers( + this IMcpServerBuilder builder, + ConcurrentDictionary> subscriptions) + { + return builder + .WithTools() + .WithTools() + .WithTools() + .WithTools() + .WithTools() + .WithTools() + .WithTools() + .WithPrompts() + .WithPrompts() + .WithResources() + .WithSubscribeToResourcesHandler(async (ctx, ct) => + { + var sessionId = ctx.Server.SessionId ?? "stdio"; + + if (!subscriptions.TryGetValue(sessionId, out var sessionSubscriptions)) + { + sessionSubscriptions = new ConcurrentDictionary(); + subscriptions[sessionId] = sessionSubscriptions; + } + + if (ctx.Params?.Uri is { } uri) + { + sessionSubscriptions.TryAdd(uri, 0); + + await ctx.Server.SampleAsync([ + new ChatMessage(ChatRole.System, "You are a helpful test server"), + new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"), + ], + chatOptions: new ChatOptions + { + MaxOutputTokens = 100, + Temperature = 0.7f, + }, + cancellationToken: ct); + } + + return new EmptyResult(); + }) + .WithUnsubscribeFromResourcesHandler(async (ctx, ct) => + { + var sessionId = ctx.Server.SessionId ?? "stdio"; + + if (subscriptions.TryGetValue(sessionId, out var sessionSubscriptions)) + { + if (ctx.Params?.Uri is { } uri) + { + sessionSubscriptions.TryRemove(uri, out _); + } + } + return new EmptyResult(); + }) + .WithCompleteHandler(async (ctx, ct) => + { + var exampleCompletions = new Dictionary> + { + { "style", ["casual", "formal", "technical", "friendly"] }, + { "temperature", ["0", "0.5", "0.7", "1.0"] }, + { "resourceId", ["1", "2", "3", "4", "5"] } + }; + + if (ctx.Params is not { } @params) + { + throw new NotSupportedException($"Params are required."); + } + + var @ref = @params.Ref; + var argument = @params.Argument; + + if (@ref is ResourceTemplateReference rtr) + { + var resourceId = rtr.Uri?.Split("/").Last(); + + if (resourceId is null) + { + return new CompleteResult(); + } + + var values = exampleCompletions["resourceId"].Where(id => id.StartsWith(argument.Value)); + + return new CompleteResult + { + Completion = new Completion { Values = [.. values], HasMore = false, Total = values.Count() } + }; + } + + if (@ref is PromptReference pr) + { + if (!exampleCompletions.TryGetValue(argument.Name, out IEnumerable? value)) + { + throw new NotSupportedException($"Unknown argument name: {argument.Name}"); + } + + var values = value.Where(value => value.StartsWith(argument.Value)); + return new CompleteResult + { + Completion = new Completion { Values = [.. values], HasMore = false, Total = values.Count() } + }; + } + + throw new NotSupportedException($"Unknown reference type: {@ref.Type}"); + }) + .WithSetLoggingLevelHandler(async (ctx, ct) => + { + if (ctx.Params?.Level is null) + { + throw new McpProtocolException("Missing required argument 'level'", McpErrorCode.InvalidParams); + } + + // 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 {ctx.Params.Level}", + }, cancellationToken: ct); + + return new EmptyResult(); + }); + } +} diff --git a/samples/EverythingServer/LoggingUpdateMessageSender.cs b/samples/EverythingServer.Core/LoggingUpdateMessageSender.cs similarity index 97% rename from samples/EverythingServer/LoggingUpdateMessageSender.cs rename to samples/EverythingServer.Core/LoggingUpdateMessageSender.cs index f8c959e71..422166046 100644 --- a/samples/EverythingServer/LoggingUpdateMessageSender.cs +++ b/samples/EverythingServer.Core/LoggingUpdateMessageSender.cs @@ -3,7 +3,7 @@ using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; -namespace EverythingServer; +namespace EverythingServer.Core; public class LoggingUpdateMessageSender(McpServer server) : BackgroundService { diff --git a/samples/EverythingServer/Prompts/ComplexPromptType.cs b/samples/EverythingServer.Core/Prompts/ComplexPromptType.cs similarity index 91% rename from samples/EverythingServer/Prompts/ComplexPromptType.cs rename to samples/EverythingServer.Core/Prompts/ComplexPromptType.cs index 8b47a07e6..d17bfa68d 100644 --- a/samples/EverythingServer/Prompts/ComplexPromptType.cs +++ b/samples/EverythingServer.Core/Prompts/ComplexPromptType.cs @@ -1,9 +1,9 @@ -using EverythingServer.Tools; +using EverythingServer.Core.Tools; using Microsoft.Extensions.AI; using ModelContextProtocol.Server; using System.ComponentModel; -namespace EverythingServer.Prompts; +namespace EverythingServer.Core.Prompts; [McpServerPromptType] public class ComplexPromptType diff --git a/samples/EverythingServer/Prompts/SimplePromptType.cs b/samples/EverythingServer.Core/Prompts/SimplePromptType.cs similarity index 91% rename from samples/EverythingServer/Prompts/SimplePromptType.cs rename to samples/EverythingServer.Core/Prompts/SimplePromptType.cs index 42b3fa3b5..c6f146b9b 100644 --- a/samples/EverythingServer/Prompts/SimplePromptType.cs +++ b/samples/EverythingServer.Core/Prompts/SimplePromptType.cs @@ -1,7 +1,7 @@ using ModelContextProtocol.Server; using System.ComponentModel; -namespace EverythingServer.Prompts; +namespace EverythingServer.Core.Prompts; [McpServerPromptType] public class SimplePromptType diff --git a/samples/EverythingServer.Core/README.md b/samples/EverythingServer.Core/README.md new file mode 100644 index 000000000..b8f7e3d98 --- /dev/null +++ b/samples/EverythingServer.Core/README.md @@ -0,0 +1,42 @@ +# EverythingServer.Core + +This is the core library containing all the shared MCP server handlers (tools, prompts, resources) used by both the HTTP and stdio implementations of the EverythingServer sample. + +## What's Inside + +This library contains: + +- **Tools**: Various example tools demonstrating different MCP capabilities + - `AddTool`: Simple addition operation + - `AnnotatedMessageTool`: Returns annotated messages + - `EchoTool`: Echoes input back to the client + - `LongRunningTool`: Demonstrates long-running operations with progress reporting + - `PrintEnvTool`: Prints environment variables + - `SampleLlmTool`: Example LLM sampling integration + - `TinyImageTool`: Returns image content + +- **Prompts**: Example prompts showing argument handling + - `SimplePromptType`: Basic prompt example + - `ComplexPromptType`: Prompt with multiple arguments + +- **Resources**: Example resources with subscriptions + - `SimpleResourceType`: Dynamic resource with URI template matching + +- **Background Services**: For managing subscriptions and logging + - `SubscriptionMessageSender`: Sends periodic updates to subscribed resources + - `LoggingUpdateMessageSender`: Sends periodic logging messages at different levels + +- **Extension Method**: `AddEverythingMcpHandlers` to configure all handlers in one call + +## Usage + +Reference this project from your MCP server implementation (HTTP or stdio) and call the extension method: + +```csharp +builder.Services + .AddMcpServer() + .WithHttpTransport() // or WithStdioServerTransport() + .AddEverythingMcpHandlers(subscriptions); +``` + +See the `EverythingServer.Http` and `EverythingServer.Stdio` projects for complete examples. diff --git a/samples/EverythingServer/ResourceGenerator.cs b/samples/EverythingServer.Core/ResourceGenerator.cs similarity index 97% rename from samples/EverythingServer/ResourceGenerator.cs rename to samples/EverythingServer.Core/ResourceGenerator.cs index 524f35063..03fd69453 100644 --- a/samples/EverythingServer/ResourceGenerator.cs +++ b/samples/EverythingServer.Core/ResourceGenerator.cs @@ -1,6 +1,6 @@ using ModelContextProtocol.Protocol; -namespace EverythingServer; +namespace EverythingServer.Core; static class ResourceGenerator { diff --git a/samples/EverythingServer/Resources/SimpleResourceType.cs b/samples/EverythingServer.Core/Resources/SimpleResourceType.cs similarity index 97% rename from samples/EverythingServer/Resources/SimpleResourceType.cs rename to samples/EverythingServer.Core/Resources/SimpleResourceType.cs index cc1aed715..88ee41b20 100644 --- a/samples/EverythingServer/Resources/SimpleResourceType.cs +++ b/samples/EverythingServer.Core/Resources/SimpleResourceType.cs @@ -3,7 +3,7 @@ using System.ComponentModel; using System.Text; -namespace EverythingServer.Resources; +namespace EverythingServer.Core.Resources; [McpServerResourceType] public class SimpleResourceType diff --git a/samples/EverythingServer/SubscriptionMessageSender.cs b/samples/EverythingServer.Core/SubscriptionMessageSender.cs similarity index 76% rename from samples/EverythingServer/SubscriptionMessageSender.cs rename to samples/EverythingServer.Core/SubscriptionMessageSender.cs index 6cb895905..9f047ad51 100644 --- a/samples/EverythingServer/SubscriptionMessageSender.cs +++ b/samples/EverythingServer.Core/SubscriptionMessageSender.cs @@ -1,8 +1,11 @@ using System.Collections.Concurrent; +using Microsoft.Extensions.Hosting; using ModelContextProtocol; using ModelContextProtocol.Server; -internal class SubscriptionMessageSender(McpServer server, ConcurrentDictionary subscriptions) : BackgroundService +namespace EverythingServer.Core; + +public class SubscriptionMessageSender(McpServer server, ConcurrentDictionary subscriptions) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { diff --git a/samples/EverythingServer/Tools/AddTool.cs b/samples/EverythingServer.Core/Tools/AddTool.cs similarity index 91% rename from samples/EverythingServer/Tools/AddTool.cs rename to samples/EverythingServer.Core/Tools/AddTool.cs index 097848773..4da9691af 100644 --- a/samples/EverythingServer/Tools/AddTool.cs +++ b/samples/EverythingServer.Core/Tools/AddTool.cs @@ -1,7 +1,7 @@ using ModelContextProtocol.Server; using System.ComponentModel; -namespace EverythingServer.Tools; +namespace EverythingServer.Core.Tools; [McpServerToolType] public class AddTool diff --git a/samples/EverythingServer/Tools/AnnotatedMessageTool.cs b/samples/EverythingServer.Core/Tools/AnnotatedMessageTool.cs similarity index 97% rename from samples/EverythingServer/Tools/AnnotatedMessageTool.cs rename to samples/EverythingServer.Core/Tools/AnnotatedMessageTool.cs index 4ebdf15ff..b2b849d94 100644 --- a/samples/EverythingServer/Tools/AnnotatedMessageTool.cs +++ b/samples/EverythingServer.Core/Tools/AnnotatedMessageTool.cs @@ -3,7 +3,7 @@ using System.ComponentModel; using System.Text; -namespace EverythingServer.Tools; +namespace EverythingServer.Core.Tools; [McpServerToolType] public class AnnotatedMessageTool diff --git a/samples/EverythingServer/Tools/EchoTool.cs b/samples/EverythingServer.Core/Tools/EchoTool.cs similarity index 87% rename from samples/EverythingServer/Tools/EchoTool.cs rename to samples/EverythingServer.Core/Tools/EchoTool.cs index 6abd6d363..425bd560f 100644 --- a/samples/EverythingServer/Tools/EchoTool.cs +++ b/samples/EverythingServer.Core/Tools/EchoTool.cs @@ -1,7 +1,7 @@ using ModelContextProtocol.Server; using System.ComponentModel; -namespace EverythingServer.Tools; +namespace EverythingServer.Core.Tools; [McpServerToolType] public class EchoTool diff --git a/samples/EverythingServer/Tools/LongRunningTool.cs b/samples/EverythingServer.Core/Tools/LongRunningTool.cs similarity index 96% rename from samples/EverythingServer/Tools/LongRunningTool.cs rename to samples/EverythingServer.Core/Tools/LongRunningTool.cs index 3dc42b5cf..6341dcc6b 100644 --- a/samples/EverythingServer/Tools/LongRunningTool.cs +++ b/samples/EverythingServer.Core/Tools/LongRunningTool.cs @@ -3,7 +3,7 @@ using ModelContextProtocol.Server; using System.ComponentModel; -namespace EverythingServer.Tools; +namespace EverythingServer.Core.Tools; [McpServerToolType] public class LongRunningTool diff --git a/samples/EverythingServer/Tools/PrintEnvTool.cs b/samples/EverythingServer.Core/Tools/PrintEnvTool.cs similarity index 92% rename from samples/EverythingServer/Tools/PrintEnvTool.cs rename to samples/EverythingServer.Core/Tools/PrintEnvTool.cs index ca289b5f3..657135bf1 100644 --- a/samples/EverythingServer/Tools/PrintEnvTool.cs +++ b/samples/EverythingServer.Core/Tools/PrintEnvTool.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Text.Json; -namespace EverythingServer.Tools; +namespace EverythingServer.Core.Tools; [McpServerToolType] public class PrintEnvTool diff --git a/samples/EverythingServer/Tools/SampleLlmTool.cs b/samples/EverythingServer.Core/Tools/SampleLlmTool.cs similarity index 97% rename from samples/EverythingServer/Tools/SampleLlmTool.cs rename to samples/EverythingServer.Core/Tools/SampleLlmTool.cs index 8a23ca499..df2a07a97 100644 --- a/samples/EverythingServer/Tools/SampleLlmTool.cs +++ b/samples/EverythingServer.Core/Tools/SampleLlmTool.cs @@ -2,7 +2,7 @@ using ModelContextProtocol.Server; using System.ComponentModel; -namespace EverythingServer.Tools; +namespace EverythingServer.Core.Tools; [McpServerToolType] public class SampleLlmTool diff --git a/samples/EverythingServer/Tools/TinyImageTool.cs b/samples/EverythingServer.Core/Tools/TinyImageTool.cs similarity index 99% rename from samples/EverythingServer/Tools/TinyImageTool.cs rename to samples/EverythingServer.Core/Tools/TinyImageTool.cs index bd88ce989..390242240 100644 --- a/samples/EverythingServer/Tools/TinyImageTool.cs +++ b/samples/EverythingServer.Core/Tools/TinyImageTool.cs @@ -2,7 +2,7 @@ using ModelContextProtocol.Server; using System.ComponentModel; -namespace EverythingServer.Tools; +namespace EverythingServer.Core.Tools; [McpServerToolType] public class TinyImageTool diff --git a/samples/EverythingServer/EverythingServer.csproj b/samples/EverythingServer.Http/EverythingServer.Http.csproj similarity index 91% rename from samples/EverythingServer/EverythingServer.csproj rename to samples/EverythingServer.Http/EverythingServer.Http.csproj index 39282f8fd..c8d1eb06e 100644 --- a/samples/EverythingServer/EverythingServer.csproj +++ b/samples/EverythingServer.Http/EverythingServer.Http.csproj @@ -19,6 +19,7 @@ + diff --git a/samples/EverythingServer/EverythingServer.http b/samples/EverythingServer.Http/EverythingServer.http similarity index 100% rename from samples/EverythingServer/EverythingServer.http rename to samples/EverythingServer.Http/EverythingServer.http diff --git a/samples/EverythingServer.Http/Program.cs b/samples/EverythingServer.Http/Program.cs new file mode 100644 index 000000000..0cd5f8e73 --- /dev/null +++ b/samples/EverythingServer.Http/Program.cs @@ -0,0 +1,63 @@ +using EverythingServer.Core; +using ModelContextProtocol.Server; +using OpenTelemetry; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using System.Collections.Concurrent; + +var builder = WebApplication.CreateBuilder(args); + +// 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> subscriptions = new(); + +builder.Services + .AddMcpServer() + .WithHttpTransport(options => + { + // Add a RunSessionHandler to remove all subscriptions for the session when it ends + #pragma warning disable MCPEXP002 // RunSessionHandler is experimental + 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(); + // 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 _); + } + }; + #pragma warning restore MCPEXP002 + }) + .AddEverythingMcpHandlers(subscriptions); + +ResourceBuilder resource = ResourceBuilder.CreateDefault().AddService("everything-server"); +builder.Services.AddOpenTelemetry() + .WithTracing(b => b.AddSource("*").AddHttpClientInstrumentation().SetResourceBuilder(resource)) + .WithMetrics(b => b.AddMeter("*").AddHttpClientInstrumentation().SetResourceBuilder(resource)) + .WithLogging(b => b.SetResourceBuilder(resource)) + .UseOtlpExporter(); + +var app = builder.Build(); + +app.MapMcp(); + +app.Run(); diff --git a/samples/EverythingServer/Properties/launchSettings.json b/samples/EverythingServer.Http/Properties/launchSettings.json similarity index 82% rename from samples/EverythingServer/Properties/launchSettings.json rename to samples/EverythingServer.Http/Properties/launchSettings.json index 74cf457ef..c0e380ee8 100644 --- a/samples/EverythingServer/Properties/launchSettings.json +++ b/samples/EverythingServer.Http/Properties/launchSettings.json @@ -6,7 +6,7 @@ "dotnetRunMessages": true, "applicationUrl": "http://localhost:3001", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_ENVIRONMENT": "Development" } }, "https": { @@ -14,7 +14,7 @@ "dotnetRunMessages": true, "applicationUrl": "https://localhost:7133;http://localhost:3001", "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/samples/EverythingServer.Http/README.md b/samples/EverythingServer.Http/README.md new file mode 100644 index 000000000..0f8f6d17d --- /dev/null +++ b/samples/EverythingServer.Http/README.md @@ -0,0 +1,47 @@ +# EverythingServer.Http + +HTTP-based MCP server demonstrating all MCP capabilities using ASP.NET Core. + +## Overview + +This is the HTTP transport implementation of the EverythingServer sample. It demonstrates how to build an MCP server that communicates over HTTP with support for: + +- Multiple concurrent sessions +- Resource subscriptions with per-session tracking +- Long-running operations with progress reporting +- Sampling (LLM integration) +- Logging level management +- OpenTelemetry integration + +## Running the Server + +```bash +dotnet run --project samples/EverythingServer.Http/EverythingServer.csproj +``` + +By default, the server runs on `http://localhost:5000` and `https://localhost:5001`. + +You can configure the port and other settings in `appsettings.json` or via command-line arguments: + +```bash +dotnet run --project samples/EverythingServer.Http/EverythingServer.csproj --urls "http://localhost:3000" +``` + +## Testing the Server + +Use the included `EverythingServer.http` file with VS Code's REST Client extension or similar tools to test the endpoints. + +## Architecture + +This project uses: +- `EverythingServer.Core` for shared MCP handlers +- ASP.NET Core for HTTP hosting +- `WithHttpTransport()` with `RunSessionHandler` for session management +- Background services started per session for subscriptions and logging updates + +## Key Features + +- **Session Management**: Each HTTP session maintains its own subscription list +- **Resource Subscriptions**: Clients can subscribe to resources and receive periodic updates +- **Logging Messages**: Periodic logging messages at various levels based on server's current logging level +- **Complete MCP Implementation**: All MCP capabilities demonstrated in one server diff --git a/samples/EverythingServer/appsettings.Development.json b/samples/EverythingServer.Http/appsettings.Development.json similarity index 100% rename from samples/EverythingServer/appsettings.Development.json rename to samples/EverythingServer.Http/appsettings.Development.json diff --git a/samples/EverythingServer/appsettings.json b/samples/EverythingServer.Http/appsettings.json similarity index 100% rename from samples/EverythingServer/appsettings.json rename to samples/EverythingServer.Http/appsettings.json diff --git a/samples/EverythingServer.Stdio/EverythingServer.Stdio.csproj b/samples/EverythingServer.Stdio/EverythingServer.Stdio.csproj new file mode 100644 index 000000000..30ac140e5 --- /dev/null +++ b/samples/EverythingServer.Stdio/EverythingServer.Stdio.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + Exe + + + + + + + + + + + + + + + diff --git a/samples/EverythingServer.Stdio/Program.cs b/samples/EverythingServer.Stdio/Program.cs new file mode 100644 index 000000000..844bf781b --- /dev/null +++ b/samples/EverythingServer.Stdio/Program.cs @@ -0,0 +1,50 @@ +using EverythingServer.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Server; +using OpenTelemetry; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using System.Collections.Concurrent; + +var builder = Host.CreateApplicationBuilder(args); + +// 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 +// For stdio mode, we use a single "stdio" key since there's only one session +ConcurrentDictionary> subscriptions = new(); +var stdioSubscriptions = new ConcurrentDictionary(); +subscriptions["stdio"] = stdioSubscriptions; + +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .AddEverythingMcpHandlers(subscriptions); + +// Register background services for stdio mode +builder.Services.AddHostedService(sp => new SubscriptionMessageSender( + sp.GetRequiredService(), + stdioSubscriptions)); +builder.Services.AddHostedService(sp => new LoggingUpdateMessageSender( + sp.GetRequiredService())); + +// Configure logging to write to stderr to avoid interfering with MCP protocol on stdout +builder.Logging.AddConsole(options => +{ + options.LogToStandardErrorThreshold = LogLevel.Trace; +}); + +ResourceBuilder resource = ResourceBuilder.CreateDefault().AddService("everything-server"); +builder.Services.AddOpenTelemetry() + .WithTracing(b => b.AddSource("*").AddHttpClientInstrumentation().SetResourceBuilder(resource)) + .WithMetrics(b => b.AddMeter("*").AddHttpClientInstrumentation().SetResourceBuilder(resource)) + .WithLogging(b => b.SetResourceBuilder(resource)) + .UseOtlpExporter(); + +var app = builder.Build(); + +await app.RunAsync(); diff --git a/samples/EverythingServer.Stdio/README.md b/samples/EverythingServer.Stdio/README.md new file mode 100644 index 000000000..6d6d3b8aa --- /dev/null +++ b/samples/EverythingServer.Stdio/README.md @@ -0,0 +1,73 @@ +# EverythingServer.Stdio + +Stdio-based MCP server demonstrating all MCP capabilities using standard input/output communication. + +## Overview + +This is the stdio transport implementation of the EverythingServer sample. It demonstrates how to build an MCP server that communicates via standard input/output (stdio), which is the most common way to integrate MCP servers with desktop applications and AI assistants. + +## Features + +The stdio server includes all the same capabilities as the HTTP version: + +- Tools (add, echo, long-running operations, etc.) +- Prompts (simple and complex with arguments) +- Resources with subscriptions +- Sampling (LLM integration) +- Logging level management +- Progress reporting +- OpenTelemetry integration + +## Running the Server + +```bash +dotnet run --project samples/EverythingServer.Stdio/EverythingServer.Stdio.csproj +``` + +The server will read JSON-RPC messages from stdin and write responses to stdout. All diagnostic logging is sent to stderr to avoid interfering with the MCP protocol. + +## Using with MCP Clients + +To use this server with an MCP client, configure it to launch the executable: + +```json +{ + "mcpServers": { + "everything-server": { + "command": "dotnet", + "args": [ + "run", + "--project", + "path/to/samples/EverythingServer.Stdio/EverythingServer.Stdio.csproj" + ] + } + } +} +``` + +Or if you've published the application: + +```json +{ + "mcpServers": { + "everything-server": { + "command": "path/to/EverythingServer.Stdio" + } + } +} +``` + +## Architecture + +This project uses: +- `EverythingServer.Core` for shared MCP handlers +- Microsoft.Extensions.Hosting for the host builder +- `WithStdioServerTransport()` for stdio communication +- Hosted background services for subscriptions and logging updates +- Console logging configured to write to stderr + +## Key Differences from HTTP Version + +- **Single Session**: Stdio servers typically handle one session per process +- **Hosted Services**: Background services are registered as hosted services instead of being started per-session +- **Logging**: All logs go to stderr to keep stdout clean for MCP protocol messages diff --git a/samples/EverythingServer/Program.cs b/samples/EverythingServer/Program.cs deleted file mode 100644 index abccbac88..000000000 --- a/samples/EverythingServer/Program.cs +++ /dev/null @@ -1,250 +0,0 @@ -using EverythingServer; -using EverythingServer.Prompts; -using EverythingServer.Resources; -using EverythingServer.Tools; -using Microsoft.Extensions.AI; -using ModelContextProtocol.AspNetCore; -using ModelContextProtocol; -using ModelContextProtocol.Protocol; -using ModelContextProtocol.Server; -using OpenTelemetry; -using OpenTelemetry.Logs; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; -using System.Collections.Concurrent; - -var builder = WebApplication.CreateBuilder(args); - -// Note: This sample requires stateful (session-based) mode because it uses: -// - SampleLlmTool: server-to-client sampling via SampleAsync -// - Resource subscriptions: unsolicited notifications via SendNotificationAsync -// - Per-session state: subscription tracking keyed by SessionId -// See https://csharp.sdk.modelcontextprotocol.io/concepts/sessions for details -// on when to prefer stateless mode instead. - -// 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> subscriptions = new(); - -builder.Services - .AddMcpServer(options => - { - // Configure server implementation details with icons and website - options.ServerInfo = new Implementation - { - Name = "Everything Server", - Version = "1.0.0", - Title = "MCP Everything Server", - Description = "A comprehensive MCP server demonstrating tools, prompts, resources, sampling, and all MCP features", - WebsiteUrl = "https://github.com/modelcontextprotocol/csharp-sdk", - Icons = [ - new Icon - { - Source = "https://raw.githubusercontent.com/microsoft/fluentui-emoji/62ecdc0d7ca5c6df32148c169556bc8d3782fca4/assets/Gear/Flat/gear_flat.svg", - MimeType = "image/svg+xml", - Sizes = ["any"], - Theme = "light" - }, - new Icon - { - Source = "https://raw.githubusercontent.com/microsoft/fluentui-emoji/62ecdc0d7ca5c6df32148c169556bc8d3782fca4/assets/Gear/3D/gear_3d.png", - MimeType = "image/png", - Sizes = ["256x256"] - } - ] - }; - }) - .WithHttpTransport(options => - { - // This sample uses subscriptions, SampleLlmTool (sampling), and RunSessionHandler. - // Set SessionMode = HttpServerSessionMode.Stateful since sessions are required for sampling. - options.SessionMode = HttpServerSessionMode.Stateful; - - // Add a RunSessionHandler to remove all subscriptions for the session when it ends -#pragma warning disable MCPEXP002 // RunSessionHandler is experimental - 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(); - // 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 _); - } - }; -#pragma warning restore MCPEXP002 - }) - .WithTools() - .WithTools() - .WithTools([ - // EchoTool with complex icon configuration demonstrating multiple icons, - // MIME types, size specifications, and theme preferences - McpServerTool.Create( - typeof(EchoTool).GetMethod(nameof(EchoTool.Echo))!, - options: new McpServerToolCreateOptions - { - Icons = [ - // High-resolution PNG icon for light theme - new Icon - { - Source = "https://raw.githubusercontent.com/microsoft/fluentui-emoji/62ecdc0d7ca5c6df32148c169556bc8d3782fca4/assets/Loudspeaker/Flat/loudspeaker_flat.svg", - MimeType = "image/svg+xml", - Sizes = ["any"], - Theme = "light" - }, - // 3D icon for dark theme - new Icon - { - Source = "https://raw.githubusercontent.com/microsoft/fluentui-emoji/62ecdc0d7ca5c6df32148c169556bc8d3782fca4/assets/Loudspeaker/3D/loudspeaker_3d.png", - MimeType = "image/png", - Sizes = ["256x256"], - Theme = "dark" - }, - // WebP format for modern browsers - // Demonstrates Data URI representation with the smallest possible valid WebP image (1x1 pixel). - // This will appear as a white box when rendered by a browser at 32x32 - new Icon - { - Source = "data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=", - MimeType = "image/webp", - Sizes = ["32x32"] - } - ] - }) - ]) - .WithTools() - .WithTools() - .WithTools() - .WithTools() - .WithPrompts() - .WithPrompts() - .WithResources() - .WithSubscribeToResourcesHandler(async (ctx, ct) => - { - if (ctx.Server.SessionId == null) - { - 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"), - new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"), - ], - chatOptions: new ChatOptions - { - MaxOutputTokens = 100, - Temperature = 0.7f, - }, - cancellationToken: ct); - } - - return new EmptyResult(); - }) - .WithUnsubscribeFromResourcesHandler(async (ctx, ct) => - { - if (ctx.Server.SessionId == null) - { - throw new McpException("Cannot remove subscription for server with null SessionId"); - } - if (ctx.Params.Uri is { } uri) - { - subscriptions[ctx.Server.SessionId].TryRemove(uri, out _); - } - return new EmptyResult(); - }) - .WithCompleteHandler(async (ctx, ct) => - { - var exampleCompletions = new Dictionary> - { - { "style", ["casual", "formal", "technical", "friendly"] }, - { "temperature", ["0", "0.5", "0.7", "1.0"] }, - { "resourceId", ["1", "2", "3", "4", "5"] } - }; - - if (ctx.Params is not { } @params) - { - throw new NotSupportedException($"Params are required."); - } - - var @ref = @params.Ref; - var argument = @params.Argument; - - if (@ref is ResourceTemplateReference rtr) - { - var resourceId = rtr.Uri?.Split("/").Last(); - - if (resourceId is null) - { - return new CompleteResult(); - } - - var values = exampleCompletions["resourceId"].Where(id => id.StartsWith(argument.Value)); - - return new CompleteResult - { - Completion = new Completion { Values = [.. values], HasMore = false, Total = values.Count() } - }; - } - - if (@ref is PromptReference pr) - { - if (!exampleCompletions.TryGetValue(argument.Name, out IEnumerable? value)) - { - throw new NotSupportedException($"Unknown argument name: {argument.Name}"); - } - - var values = value.Where(value => value.StartsWith(argument.Value)); - return new CompleteResult - { - Completion = new Completion { Values = [.. values], HasMore = false, Total = values.Count() } - }; - } - - throw new NotSupportedException($"Unknown reference type: {@ref.Type}"); - }) - .WithSetLoggingLevelHandler(async (ctx, ct) => - { - // 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 {ctx.Params.Level}", - }, cancellationToken: ct); - - return new EmptyResult(); - }); - -ResourceBuilder resource = ResourceBuilder.CreateDefault().AddService("everything-server"); -builder.Services.AddOpenTelemetry() - .WithTracing(b => b.AddSource("*").AddHttpClientInstrumentation().SetResourceBuilder(resource)) - .WithMetrics(b => b.AddMeter("*").AddHttpClientInstrumentation().SetResourceBuilder(resource)) - .WithLogging(b => b.SetResourceBuilder(resource)) - .UseOtlpExporter(); - -var app = builder.Build(); - -app.MapMcp(); - -app.Run();