# MCP C# SDK The MCP C# SDK is the official .NET implementation of the Model Context Protocol, a standardized protocol that enables seamless integration between Large Language Models and various data sources, tools, and services. This SDK provides both client and server implementations, allowing .NET applications to expose functionality as MCP servers or consume functionality from MCP servers as clients. The protocol operates through a JSON-RPC message transport layer supporting multiple transport mechanisms including stdio, HTTP with Server-Sent Events (SSE), and streamable HTTP. The SDK consists of three packages: ModelContextProtocol.Core for low-level client/server APIs, ModelContextProtocol for dependency injection and hosting extensions compatible with Microsoft.Extensions.Hosting, and ModelContextProtocol.AspNetCore for HTTP-based MCP servers. It features attribute-based tool registration with automatic JSON schema generation, native integration with Microsoft.Extensions.AI for seamless LLM interaction, comprehensive support for tools, prompts, and resources, built-in support for sampling (server-to-client LLM requests), interactive elicitation for requesting user input during tool execution, and full AOT compilation support through generic APIs. ## Creating an MCP Client with Stdio Transport Connect to an MCP server running as a child process via standard input/output streams. ```csharp using ModelContextProtocol.Client; // Configure transport to launch server process var transport = new StdioClientTransport(new StdioClientTransportOptions { Name = "Weather Server", Command = "npx", Arguments = ["-y", "@modelcontextprotocol/server-everything"], WorkingDirectory = "/path/to/server", EnvironmentVariables = new Dictionary { ["API_KEY"] = "your-api-key" } }); // Create and initialize client var client = await McpClient.CreateAsync( transport, clientOptions: new McpClientOptions { ClientInfo = new Implementation { Name = "MyClient", Version = "1.0.0" }, InitializationTimeout = TimeSpan.FromSeconds(30) }, cancellationToken: cancellationToken); // Client is now connected and ready to use Console.WriteLine($"Connected to: {client.ServerInfo.Name} v{client.ServerInfo.Version}"); Console.WriteLine($"Server instructions: {client.ServerInstructions}"); // Cleanup await client.DisposeAsync(); ``` ## Creating an MCP Client with HTTP Transport Connect to an MCP server via HTTP using Server-Sent Events or streamable HTTP protocol. ```csharp using ModelContextProtocol.Client; // Configure HTTP transport with auto-detection var transport = new HttpClientTransport(new HttpClientTransportOptions { Endpoint = new Uri("http://localhost:3001/mcp"), TransportMode = HttpTransportMode.AutoDetect, ConnectionTimeout = TimeSpan.FromSeconds(30), RequestTimeout = TimeSpan.FromMinutes(5), OAuth = new ClientOAuthOptions { ClientId = "your-client-id", ClientSecret = "your-client-secret" } }); // Create client with custom HttpClient if needed using var httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(10) }; var transport2 = new HttpClientTransport( new HttpClientTransportOptions { Endpoint = new Uri("http://localhost:3001/mcp") }, httpClient, ownsHttpClient: true); var client = await McpClient.CreateAsync(transport); // Verify server capabilities if (client.ServerCapabilities.Tools?.ListChanged == true) { Console.WriteLine("Server supports dynamic tool updates"); } await client.DisposeAsync(); ``` ## Listing and Calling Tools Discover available tools from a server and invoke them with arguments. ```csharp using ModelContextProtocol.Client; using System.Text.Json; var client = await McpClient.CreateAsync(transport); // List all tools - returns IList for immediate full enumeration IList tools = await client.ListToolsAsync(); foreach (var tool in tools) { Console.WriteLine($"Tool: {tool.Name}"); Console.WriteLine($" Description: {tool.Description}"); Console.WriteLine($" Schema: {tool.InputSchema}"); } // Enumerate tools lazily with pagination await foreach (var tool in client.EnumerateToolsAsync(cancellationToken: cancellationToken)) { Console.WriteLine($"Discovered: {tool.Name}"); } // Call a tool with arguments var arguments = new Dictionary { ["latitude"] = 40.7128, ["longitude"] = -74.0060, ["units"] = "fahrenheit" }; CallToolResult result = await client.CallToolAsync( "GetForecast", arguments, progress: new Progress(p => Console.WriteLine($"Progress: {p.Progress}/{p.Total}")), cancellationToken: cancellationToken); // Process results foreach (var content in result.Content) { if (content.Type == "text" && content.Text is string text) { Console.WriteLine($"Result: {text}"); } else if (content.Type == "image" && content.Data is string imageData) { Console.WriteLine($"Image: {content.MimeType}"); // imageData is base64 encoded } } ``` ## Using Tools with Microsoft.Extensions.AI Chat Clients Seamlessly integrate MCP tools with any IChatClient implementation for automatic tool calling. ```csharp using Microsoft.Extensions.AI; using ModelContextProtocol.Client; using OpenAI; // Setup MCP client var mcpClient = await McpClient.CreateAsync(transport); // Get tools - McpClientTool inherits from AIFunction IList tools = await mcpClient.ListToolsAsync(); // Setup any IChatClient (OpenAI, Anthropic, etc.) var openAIClient = new OpenAIClient(apiKey) .GetChatClient("gpt-4o") .AsIChatClient() .AsBuilder() .UseFunctionInvocation() // Enable automatic tool calling .Build(); // Chat with tool support List messages = [ new(ChatRole.User, "What's the weather forecast for New York City?") ]; var response = await openAIClient.GetResponseAsync( messages, new ChatOptions { Tools = [.. tools], // Pass MCP tools directly Temperature = 0.7f, MaxOutputTokens = 1000 }, cancellationToken: cancellationToken); Console.WriteLine(response.Message.Text); // Streaming with tools await foreach (var update in openAIClient.GetStreamingResponseAsync( messages, new ChatOptions { Tools = [.. tools] })) { Console.Write(update.Text); } ``` ## Listing and Getting Prompts Retrieve reusable prompt templates with parameters from MCP servers. ```csharp using ModelContextProtocol.Client; var client = await McpClient.CreateAsync(transport); // List all available prompts IList prompts = await client.ListPromptsAsync(cancellationToken); foreach (var prompt in prompts) { Console.WriteLine($"Prompt: {prompt.Name} - {prompt.Description}"); if (prompt.Arguments is not null) { foreach (var arg in prompt.Arguments) { Console.WriteLine($" Arg: {arg.Name} ({arg.Type}) - {arg.Description}"); Console.WriteLine($" Required: {arg.Required}"); } } } // Get a prompt without arguments GetPromptResult simplePrompt = await client.GetPromptAsync( "summarize", cancellationToken: cancellationToken); foreach (var message in simplePrompt.Messages) { Console.WriteLine($"{message.Role}: {message.Content.Text}"); } // Get a prompt with arguments var promptArgs = new Dictionary { ["temperature"] = 0.8, ["style"] = "formal", ["max_length"] = 500 }; GetPromptResult complexPrompt = await client.GetPromptAsync( "custom_writer", promptArgs, serializerOptions: new JsonSerializerOptions { PropertyNameCaseInsensitive = true }, cancellationToken: cancellationToken); // Use prompt messages with chat client var chatMessages = complexPrompt.Messages.Select(m => new ChatMessage(m.Role switch { "user" => ChatRole.User, "assistant" => ChatRole.Assistant, _ => ChatRole.System }, m.Content.Text)).ToList(); ``` ## Reading Resources Access server-provided resources like files, database records, or API data. ```csharp using ModelContextProtocol.Client; var client = await McpClient.CreateAsync(transport); // List all available resources IList resources = await client.ListResourcesAsync(cancellationToken); foreach (var resource in resources) { Console.WriteLine($"Resource: {resource.Name}"); Console.WriteLine($" URI: {resource.Uri}"); Console.WriteLine($" MIME Type: {resource.MimeType}"); Console.WriteLine($" Description: {resource.Description}"); } // Read a static resource by URI ReadResourceResult result = await client.ReadResourceAsync( "file:///data/config.json", cancellationToken); foreach (var content in result.Contents) { if (content.Type == "text") { Console.WriteLine($"Text content: {content.Text}"); } else if (content.Type == "blob") { Console.WriteLine($"Binary data: {content.MimeType}, {content.Blob?.Length} bytes"); } } // List resource templates (dynamic URIs with parameters) IList templates = await client.ListResourceTemplatesAsync(cancellationToken); foreach (var template in templates) { Console.WriteLine($"Template: {template.UriTemplate}"); } // Read from a resource template with parameters var templateArgs = new Dictionary { ["user_id"] = "12345", ["format"] = "json" }; ReadResourceResult templateResult = await client.ReadResourceAsync( "user://profile/{user_id}.{format}", templateArgs, cancellationToken); ``` ## Subscribing to Resource Updates Monitor resources for changes with real-time notifications from the server. ```csharp using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; var client = await McpClient.CreateAsync(transport); // Register notification handler for resource updates using var subscription = client.RegisterNotificationHandler( "notifications/resources/updated", async (notification, ct) => { var uri = notification.Params?.GetProperty("uri").GetString(); Console.WriteLine($"Resource updated: {uri}"); // Re-read the updated resource var updatedContent = await client.ReadResourceAsync(uri!, ct); Console.WriteLine($"New content: {updatedContent.Contents[0].Text}"); }); // Subscribe to specific resources await client.SubscribeToResourceAsync("file:///data/config.json", cancellationToken); await client.SubscribeToResourceAsync("database://users/active", cancellationToken); // Server will now send notifications when these resources change // Keep application running to receive notifications await Task.Delay(TimeSpan.FromHours(1), cancellationToken); // Unsubscribe when done await client.UnsubscribeFromResourceAsync("file:///data/config.json", cancellationToken); ``` ## Handling Elicitation Requests in Clients Enable clients to respond to server requests for additional user input during tool execution. ```csharp using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; // Configure client with elicitation support var client = await McpClient.CreateAsync( transport, clientOptions: new McpClientOptions { ClientInfo = new Implementation { Name = "InteractiveClient", Version = "1.0" }, Capabilities = new ClientCapabilities { Elicitation = new ElicitationCapability { Form = new FormElicitationCapability(), // Support form-based input Url = new UrlElicitationCapability() // Support URL navigation } }, Handlers = new McpClientHandlers { ElicitationHandler = HandleElicitationAsync } }); // Elicitation handler implementation async Task HandleElicitationAsync( ElicitRequestParams? request, CancellationToken cancellationToken) { if (request?.Mode == "form") { // Handle form mode - collect structured data from user Console.WriteLine($"Server requests: {request.Message}"); var responses = new Dictionary(); if (request.RequestedSchema?.Properties is not null) { foreach (var (name, schema) in request.RequestedSchema.Properties) { Console.Write($"{schema.Description ?? name}: "); var input = Console.ReadLine(); // Parse input based on schema type responses[name] = schema switch { BooleanSchema => bool.Parse(input ?? "false"), NumberSchema => double.Parse(input ?? "0"), _ => input }; } } return new ElicitResult { Action = "accept", Content = responses }; } else if (request?.Mode == "url") { // Handle URL mode - navigate user to external URL Console.WriteLine($"Server requests URL navigation: {request.Message}"); Console.WriteLine($"URL: {request.Url}"); Console.Write("Open this URL? (y/n): "); var consent = Console.ReadLine(); if (consent?.ToLower() == "y") { Process.Start(new ProcessStartInfo(request.Url!) { UseShellExecute = true }); return new ElicitResult { Action = "accept" }; } return new ElicitResult { Action = "decline" }; } return new ElicitResult { Action = "reject" }; } ``` ## Creating a Basic MCP Server with Hosting Build an MCP server using Microsoft.Extensions.Hosting with stdio transport. ```csharp using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using ModelContextProtocol.Server; using System.ComponentModel; var builder = Host.CreateApplicationBuilder(args); // Configure logging to stderr (stdout is used for MCP protocol) builder.Logging.AddConsole(options => { options.LogToStandardErrorThreshold = LogLevel.Trace; }); // Register MCP server with stdio transport and tools builder.Services .AddMcpServer(options => { options.ServerInfo = new Implementation { Name = "WeatherServer", Version = "1.0.0" }; options.ServerInstructions = "A server providing weather information tools."; }) .WithStdioServerTransport() .WithToolsFromAssembly(); // Auto-discover tools in current assembly // Register dependencies used by tools builder.Services.AddHttpClient(); await builder.Build().RunAsync(); // Tool definition in same or separate file [McpServerToolType] public static class WeatherTool { [McpServerTool, Description("Get current weather for a city")] public static async Task GetWeather( [Description("City name")] string city, [Description("Country code (e.g., US)")] string? country = null, HttpClient? httpClient = null, // Injected via DI CancellationToken cancellationToken = default) { var location = country != null ? $"{city},{country}" : city; var response = await httpClient!.GetStringAsync( $"https://api.weather.com/v1/current?location={location}", cancellationToken); return response; } } ``` ## Creating an AspNetCore MCP Server with HTTP Transport Build an HTTP-based MCP server using ASP.NET Core with multiple tools and resources. ```csharp using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.Server; using System.ComponentModel; using System.Net.Http.Headers; var builder = WebApplication.CreateBuilder(args); // Configure MCP server with HTTP transport builder.Services.AddMcpServer(options => { options.ServerInfo = new Implementation { Name = "DataAnalysisServer", Version = "2.0.0" }; }) .WithHttpTransport(httpOptions => { httpOptions.Stateless = false; // Enable stateful sessions httpOptions.IdleTimeout = TimeSpan.FromMinutes(30); httpOptions.OnSessionStart = async (httpContext) => { var userId = httpContext.User.Identity?.Name; Console.WriteLine($"Session started for user: {userId}"); }; }) .AddAuthorizationFilters() // Enable [Authorize] on tools .WithTools() .WithTools() .WithPrompts() .WithResources(); // Configure HttpClient for tools builder.Services.AddHttpClient("ExternalAPI", client => { client.BaseAddress = new Uri("https://api.example.com"); client.DefaultRequestHeaders.UserAgent.Add( new ProductInfoHeaderValue("MCPServer", "1.0")); }); builder.Services.AddAuthorization(); builder.Services.AddAuthentication(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); // Map MCP endpoints at /mcp app.MapMcp("/mcp"); app.Run(); ``` ## Defining Tools with Attributes and Dependency Injection Create tools using attributes with automatic schema generation and DI support. ```csharp using ModelContextProtocol; using ModelContextProtocol.Server; using System.ComponentModel; // Mark class as containing MCP tools [McpServerToolType] public sealed class WeatherTools { private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; // Constructor injection of dependencies public WeatherTools(IHttpClientFactory httpClientFactory, ILogger logger) { _httpClientFactory = httpClientFactory; _logger = logger; } [McpServerTool( Name = "get_alerts", Title = "Weather Alerts", ReadOnly = true, Idempotent = true)] [Description("Get weather alerts for a US state")] [McpMeta("category", "weather")] // Custom metadata [McpMeta("dataSource", "weather.gov")] public async Task GetAlerts( [Description("Two-letter state abbreviation (e.g., NY)")] string state, CancellationToken cancellationToken = default) { _logger.LogInformation("Fetching alerts for {State}", state); var client = _httpClientFactory.CreateClient("WeatherApi"); var response = await client.GetFromJsonAsync( $"/alerts/active/area/{state}", cancellationToken); if (response?.Features.Count == 0) { return "No active alerts"; } return string.Join("\n---\n", response.Features.Select(f => $"Event: {f.Properties.Event}\nSeverity: {f.Properties.Severity}\n" + $"Description: {f.Properties.Description}")); } [McpServerTool, Description("Get weather forecast for coordinates")] public async Task GetForecast( [Description("Latitude")] double latitude, [Description("Longitude")] double longitude, [Description("Number of periods")] int periods = 7) { if (latitude < -90 || latitude > 90) { throw new McpException("Latitude must be between -90 and 90"); } // Implementation return $"Forecast for {latitude},{longitude}: ..."; } } public record AlertResponse(List Features); public record AlertFeature(AlertProperties Properties); public record AlertProperties(string Event, string Severity, string Description); ``` ## Implementing Interactive Tools with Elicitation Enable servers to request additional information from users during tool execution. ```csharp using ModelContextProtocol.Server; using ModelContextProtocol.Protocol; using System.ComponentModel; [McpServerToolType] public static class InteractiveTools { [McpServerTool, Description("Interactive number guessing game")] public static async Task GuessTheNumber( McpServer server, CancellationToken cancellationToken) { var secretNumber = new Random().Next(1, 101); int attempts = 0; int maxAttempts = 5; while (attempts < maxAttempts) { // Request user input via form elicitation var elicitResult = await server.ElicitAsync( new ElicitRequestParams { Mode = "form", Message = $"Guess a number between 1 and 100 (Attempt {attempts + 1}/{maxAttempts})", RequestedSchema = new ElicitRequestParams.RequestSchema { Properties = new Dictionary { ["guess"] = new ElicitRequestParams.NumberSchema { Description = "Your guess", Minimum = 1, Maximum = 100 } }, Required = ["guess"] } }, cancellationToken); if (elicitResult.Action != "accept" || elicitResult.Content is null) { return "Game cancelled"; } attempts++; var guess = Convert.ToInt32(elicitResult.Content["guess"]); if (guess == secretNumber) { return $"Correct! You guessed the number in {attempts} attempts!"; } else if (guess < secretNumber) { Console.WriteLine("Too low!"); } else { Console.WriteLine("Too high!"); } } return $"Game over! The number was {secretNumber}"; } [McpServerTool, Description("Request OAuth authorization via URL")] public static async Task ConnectThirdPartyService( McpServer server, [Description("Service name")] string serviceName, CancellationToken cancellationToken) { var elicitationId = Guid.NewGuid().ToString(); var authUrl = $"https://auth.example.com/oauth?service={serviceName}&state={elicitationId}"; // Request URL mode elicitation for OAuth flow var result = await server.ElicitAsync( new ElicitRequestParams { Mode = "url", ElicitationId = elicitationId, Url = authUrl, Message = $"Please authorize access to {serviceName} by logging in through your browser." }, cancellationToken); if (result.Action == "accept") { return $"Authorization successful for {serviceName}"; } return "Authorization declined"; } } ``` ## Defining Prompts with Attributes Create reusable prompt templates with parameters using attribute-based registration. ```csharp using Microsoft.Extensions.AI; using ModelContextProtocol.Server; using System.ComponentModel; [McpServerPromptType] public static class AnalysisPrompts { // Simple prompt without arguments [McpServerPrompt(Name = "summarize")] [Description("A prompt to summarize content")] public static string Summarize() => "Please provide a concise summary of the following content:"; // Prompt with arguments returning string [McpServerPrompt(Name = "code_review")] [Description("Generate a code review prompt")] public static string CodeReview( [Description("Programming language")] string language, [Description("Focus areas")] string[] focusAreas) { var areas = string.Join(", ", focusAreas); return $"Review this {language} code with focus on: {areas}"; } // Complex prompt with chat messages [McpServerPrompt(Name = "data_analysis")] [Description("Multi-turn data analysis prompt")] public static IEnumerable DataAnalysis( [Description("Dataset description")] string dataset, [Description("Analysis type")] string analysisType = "descriptive") { return [ new ChatMessage(ChatRole.System, "You are a data scientist expert in statistical analysis."), new ChatMessage(ChatRole.User, $"I have a dataset: {dataset}. I need a {analysisType} analysis."), new ChatMessage(ChatRole.Assistant, "I'll help you analyze this dataset. What specific metrics are you interested in?") ]; } // Prompt with embedded resource [McpServerPrompt(Name = "visual_analysis")] [Description("Analyze data with visualization")] public static IEnumerable VisualAnalysis(string chartData) { return [ new ChatMessage(ChatRole.User, "Analyze this chart and provide insights:"), new ChatMessage(ChatRole.User, [ new DataContent(chartData, "image/png") // Embedded image ]) ]; } } ``` ## Defining Resources with Attributes Expose data sources as MCP resources with static URIs or dynamic templates. ```csharp using ModelContextProtocol; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using System.ComponentModel; [McpServerResourceType] public sealed class DataResourceProvider { private readonly IConfiguration _config; private readonly DatabaseContext _db; public DataResourceProvider(IConfiguration config, DatabaseContext db) { _config = config; _db = db; } // Static resource - fixed URI [McpServerResource( UriTemplate = "config://app/settings", Name = "Application Settings", MimeType = "application/json")] [Description("Current application configuration")] public string GetAppSettings() { return JsonSerializer.Serialize(_config.AsEnumerable()); } // Dynamic resource with template parameters [McpServerResource( UriTemplate = "database://users/{userId}/profile", Name = "User Profile")] [Description("Get user profile data")] public async Task GetUserProfile( [Description("User ID")] string userId, CancellationToken cancellationToken = default) { var user = await _db.Users.FindAsync([userId], cancellationToken); if (user == null) { throw new McpException($"User {userId} not found"); } return JsonSerializer.Serialize(user); } // Resource returning structured content [McpServerResource(UriTemplate = "reports://sales/{year}/{month}")] [Description("Monthly sales report")] public async Task GetSalesReport( [Description("Year")] int year, [Description("Month (1-12)")] int month) { var report = await GenerateReportAsync(year, month); return new TextContent { Type = "text", Text = report, MimeType = "text/markdown" }; } // Resource with binary content [McpServerResource(UriTemplate = "files://documents/{docId}.pdf")] [Description("Download document as PDF")] public async Task GetDocument(string docId) { byte[] pdfData = await LoadDocumentAsync(docId); return new BlobContent { Type = "blob", Blob = Convert.ToBase64String(pdfData), MimeType = "application/pdf" }; } private Task GenerateReportAsync(int year, int month) => Task.FromResult($"# Sales Report {year}-{month:D2}\n..."); private Task LoadDocumentAsync(string docId) => Task.FromResult(Array.Empty()); } ``` ## Implementing Server-to-Client Sampling (LLM Requests) Enable MCP servers to request LLM capabilities from connected clients. ```csharp using Microsoft.Extensions.AI; using ModelContextProtocol; using ModelContextProtocol.Server; using System.ComponentModel; [McpServerToolType] public sealed class ContentTools { [McpServerTool, Description("Summarize content from a URL")] public static async Task SummarizeUrl( McpServer server, // Auto-injected server instance HttpClient httpClient, [Description("URL to fetch and summarize")] string url, [Description("Max summary length")] int maxLength = 200, CancellationToken cancellationToken = default) { // Download content string content = await httpClient.GetStringAsync(url, cancellationToken); // Request LLM summarization from client using sampling var messages = new ChatMessage[] { new(ChatRole.User, "Summarize the following content concisely:"), new(ChatRole.User, content) }; var options = new ChatOptions { MaxOutputTokens = maxLength, Temperature = 0.3f, ModelId = "claude-3-5-sonnet-20241022" }; // Use server's sampling chat client to request from connected client IChatClient samplingClient = server.AsSamplingChatClient(); var summary = await samplingClient.GetResponseAsync( messages, options, cancellationToken); return summary.Message.Text ?? "Unable to generate summary"; } // Alternative: Use SampleAsync directly for more control [McpServerTool, Description("Analyze and categorize text")] public static async Task CategorizeText( McpServer server, [Description("Text to categorize")] string text) { var request = new CreateMessageRequestParams { Messages = [ new SamplingMessage { Role = "user", Content = new TextContent { Type = "text", Text = $"Categorize this text into one of: news, tech, sports, entertainment\n\n{text}" } } ], MaxTokens = 50, Temperature = 0.1f }; CreateMessageResult result = await server.SampleAsync(request); return result.Content.Type switch { "text" => result.Content.Text!, _ => "Unable to categorize" }; } } ``` ## Creating Low-Level Servers with Manual Handler Registration Build MCP servers with explicit control over all request handlers and capabilities. ```csharp using ModelContextProtocol; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using System.Text.Json; var options = new McpServerOptions { ServerInfo = new Implementation { Name = "CustomServer", Version = "1.0.0" }, ServerInstructions = "A custom server with manual handlers", ProtocolVersion = "2024-11-05", Capabilities = new ServerCapabilities { Tools = new ToolsCapability { ListChanged = true }, Resources = new ResourcesCapability { Subscribe = true, ListChanged = true }, Prompts = new PromptsCapability { ListChanged = false } }, Handlers = new McpServerHandlers { // Tool handlers ListToolsHandler = (request, cancellationToken) => { var tools = new List { new Tool { Name = "echo", Description = "Echo input back", InputSchema = JsonSerializer.SerializeToElement(new { type = "object", properties = new { message = new { type = "string", description = "Message to echo" } }, required = new[] { "message" } }) } }; return ValueTask.FromResult(new ListToolsResult { Tools = tools }); }, CallToolHandler = (request, cancellationToken) => { if (request.Params?.Name == "echo") { if (!request.Params.Arguments?.TryGetValue("message", out var msg) ?? true) { throw new McpProtocolException( "Missing 'message' argument", McpErrorCode.InvalidParams); } return ValueTask.FromResult(new CallToolResult { Content = [ new TextContentBlock { Type = "text", Text = $"Echo: {msg}" } ] }); } throw new McpProtocolException( $"Unknown tool: {request.Params?.Name}", McpErrorCode.InvalidRequest); }, // Resource handlers ListResourcesHandler = (request, cancellationToken) => { return ValueTask.FromResult(new ListResourcesResult { Resources = [ new Resource { Uri = "memory://data/config", Name = "Configuration", MimeType = "application/json", Description = "Server configuration" } ] }); }, ReadResourceHandler = (request, cancellationToken) => { if (request.Params?.Uri == "memory://data/config") { return ValueTask.FromResult(new ReadResourceResult { Contents = [ new TextContent { Type = "text", Uri = request.Params.Uri, Text = JsonSerializer.Serialize(new { setting1 = "value1" }), MimeType = "application/json" } ] }); } throw new McpProtocolException("Resource not found", McpErrorCode.InvalidRequest); } } }; // Create server with stdio transport var transport = new StdioServerTransport("CustomServer"); await using var server = McpServer.Create(transport, options); // Run server (blocks until client disconnects) await server.RunAsync(); ``` ## Implementing Client Sampling Handler Enable MCP clients to respond to server sampling requests with LLM capabilities. ```csharp using Microsoft.Extensions.AI; using ModelContextProtocol.Client; using OpenAI; // Setup IChatClient with your preferred LLM provider var openAIClient = new OpenAIClient(apiKey); IChatClient chatClient = openAIClient .GetChatClient("gpt-4o") .AsIChatClient() .AsBuilder() .UseFunctionInvocation() .UseOpenTelemetry() .Build(); // Create sampling handler from IChatClient var samplingHandler = McpClient.CreateSamplingHandler(chatClient); // Create MCP client with sampling capability var mcpClient = await McpClient.CreateAsync( transport, clientOptions: new McpClientOptions { ClientInfo = new Implementation { Name = "SamplingClient", Version = "1.0" }, Capabilities = new ClientCapabilities { Sampling = new SamplingCapability() // Advertise sampling support }, Handlers = new McpClientHandlers { SamplingHandler = samplingHandler } }); // Alternative: Manual sampling handler with full control var manualHandler = async ( CreateMessageRequestParams? request, IProgress progress, CancellationToken cancellationToken) => { if (request?.Messages == null) { throw new McpException("No messages provided"); } // Convert MCP messages to chat messages var chatMessages = request.Messages.Select(m => new ChatMessage( m.Role switch { "user" => ChatRole.User, "assistant" => ChatRole.Assistant, _ => ChatRole.System }, m.Content.Type == "text" ? m.Content.Text! : "[binary content]" )).ToList(); // Call LLM var response = await chatClient.GetResponseAsync( chatMessages, new ChatOptions { MaxOutputTokens = request.MaxTokens, Temperature = request.Temperature, ModelId = request.ModelPreferences?.Hints?[0].Name }, cancellationToken); // Report progress if requested progress?.Report(new ProgressNotificationValue { Progress = 100, Total = 100 }); // Return result in MCP format return new CreateMessageResult { Role = "assistant", Content = new TextContent { Type = "text", Text = response.Message.Text }, Model = response.ModelId ?? request.ModelPreferences?.Hints?[0].Name ?? "unknown", StopReason = response.FinishReason switch { ChatFinishReason.Stop => "endTurn", ChatFinishReason.Length => "maxTokens", ChatFinishReason.ToolCalls => "toolUse", _ => "unknown" } }; }; ``` ## Advanced Tool Definition with Structured Content Create tools that return rich content types including images, structured data, and resources. ```csharp using ModelContextProtocol; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using System.ComponentModel; [McpServerToolType] public sealed class VisualizationTools { [McpServerTool(UseStructuredContent = true)] [Description("Generate a chart from data")] public static async Task GenerateChart( [Description("Chart data as JSON")] string data, [Description("Chart type: bar, line, pie")] string chartType = "bar") { // Generate chart image byte[] chartImage = await CreateChartImageAsync(data, chartType); string base64Image = Convert.ToBase64String(chartImage); // Return multiple content types return new CallToolResult { Content = [ new TextContentBlock { Type = "text", Text = $"Generated {chartType} chart from provided data" }, new ImageContentBlock { Type = "image", Data = base64Image, MimeType = "image/png" } ], IsError = false }; } [McpServerTool(UseStructuredContent = true)] [Description("Analyze data and return structured results")] public static CallToolResult AnalyzeData( [Description("Dataset as JSON")] string dataset) { var analysis = PerformAnalysis(dataset); return new CallToolResult { Content = [ new TextContentBlock { Type = "text", Text = JsonSerializer.Serialize(new { mean = analysis.Mean, median = analysis.Median, stdDev = analysis.StandardDeviation, summary = analysis.Summary }) } ] }; } [McpServerTool] [Description("Create report with embedded resources")] public static CallToolResult GenerateReport(string reportType) { return new CallToolResult { Content = [ new TextContentBlock { Type = "text", Text = "# Report\n\n..." }, new ResourceContentBlock { Type = "resource", Resource = new ResourceReference { Uri = "report://generated/latest", Text = "Full report available at this resource" } } ] }; } private static Task CreateChartImageAsync(string data, string type) => Task.FromResult(new byte[] { 0x89, 0x50, 0x4E, 0x47 }); // PNG header private static (double Mean, double Median, double StandardDeviation, string Summary) PerformAnalysis(string data) => (42.0, 41.5, 5.2, "Data shows normal distribution"); } ``` ## Summary The MCP C# SDK enables .NET developers to build sophisticated integrations between Large Language Models and external data sources, tools, and services through a standardized protocol. The SDK's attribute-based programming model makes it trivial to expose C# methods as MCP tools, prompts, or resources with automatic JSON schema generation and parameter validation. Dependency injection support allows tools to access databases, HTTP clients, configuration, and any other registered services, making it natural to integrate MCP capabilities into existing .NET applications. The interactive elicitation feature enables dynamic user interactions during tool execution, supporting both form-based data collection and URL-based out-of-band flows for scenarios like OAuth authentication and payment processing. For production scenarios, the AspNetCore package provides HTTP-based servers with session management, authentication/authorization integration, and standard ASP.NET Core middleware support. The client APIs integrate seamlessly with Microsoft.Extensions.AI, allowing MCP tools to be used directly with any IChatClient implementation from OpenAI, Anthropic, Azure OpenAI, or other providers. Advanced features like bidirectional sampling enable servers to request LLM operations from clients, creating sophisticated multi-agent workflows where both client and server can leverage AI capabilities. The SDK's support for multiple transport mechanisms (stdio, SSE, streamable HTTP), interactive elicitation for gathering user input, and full AOT compilation compatibility makes it suitable for everything from command-line utilities to high-performance web services.