diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx index a70e3e310..bf59ac982 100644 --- a/ModelContextProtocol.slnx +++ b/ModelContextProtocol.slnx @@ -42,7 +42,9 @@ - + + + diff --git a/samples/EverythingServer.Core/EverythingServer.Core.csproj b/samples/EverythingServer.Core/EverythingServer.Core.csproj new file mode 100644 index 000000000..cddc8d0b9 --- /dev/null +++ b/samples/EverythingServer.Core/EverythingServer.Core.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/samples/EverythingServer.Core/EverythingServerExtensions.cs b/samples/EverythingServer.Core/EverythingServerExtensions.cs new file mode 100644 index 000000000..730c24db9 --- /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"), + ], + options: 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 88% rename from samples/EverythingServer/Prompts/SimplePromptType.cs rename to samples/EverythingServer.Core/Prompts/SimplePromptType.cs index d6ba51a33..e849cabfb 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 da185425f..7bf9c753a 100644 --- a/samples/EverythingServer/Resources/SimpleResourceType.cs +++ b/samples/EverythingServer.Core/Resources/SimpleResourceType.cs @@ -2,7 +2,7 @@ using ModelContextProtocol.Server; using System.ComponentModel; -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 87% rename from samples/EverythingServer/Tools/AddTool.cs rename to samples/EverythingServer.Core/Tools/AddTool.cs index ccaa306d6..22b06e9ce 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 7f92d0ae1..6ea97aa87 100644 --- a/samples/EverythingServer/Tools/AnnotatedMessageTool.cs +++ b/samples/EverythingServer.Core/Tools/AnnotatedMessageTool.cs @@ -2,7 +2,7 @@ using ModelContextProtocol.Server; using System.ComponentModel; -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 405b5e823..e24b8d529 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 6bbe6e51d..d4a39d3cc 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 87% rename from samples/EverythingServer/EverythingServer.csproj rename to samples/EverythingServer.Http/EverythingServer.Http.csproj index eadf720ca..75abfb542 100644 --- a/samples/EverythingServer/EverythingServer.csproj +++ b/samples/EverythingServer.Http/EverythingServer.Http.csproj @@ -15,6 +15,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..3c7ddd120 --- /dev/null +++ b/samples/EverythingServer.Http/Program.cs @@ -0,0 +1,61 @@ +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 + 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 _); + } + }; + }) + .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 100% rename from samples/EverythingServer/Properties/launchSettings.json rename to samples/EverythingServer.Http/Properties/launchSettings.json 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 acaa7a37c..000000000 --- a/samples/EverythingServer/Program.cs +++ /dev/null @@ -1,180 +0,0 @@ -using EverythingServer; -using EverythingServer.Prompts; -using EverythingServer.Resources; -using EverythingServer.Tools; -using Microsoft.Extensions.AI; -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); - -// 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 - 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 _); - } - }; - }) - .WithTools() - .WithTools() - .WithTools() - .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"), - ], - options: 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) => - { - 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(); - }); - -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();