From a807ba890a62b440af4394103a8173e8bba69824 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Mon, 23 Feb 2026 23:19:15 -0800 Subject: [PATCH 01/15] Add conceptual docs for tools, resources, prompts, roots, completions, transports, ping, cancellation, pagination, and capabilities Address documentation gaps identified in SDK conformance assessment: - 14 FAIL and 17 PARTIAL features now documented - New docs cover content types, change notifications, subscriptions, templates, pagination, and capability negotiation - Updated elicitation doc with default values and enum schema formats - Reorganized toc.yml and index.md with categorized navigation - All code snippets verified against SDK compilation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/concepts/cancellation/cancellation.md | 93 ++++++++ docs/concepts/capabilities/capabilities.md | 134 +++++++++++ docs/concepts/completions/completions.md | 122 ++++++++++ docs/concepts/elicitation/elicitation.md | 76 ++++++ docs/concepts/index.md | 22 ++ docs/concepts/pagination/pagination.md | 98 ++++++++ docs/concepts/ping/ping.md | 57 +++++ docs/concepts/prompts/prompts.md | 217 +++++++++++++++++ docs/concepts/resources/resources.md | 257 +++++++++++++++++++++ docs/concepts/roots/roots.md | 112 +++++++++ docs/concepts/toc.yml | 20 ++ docs/concepts/tools/tools.md | 247 ++++++++++++++++++++ docs/concepts/transports/transports.md | 173 ++++++++++++++ 13 files changed, 1628 insertions(+) create mode 100644 docs/concepts/cancellation/cancellation.md create mode 100644 docs/concepts/capabilities/capabilities.md create mode 100644 docs/concepts/completions/completions.md create mode 100644 docs/concepts/pagination/pagination.md create mode 100644 docs/concepts/ping/ping.md create mode 100644 docs/concepts/prompts/prompts.md create mode 100644 docs/concepts/resources/resources.md create mode 100644 docs/concepts/roots/roots.md create mode 100644 docs/concepts/tools/tools.md create mode 100644 docs/concepts/transports/transports.md diff --git a/docs/concepts/cancellation/cancellation.md b/docs/concepts/cancellation/cancellation.md new file mode 100644 index 000000000..8f696df15 --- /dev/null +++ b/docs/concepts/cancellation/cancellation.md @@ -0,0 +1,93 @@ +--- +title: Cancellation +author: jeffhandley +description: How to cancel in-flight MCP requests using cancellation tokens and notifications. +uid: cancellation +--- + +## Cancellation + +MCP supports [cancellation] of in-flight requests. Either side can cancel a previously issued request by using .NET's [task cancellation] with a `CancellationToken` to send a cancellation notification. + +[cancellation]: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation +[task cancellation]: https://learn.microsoft.com/dotnet/standard/parallel-programming/task-cancellation + +### Overview + +When a client cancels a pending request, a `notifications/cancelled` notification is sent to the server. The server's `CancellationToken` for that request is then triggered, allowing the server-side handler to stop work gracefully. This same mechanism works in reverse for server-to-client requests. + +### Client-side cancellation + +All client methods accept a `CancellationToken` parameter. Cancel a pending request by canceling the token: + +```csharp +using var cts = new CancellationTokenSource(); + +// Start a long-running tool call +var toolTask = client.CallToolAsync( + "long_running_operation", + new Dictionary { ["duration"] = 60 }, + cancellationToken: cts.Token); + +// Cancel after 5 seconds +cts.CancelAfter(TimeSpan.FromSeconds(5)); + +try +{ + var result = await toolTask; +} +catch (OperationCanceledException) +{ + Console.WriteLine("Tool call was cancelled."); +} +``` + +When the `CancellationToken` is cancelled, a `notifications/cancelled` notification is automatically sent to the server with the request ID, allowing the server to stop processing. + +### Server-side cancellation handling + +Server tool methods should accept a `CancellationToken` parameter and check it during long-running operations: + +```csharp +[McpServerTool, Description("A long-running computation")] +public static async Task LongComputation( + [Description("Number of iterations")] int iterations, + CancellationToken cancellationToken) +{ + for (int i = 0; i < iterations; i++) + { + // Check for cancellation on each iteration + cancellationToken.ThrowIfCancellationRequested(); + + await Task.Delay(1000, cancellationToken); + } + + return $"Completed {iterations} iterations."; +} +``` + +When the client sends a cancellation notification, the `CancellationToken` provided to the tool method is automatically triggered. The `OperationCanceledException` propagates back to the client as a cancellation response. + +### Cancellation notification details + +The cancellation notification includes: + +- **RequestId**: The ID of the request to cancel, allowing the receiver to correlate the cancellation with the correct in-flight request. +- **Reason**: An optional human-readable reason for the cancellation. + +```csharp +// This is sent automatically when a CancellationToken is cancelled, +// but you can also observe cancellation notifications: +mcpClient.RegisterNotificationHandler( + NotificationMethods.CancelledNotification, + (notification, ct) => + { + var cancelled = notification.Params?.Deserialize( + McpJsonUtilities.DefaultOptions); + if (cancelled is not null) + { + Console.WriteLine($"Request {cancelled.RequestId} cancelled: {cancelled.Reason}"); + } + return default; + }); +``` diff --git a/docs/concepts/capabilities/capabilities.md b/docs/concepts/capabilities/capabilities.md new file mode 100644 index 000000000..a1c464f83 --- /dev/null +++ b/docs/concepts/capabilities/capabilities.md @@ -0,0 +1,134 @@ +--- +title: Capabilities +author: jeffhandley +description: How capability and protocol version negotiation works in MCP. +uid: capabilities +--- + +## Capabilities + +MCP uses a [capability negotiation] mechanism during connection initialization. Clients and servers exchange their supported capabilities, allowing each side to understand what features the other supports and adapt behavior accordingly. + +[capability negotiation]: https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization + +### Overview + +When a client connects to a server, the initialization handshake includes: + +1. The **client** sends an `initialize` request with its and protocol version. +2. The **server** responds with its and the negotiated protocol version. +3. The client sends an `initialized` notification to confirm the session is ready. + +Both sides should check the other's capabilities before using optional features. + +### Client capabilities + + declares what features the client supports: + +| Capability | Type | Description | +|-----------|------|-------------| +| `Roots` | | Client can provide filesystem root URIs | +| `Sampling` | | Client can handle LLM sampling requests | +| `Elicitation` | | Client can present forms or URLs to the user | +| `Experimental` | `IDictionary` | Experimental capabilities | + +Configure client capabilities when creating an MCP client: + +```csharp +var options = new McpClientOptions +{ + Capabilities = new ClientCapabilities + { + Roots = new RootsCapability { ListChanged = true }, + Sampling = new SamplingCapability(), + Elicitation = new ElicitationCapability + { + Form = new FormElicitationCapability(), + Url = new UrlElicitationCapability() + } + } +}; + +await using var client = await McpClient.CreateAsync(transport, options); +``` + +Handlers for each capability (roots, sampling, elicitation) are covered in their respective documentation pages. + +### Server capabilities + + declares what features the server supports: + +| Capability | Type | Description | +|-----------|------|-------------| +| `Tools` | | Server exposes callable tools | +| `Prompts` | | Server exposes prompt templates | +| `Resources` | | Server exposes readable resources | +| `Logging` | | Server can send log messages | +| `Completions` | | Server supports argument completions | +| `Experimental` | `IDictionary` | Experimental capabilities | + +Server capabilities are automatically inferred from the configured features. For example, registering tools with `.WithTools()` automatically declares the tools capability. + +### Checking capabilities + +Before using an optional feature, check whether the other side declared the corresponding capability. + +#### Checking server capabilities from the client + +```csharp +await using var client = await McpClient.CreateAsync(transport); + +// Check if the server supports tools +if (client.ServerCapabilities.Tools is not null) +{ + var tools = await client.ListToolsAsync(); +} + +// Check if the server supports resources with subscriptions +if (client.ServerCapabilities.Resources is { Subscribe: true }) +{ + await client.SubscribeToResourceAsync("config://app/settings"); +} + +// Check if the server supports prompts with list-changed notifications +if (client.ServerCapabilities.Prompts is { ListChanged: true }) +{ + mcpClient.RegisterNotificationHandler( + NotificationMethods.PromptListChangedNotification, + async (notification, ct) => + { + var prompts = await mcpClient.ListPromptsAsync(cancellationToken: ct); + }); +} + +// Check if the server supports logging +if (client.ServerCapabilities.Logging is not null) +{ + await client.SetLoggingLevelAsync(LoggingLevel.Info); +} + +// Check if the server supports completions +if (client.ServerCapabilities.Completions is not null) +{ + var completions = await client.CompleteAsync( + new PromptReference { Name = "my_prompt" }, + argumentName: "language", + argumentValue: "py"); +} +``` + +### Protocol version negotiation + +The client specifies the MCP protocol version it supports in the `initialize` request. The server responds with the negotiated protocol version, which may differ from the client's requested version if the server supports an earlier version. + +After initialization, the negotiated version is available on both sides: + +```csharp +// On the client +string? version = client.NegotiatedProtocolVersion; + +// On the server (within a tool or handler) +string? version = server.NegotiatedProtocolVersion; +``` + +Version negotiation is handled automatically. If the client and server cannot agree on a compatible protocol version, the initialization fails with an error. diff --git a/docs/concepts/completions/completions.md b/docs/concepts/completions/completions.md new file mode 100644 index 000000000..0bc0b21aa --- /dev/null +++ b/docs/concepts/completions/completions.md @@ -0,0 +1,122 @@ +--- +title: Completions +author: jeffhandley +description: How to implement and use argument auto-completion for prompts and resources. +uid: completions +--- + +## Completions + +MCP [completions] allow servers to provide argument auto-completion suggestions for prompt and resource template parameters. This helps clients offer a better user experience by suggesting valid values as the user types. + +[completions]: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion + +### Overview + +Completions work with two types of references: + +- **Prompt argument completions**: Suggest values for prompt parameters (e.g., language names, style options) +- **Resource template argument completions**: Suggest values for URI template parameters (e.g., file paths, resource IDs) + +The server returns a object containing a list of suggested values, an optional total count, and a flag indicating if more values are available. + +### Implementing completions on the server + +Register a completion handler when building the server. The handler receives a reference (prompt or resource template) and the current argument value: + +```csharp +builder.Services.AddMcpServer() + .WithHttpTransport() + .WithPrompts() + .WithResources() + .WithCompleteHandler(async (ctx, ct) => + { + if (ctx.Params is not { } @params) + throw new McpProtocolException("Params are required.", McpErrorCode.InvalidParams); + + var argument = @params.Argument; + + // Handle prompt argument completions + if (@params.Ref is PromptReference promptRef) + { + var suggestions = argument.Name switch + { + "language" => new[] { "csharp", "python", "javascript", "typescript", "go", "rust" }, + "style" => new[] { "casual", "formal", "technical", "friendly" }, + _ => Array.Empty() + }; + + // Filter suggestions based on what the user has typed so far + var filtered = suggestions.Where(s => s.StartsWith(argument.Value, StringComparison.OrdinalIgnoreCase)).ToList(); + + return new CompleteResult + { + Completion = new Completion + { + Values = filtered, + Total = filtered.Count, + HasMore = false + } + }; + } + + // Handle resource template argument completions + if (@params.Ref is ResourceTemplateReference resourceRef) + { + var availableIds = new[] { "1", "2", "3", "4", "5" }; + var filtered = availableIds.Where(id => id.StartsWith(argument.Value)).ToList(); + + return new CompleteResult + { + Completion = new Completion + { + Values = filtered, + Total = filtered.Count, + HasMore = false + } + }; + } + + return new CompleteResult(); + }); +``` + +### Requesting completions on the client + +Clients request completions using . Provide a reference to the prompt or resource template, the argument name, and the current partial value: + +#### Prompt argument completions + +```csharp +// Get completions for a prompt argument +CompleteResult result = await client.CompleteAsync( + new PromptReference { Name = "code_review" }, + argumentName: "language", + argumentValue: "type"); + +// result.Completion.Values might contain: ["typescript"] +foreach (var suggestion in result.Completion.Values) +{ + Console.WriteLine($" {suggestion}"); +} + +if (result.Completion.HasMore == true) +{ + Console.WriteLine($" ... and more ({result.Completion.Total} total)"); +} +``` + +#### Resource template argument completions + +```csharp +// Get completions for a resource template argument +CompleteResult result = await client.CompleteAsync( + new ResourceTemplateReference { Uri = "file:///{path}" }, + argumentName: "path", + argumentValue: "src/"); + +foreach (var suggestion in result.Completion.Values) +{ + Console.WriteLine($" {suggestion}"); +} +``` diff --git a/docs/concepts/elicitation/elicitation.md b/docs/concepts/elicitation/elicitation.md index b4d73a85d..e484bd3a0 100644 --- a/docs/concepts/elicitation/elicitation.md +++ b/docs/concepts/elicitation/elicitation.md @@ -34,6 +34,82 @@ For enum types, the SDK supports several schema formats: - **TitledMultiSelectEnumSchema**: A multi-select enum with display titles for each option. - **LegacyTitledEnumSchema** (deprecated): The legacy enum schema using `enumNames` for backward compatibility. +#### Default values + +Each schema type supports a `Default` property that specifies a pre-populated value for the form field. +Clients should use defaults to pre-fill form fields, making it easier for users to accept common values or see expected input formats. + +```csharp +var result = await server.ElicitAsync(new ElicitRequestParams +{ + Message = "Configure your preferences", + RequestedSchema = new ElicitRequestParams.RequestSchema + { + Properties = new Dictionary + { + ["name"] = new ElicitRequestParams.StringSchema + { + Description = "Your display name", + Default = "User" + }, + ["maxResults"] = new ElicitRequestParams.NumberSchema + { + Description = "Maximum number of results", + Default = 25 + }, + ["enableNotifications"] = new ElicitRequestParams.BooleanSchema + { + Description = "Enable push notifications", + Default = true + }, + ["theme"] = new ElicitRequestParams.UntitledSingleSelectEnumSchema + { + Description = "UI theme", + Enum = ["light", "dark", "system"], + Default = "system" + } + } + } +}, cancellationToken); +``` + +If the client returns accepted content with missing fields, the server automatically fills those fields with their schema defaults. This ensures tools always receive complete data regardless of client behavior. + +#### Enum schema formats + +Enum schemas allow the server to present a set of choices to the user. + +- : Simple single-select where enum values serve as both the value and display text. +- : Single-select with separate display titles for each option using JSON Schema `oneOf` with `const` and `title`. +- : Multi-select allowing multiple values. +- : Multi-select with display titles. + +```csharp +// Titled single-select: display titles differ from values +["priority"] = new ElicitRequestParams.TitledSingleSelectEnumSchema +{ + Description = "Task priority", + OneOf = + [ + new ElicitRequestParams.EnumSchemaOption { Const = "p0", Title = "Critical (P0)" }, + new ElicitRequestParams.EnumSchemaOption { Const = "p1", Title = "High (P1)" }, + new ElicitRequestParams.EnumSchemaOption { Const = "p2", Title = "Normal (P2)" }, + ], + Default = "p2" +}, + +// Multi-select: user can select multiple values +["tags"] = new ElicitRequestParams.UntitledMultiSelectEnumSchema +{ + Description = "Tags to apply", + Items = new ElicitRequestParams.UntitledEnumItemsSchema + { + Enum = ["bug", "feature", "docs", "test"] + }, + Default = ["bug"] +} +``` + The server can request a single input or multiple inputs at once. To help distinguish multiple inputs, each input has a unique name. diff --git a/docs/concepts/index.md b/docs/concepts/index.md index 044d4fee7..14392f9dd 100644 --- a/docs/concepts/index.md +++ b/docs/concepts/index.md @@ -4,10 +4,32 @@ Welcome to the conceptual documentation for the Model Context Protocol SDK. Here ## Contents +### Base Protocol + | Title | Description | | - | - | +| [Capabilities](capabilities/capabilities.md) | Learn how client and server capabilities are negotiated during initialization, including protocol version negotiation. | +| [Transports](transports/transports.md) | Learn how to configure stdio, Streamable HTTP, and SSE transports for client-server communication. | +| [Ping](ping/ping.md) | Learn how to verify connection health using the ping mechanism. | | [Progress tracking](progress/progress.md) | Learn how to track progress for long-running operations through notification messages. | +| [Cancellation](cancellation/cancellation.md) | Learn how to cancel in-flight MCP requests using cancellation tokens and notifications. | +| [Pagination](pagination/pagination.md) | Learn how to use cursor-based pagination when listing tools, prompts, and resources. | + +### Client Features + +| Title | Description | +| - | - | +| [Roots](roots/roots.md) | Learn how clients provide filesystem roots to servers for context-aware operations. | | [Elicitation](elicitation/elicitation.md) | Learn how to request additional information from users during interactions. | + +### Server Features + +| Title | Description | +| - | - | +| [Tools](tools/tools.md) | Learn how to implement and consume tools that return text, images, audio, and embedded resources. | +| [Resources](resources/resources.md) | Learn how to expose and consume data through MCP resources, including templates and subscriptions. | +| [Prompts](prompts/prompts.md) | Learn how to implement and consume reusable prompt templates with rich content types. | +| [Completions](completions/completions.md) | Learn how to implement argument auto-completion for prompts and resource templates. | | [Logging](logging/logging.md) | Learn how to implement logging in MCP servers and how clients can consume log messages. | | [HTTP Context](httpcontext/httpcontext.md) | Learn how to access the underlying `HttpContext` for a request. | | [MCP Server Handler Filters](filters.md) | Learn how to add filters to the handler pipeline. Filters let you wrap the original handler with additional functionality. | diff --git a/docs/concepts/pagination/pagination.md b/docs/concepts/pagination/pagination.md new file mode 100644 index 000000000..96467250e --- /dev/null +++ b/docs/concepts/pagination/pagination.md @@ -0,0 +1,98 @@ +--- +title: Pagination +author: jeffhandley +description: How to use cursor-based pagination when listing tools, prompts, and resources. +uid: pagination +--- + +## Pagination + +MCP uses [cursor-based pagination] for list operations that may return large result sets. This applies to listing tools, prompts, resources, and resource templates. + +[cursor-based pagination]: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination + +### Overview + +Instead of offset-based pagination (page 1, page 2, etc.), MCP uses opaque cursor tokens. Each paginated response may include a `NextCursor` value. If present, pass it in the next request to retrieve the next page of results. + +Two levels of API are provided for paginated operations: + +1. **Convenience methods** (e.g., `ListToolsAsync()` returning `IList`) that automatically handle pagination and return all results. +2. **Raw methods** (e.g., `ListToolsAsync(ListToolsRequestParams)` returning the result type directly) that provide direct control over pagination. + +### Automatic pagination + +The convenience methods on handle pagination automatically, fetching all pages and returning the complete list: + +```csharp +// Fetches all tools, handling pagination automatically +IList allTools = await client.ListToolsAsync(); + +// Fetches all resources, handling pagination automatically +IList allResources = await client.ListResourcesAsync(); + +// Fetches all prompts, handling pagination automatically +IList allPrompts = await client.ListPromptsAsync(); + +// Fetches all resource templates, handling pagination automatically +IList allTemplates = await client.ListResourceTemplatesAsync(); +``` + +### Manual pagination + +For more control, use the raw methods that accept request parameters and return paginated results. This is useful for processing results page by page or limiting the number of results retrieved: + +```csharp +string? cursor = null; + +do +{ + var result = await client.ListToolsAsync(new ListToolsRequestParams + { + Cursor = cursor + }); + + // Process this page of results + foreach (var tool in result.Tools) + { + Console.WriteLine($"{tool.Name}: {tool.Description}"); + } + + // Get the cursor for the next page (null when no more pages) + cursor = result.NextCursor; + +} while (cursor is not null); +``` + +### Pagination on the server + +When implementing custom list handlers on the server, pagination is supported by examining the `Cursor` property of the request parameters and returning a `NextCursor` in the result: + +```csharp +builder.Services.AddMcpServer() + .WithHttpTransport() + .WithListResourcesHandler(async (ctx, ct) => + { + const int pageSize = 10; + int startIndex = 0; + + // Parse cursor to determine starting position + if (ctx.Params?.Cursor is { } cursor) + { + startIndex = int.Parse(cursor); + } + + var allResources = GetAllResources(); + var page = allResources.Skip(startIndex).Take(pageSize).ToList(); + var hasMore = startIndex + pageSize < allResources.Count; + + return new ListResourcesResult + { + Resources = page, + NextCursor = hasMore ? (startIndex + pageSize).ToString() : null + }; + }); +``` + +> [!NOTE] +> The cursor format is opaque to the client. Servers can use any encoding scheme (numeric offsets, encoded tokens, database cursors, etc.) as long as they can parse their own cursors on subsequent requests. diff --git a/docs/concepts/ping/ping.md b/docs/concepts/ping/ping.md new file mode 100644 index 000000000..6640b42f9 --- /dev/null +++ b/docs/concepts/ping/ping.md @@ -0,0 +1,57 @@ +--- +title: Ping +author: jeffhandley +description: How to use the MCP ping mechanism to check connection health. +uid: ping +--- + +## Ping + +MCP includes a [ping mechanism] that allows either side of a connection to verify that the other side is still responsive. This is useful for connection health monitoring and keep-alive scenarios. + +[ping mechanism]: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping + +### Overview + +The ping operation is a simple request/response exchange. Either the client or the server can initiate a ping, and the other side responds automatically. Ping responses are handled automatically—callers only need to invoke the method to send a ping. + +### Pinging from the client + +Use the method to verify the server is responsive: + +```csharp +await using var client = await McpClient.CreateAsync(transport); + +try +{ + await client.PingAsync(); + Console.WriteLine("Server is responsive."); +} +catch (OperationCanceledException) +{ + Console.WriteLine("Ping timed out - server may be unresponsive."); +} +catch (McpException ex) +{ + Console.WriteLine($"Ping failed: {ex.Message}"); +} +``` + +A timeout can also be specified using a `CancellationToken`: + +```csharp +using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + +try +{ + await client.PingAsync(cancellationToken: cts.Token); +} +catch (OperationCanceledException) +{ + Console.WriteLine("Server did not respond within 5 seconds."); +} +``` + +### Automatic ping handling + +Incoming ping requests from either side are responded to automatically. No additional configuration is needed—when a ping request is received, a ping response is sent immediately. diff --git a/docs/concepts/prompts/prompts.md b/docs/concepts/prompts/prompts.md new file mode 100644 index 000000000..3ba8723ac --- /dev/null +++ b/docs/concepts/prompts/prompts.md @@ -0,0 +1,217 @@ +--- +title: Prompts +author: jeffhandley +description: How to implement and consume MCP prompts that return text, images, and embedded resources. +uid: prompts +--- + +## Prompts + +MCP [prompts] allow servers to expose reusable prompt templates to clients. Prompts provide a way for servers to define structured messages that can be parameterized and composed into conversations. + +[prompts]: https://modelcontextprotocol.io/specification/2025-11-25/server/prompts + +This document covers implementing prompts on the server, consuming them from the client, rich content types, and change notifications. + +### Defining prompts on the server + +Prompts are defined as methods marked with the attribute within a class marked with . Prompts can return `ChatMessage` instances for simple text/image content, or instances when protocol-specific content types like are needed. + +#### Simple prompts + +A prompt without arguments returns a fixed message: + +```csharp +[McpServerPromptType] +public class MyPrompts +{ + [McpServerPrompt, Description("A simple greeting prompt")] + public static ChatMessage Greeting() + => new(ChatRole.User, "Hello! How can you help me today?"); +} +``` + +#### Prompts with arguments + +Prompts can accept parameters to customize the generated messages. Use `[Description]` attributes to document each parameter: + +```csharp +[McpServerPromptType] +public class CodePrompts +{ + [McpServerPrompt, Description("Generates a code review prompt")] + public static IEnumerable CodeReview( + [Description("The programming language")] string language, + [Description("The code to review")] string code) + { + return + [ + new ChatMessage(ChatRole.User, + $"Please review the following {language} code:\n\n```{language}\n{code}\n```"), + new ChatMessage(ChatRole.Assistant, + "I'll review the code for correctness, style, and potential improvements.") + ]; + } +} +``` + +Register prompt types when building the server: + +```csharp +builder.Services.AddMcpServer() + .WithHttpTransport() + .WithPrompts() + .WithPrompts(); +``` + +### Rich content in prompts + +Prompt messages can contain more than just text. For text and image content, use `ChatMessage` from Microsoft.Extensions.AI. For protocol-specific content types like embedded resources, use instead. + +#### Image content + +Include images in prompts using `DataContent`: + +```csharp +[McpServerPrompt, Description("A prompt that includes an image for analysis")] +public static IEnumerable AnalyzeImage( + [Description("Instructions for the analysis")] string instructions) +{ + byte[] imageBytes = LoadSampleImage(); + return + [ + new ChatMessage(ChatRole.User, + [ + new TextContent($"Please analyze this image: {instructions}"), + new DataContent(imageBytes, "image/png") + ]) + ]; +} +``` + +#### Embedded resources + +For protocol-specific content types like , use instead of `ChatMessage`. `PromptMessage` has a `Role` property and a single `Content` property of type : + +```csharp +[McpServerPrompt, Description("A prompt that includes a file resource")] +public static IEnumerable ReviewDocument( + [Description("Path to the document to review")] string path) +{ + string content = File.ReadAllText(path); + return + [ + new PromptMessage + { + Role = Role.User, + Content = new TextContentBlock { Text = "Please review the following document:" } + }, + new PromptMessage + { + Role = Role.User, + Content = new EmbeddedResourceBlock + { + Resource = new TextResourceContents + { + Uri = $"file:///{path}", + MimeType = "text/plain", + Text = content + } + } + } + ]; +} +``` + +For binary resources, use the factory method: + +```csharp +new PromptMessage +{ + Role = Role.User, + Content = new EmbeddedResourceBlock + { + Resource = BlobResourceContents.FromBytes(pdfBytes, "data://report.pdf", "application/pdf") + } +} +``` + +### Consuming prompts on the client + +Clients can discover and use prompts through . + +#### Listing prompts + +```csharp +IList prompts = await client.ListPromptsAsync(); + +foreach (var prompt in prompts) +{ + Console.WriteLine($"{prompt.Name}: {prompt.Description}"); + + // Show available arguments + if (prompt.ProtocolPrompt.Arguments is { Count: > 0 }) + { + foreach (var arg in prompt.ProtocolPrompt.Arguments) + { + var required = arg.Required == true ? " (required)" : ""; + Console.WriteLine($" - {arg.Name}: {arg.Description}{required}"); + } + } +} +``` + +#### Getting a prompt + +```csharp +GetPromptResult result = await client.GetPromptAsync( + "code_review", + new Dictionary + { + ["language"] = "csharp", + ["code"] = "public static int Add(int a, int b) => a + b;" + }); + +// Process the returned messages (PromptMessage has a single Content block) +foreach (var message in result.Messages) +{ + Console.WriteLine($"[{message.Role}]:"); + switch (message.Content) + { + case TextContentBlock text: + Console.WriteLine($" {text.Text}"); + break; + case ImageContentBlock image: + Console.WriteLine($" [image] {image.MimeType}"); + break; + case EmbeddedResourceBlock resource: + Console.WriteLine($" Resource: {resource.Resource.Uri}"); + break; + } +} +``` + +### Prompt list change notifications + +Servers can dynamically add, remove, or modify prompts at runtime and notify connected clients. + +#### Sending notifications from the server + +```csharp +// After adding or removing prompts dynamically +await server.SendNotificationAsync( + NotificationMethods.PromptListChangedNotification, + new PromptListChangedNotificationParams()); +``` + +#### Handling notifications on the client + +```csharp +mcpClient.RegisterNotificationHandler( + NotificationMethods.PromptListChangedNotification, + async (notification, cancellationToken) => + { + var updatedPrompts = await mcpClient.ListPromptsAsync(cancellationToken: cancellationToken); + Console.WriteLine($"Prompt list updated. {updatedPrompts.Count} prompts available."); + }); +``` diff --git a/docs/concepts/resources/resources.md b/docs/concepts/resources/resources.md new file mode 100644 index 000000000..542b24dcb --- /dev/null +++ b/docs/concepts/resources/resources.md @@ -0,0 +1,257 @@ +--- +title: Resources +author: jeffhandley +description: How to implement and consume MCP resources for exposing data to clients. +uid: resources +--- + +## Resources + +MCP [resources] allow servers to expose data and content to clients. Resources represent any kind of data that a server wants to make available—files, database records, API responses, live system data, and more. + +[resources]: https://modelcontextprotocol.io/specification/2025-11-25/server/resources + +This document covers implementing resources on the server, consuming them from the client, resource templates, subscriptions, and change notifications. + +### Defining resources on the server + +Resources are defined as methods marked with the attribute within a class marked with . The attribute specifies the URI template that identifies the resource. + +#### Direct resources + +Direct resources have a fixed URI and are returned in the resource list: + +```csharp +[McpServerResourceType] +public class MyResources +{ + [McpServerResource(UriTemplate = "config://app/settings", Name = "App Settings", MimeType = "application/json")] + [Description("Returns application configuration settings")] + public static string GetSettings() => JsonSerializer.Serialize(new { theme = "dark", language = "en" }); +} +``` + +#### Template resources + +Template resources use [URI templates (RFC 6570)] with parameters. They are returned separately in the resource templates list and can match a range of URIs: + +[URI templates (RFC 6570)]: https://tools.ietf.org/html/rfc6570 + +```csharp +[McpServerResourceType] +public class FileResources +{ + [McpServerResource(UriTemplate = "file:///{path}", Name = "File Resource")] + [Description("Reads a file by its path")] + public static ResourceContents ReadFile(string path) + { + if (File.Exists(path)) + { + return new TextResourceContents + { + Uri = $"file:///{path}", + MimeType = "text/plain", + Text = File.ReadAllText(path) + }; + } + + throw new McpException($"File not found: {path}"); + } +} +``` + +Register resource types when building the server: + +```csharp +builder.Services.AddMcpServer() + .WithHttpTransport() + .WithResources() + .WithResources(); +``` + +### Reading text resources + +Text resources return their content as with a `Text` property: + +```csharp +[McpServerResource(UriTemplate = "notes://daily/{date}", Name = "Daily Notes")] +[Description("Returns notes for a given date")] +public static TextResourceContents GetDailyNotes(string date) +{ + return new TextResourceContents + { + Uri = $"notes://daily/{date}", + MimeType = "text/markdown", + Text = $"# Notes for {date}\n\n- Meeting at 10am\n- Review PRs" + }; +} +``` + +### Reading binary resources + +Binary resources return their content as with a `Blob` property containing the raw bytes. Use the factory method to construct instances: + +```csharp +[McpServerResource(UriTemplate = "images://photos/{id}", Name = "Photo")] +[Description("Returns a photo by ID")] +public static BlobResourceContents GetPhoto(int id) +{ + byte[] imageData = LoadPhoto(id); + return BlobResourceContents.FromBytes(imageData, $"images://photos/{id}", "image/png"); +} +``` + +### Consuming resources on the client + +Clients can discover and read resources using : + +#### Listing resources + +```csharp +// List direct resources +IList resources = await client.ListResourcesAsync(); + +foreach (var resource in resources) +{ + Console.WriteLine($"{resource.Name} ({resource.Uri})"); + Console.WriteLine($" MIME: {resource.MimeType}"); + Console.WriteLine($" Description: {resource.Description}"); +} +``` + +#### Listing resource templates + +```csharp +// List resource templates (parameterized URIs) +IList templates = await client.ListResourceTemplatesAsync(); + +foreach (var template in templates) +{ + Console.WriteLine($"{template.Name}: {template.UriTemplate}"); +} +``` + +#### Reading a resource + +```csharp +// Read a direct resource by URI +ReadResourceResult result = await client.ReadResourceAsync("config://app/settings"); + +foreach (var content in result.Contents) +{ + if (content is TextResourceContents text) + { + Console.WriteLine($"[{text.MimeType}] {text.Text}"); + } + else if (content is BlobResourceContents blob) + { + Console.WriteLine($"[{blob.MimeType}] {blob.Blob.Length} bytes"); + } +} +``` + +#### Reading a template resource + +```csharp +// Read a resource using a URI template with parameter values +ReadResourceResult result = await client.ReadResourceAsync( + "file:///{path}", + new Dictionary { ["path"] = "docs/readme.md" }); +``` + +### Resource subscriptions + +Clients can subscribe to resource updates to be notified when a resource's content changes. The server must declare subscription support in its capabilities. + +#### Subscribing on the client + +```csharp +// Subscribe with an inline handler +IAsyncDisposable subscription = await client.SubscribeToResourceAsync( + "config://app/settings", + async (notification, cancellationToken) => + { + Console.WriteLine($"Resource updated: {notification.Uri}"); + + // Re-read the resource to get updated content + var updated = await client.ReadResourceAsync(notification.Uri, cancellationToken: cancellationToken); + // Process updated content... + }); + +// Later, unsubscribe by disposing +await subscription.DisposeAsync(); +``` + +Clients can also subscribe and unsubscribe separately: + +```csharp +// Subscribe without a handler (use a global notification handler instead) +await client.SubscribeToResourceAsync("config://app/settings"); + +// Unsubscribe when no longer interested +await client.UnsubscribeFromResourceAsync("config://app/settings"); +``` + +#### Handling subscriptions on the server + +Register subscription handlers when building the server: + +```csharp +builder.Services.AddMcpServer() + .WithHttpTransport() + .WithResources() + .WithSubscribeToResourcesHandler(async (ctx, ct) => + { + if (ctx.Params?.Uri is { } uri) + { + // Track the subscription (e.g., in a concurrent dictionary) + subscriptions.TryAdd(uri, ctx.Server.SessionId); + } + return new EmptyResult(); + }) + .WithUnsubscribeFromResourcesHandler(async (ctx, ct) => + { + if (ctx.Params?.Uri is { } uri) + { + subscriptions.TryRemove(uri, out _); + } + return new EmptyResult(); + }); +``` + +#### Sending resource update notifications + +When a resource's content changes, the server notifies subscribed clients: + +```csharp +// Notify that a specific resource was updated +await server.SendNotificationAsync( + NotificationMethods.ResourceUpdatedNotification, + new ResourceUpdatedNotificationParams { Uri = "config://app/settings" }); +``` + +### Resource list change notifications + +When the set of available resources changes (resources added or removed), the server notifies clients: + +#### Sending notifications from the server + +```csharp +// After adding or removing resources dynamically +await server.SendNotificationAsync( + NotificationMethods.ResourceListChangedNotification, + new ResourceListChangedNotificationParams()); +``` + +#### Handling notifications on the client + +```csharp +mcpClient.RegisterNotificationHandler( + NotificationMethods.ResourceListChangedNotification, + async (notification, cancellationToken) => + { + // Refresh the resource list + var updatedResources = await mcpClient.ListResourcesAsync(cancellationToken: cancellationToken); + Console.WriteLine($"Resource list updated. {updatedResources.Count} resources available."); + }); +``` diff --git a/docs/concepts/roots/roots.md b/docs/concepts/roots/roots.md new file mode 100644 index 000000000..8d1d2097c --- /dev/null +++ b/docs/concepts/roots/roots.md @@ -0,0 +1,112 @@ +--- +title: Roots +author: jeffhandley +description: How to provide filesystem roots from clients to MCP servers. +uid: roots +--- + +## Roots + +MCP [roots] allow clients to inform servers about the relevant locations in the filesystem or other hierarchical data sources. Roots are a client-provided feature—the client declares its root URIs during initialization, and the server can request them to understand the working context. + +[roots]: https://modelcontextprotocol.io/specification/2025-11-25/client/roots + +### Overview + +Roots provide a mechanism for the client to tell the server which directories, projects, or repositories are relevant to the current session. A server might use roots to: + +- Scope file searches to the user's project directories +- Understand which repositories are being worked on +- Limit operations to specific filesystem boundaries + +Each root is represented by a with a URI and an optional human-readable name. + +### Declaring roots capability on the client + +Clients advertise their support for roots in the capabilities sent during initialization. To enable roots, configure the property and provide a handler that returns the root list: + +```csharp +var options = new McpClientOptions +{ + Capabilities = new ClientCapabilities + { + Roots = new RootsCapability + { + ListChanged = true // Notify server when roots change + } + }, + Handlers = new McpClientHandlers + { + RootsHandler = (request, cancellationToken) => + { + return ValueTask.FromResult(new ListRootsResult + { + Roots = + [ + new Root + { + Uri = "file:///home/user/projects/my-app", + Name = "My Application" + }, + new Root + { + Uri = "file:///home/user/projects/shared-lib", + Name = "Shared Library" + } + ] + }); + } + } +}; + +await using var client = await McpClient.CreateAsync(transport, options); +``` + +### Requesting roots from the server + +Servers can request the client's root list using : + +```csharp +[McpServerTool, Description("Lists the user's project roots")] +public static async Task ListProjectRoots(McpServer server, CancellationToken cancellationToken) +{ + var result = await server.RequestRootsAsync(new ListRootsRequestParams(), cancellationToken); + + var summary = new StringBuilder(); + foreach (var root in result.Roots) + { + summary.AppendLine($"- {root.Name ?? root.Uri}: {root.Uri}"); + } + + return summary.ToString(); +} +``` + +### Roots change notifications + +When the set of roots changes (for example, the user opens a new project), the client notifies the server so it can update its understanding of the working context. + +#### Sending change notifications from the client + +Roots change notifications are automatically sent when the client's roots handler is updated. However, clients can also send the notification explicitly: + +```csharp +await mcpClient.SendNotificationAsync( + NotificationMethods.RootsListChangedNotification, + new RootsListChangedNotificationParams()); +``` + +#### Handling change notifications on the server + +Servers can register a handler to respond when the client's roots change: + +```csharp +server.RegisterNotificationHandler( + NotificationMethods.RootsListChangedNotification, + async (notification, cancellationToken) => + { + // Re-request the roots list to get the updated set + var result = await server.RequestRootsAsync(new ListRootsRequestParams(), cancellationToken); + Console.WriteLine($"Roots updated. {result.Roots.Count} roots available."); + }); +``` diff --git a/docs/concepts/toc.yml b/docs/concepts/toc.yml index 59ef1f1b7..18845e64a 100644 --- a/docs/concepts/toc.yml +++ b/docs/concepts/toc.yml @@ -3,16 +3,36 @@ items: href: index.md - name: Base Protocol items: + - name: Capabilities + uid: capabilities + - name: Transports + uid: transports + - name: Ping + uid: ping - name: Progress uid: progress + - name: Cancellation + uid: cancellation + - name: Pagination + uid: pagination - name: Tasks uid: tasks - name: Client Features items: + - name: Roots + uid: roots - name: Elicitation uid: elicitation - name: Server Features items: + - name: Tools + uid: tools + - name: Resources + uid: resources + - name: Prompts + uid: prompts + - name: Completions + uid: completions - name: Logging uid: logging - name: HTTP Context diff --git a/docs/concepts/tools/tools.md b/docs/concepts/tools/tools.md new file mode 100644 index 000000000..8afb78b4b --- /dev/null +++ b/docs/concepts/tools/tools.md @@ -0,0 +1,247 @@ +--- +title: Tools +author: jeffhandley +description: How to implement and consume MCP tools that return text, images, audio, and embedded resources. +uid: tools +--- + +## Tools + +MCP [tools] allow servers to expose callable functions to clients. Tools are the primary mechanism for LLMs to take action through MCP—they enable everything from querying databases to calling web APIs. + +[tools]: https://modelcontextprotocol.io/specification/2025-11-25/server/tools + +This document covers tool content types, change notifications, and schema generation. + +### Defining tools on the server + +Tools are defined as methods marked with the attribute within a class marked with . Parameters are automatically deserialized from JSON and documented using `[Description]` attributes. + +```csharp +[McpServerToolType] +public class MyTools +{ + [McpServerTool, Description("Echoes the input message back")] + public static string Echo([Description("The message to echo")] string message) + => $"Echo: {message}"; +} +``` + +Register the tool type when building the server: + +```csharp +builder.Services.AddMcpServer() + .WithHttpTransport() + .WithTools(); +``` + +### Content types + +Tools can return various content types. The simplest is a `string`, which is automatically wrapped in a . For richer content, tools can return one or more instances. + +#### Text content + +Return a `string` or a directly: + +```csharp +[McpServerTool, Description("Returns a greeting")] +public static string Greet(string name) => $"Hello, {name}!"; +``` + +#### Image content + +Return an with base64-encoded image data and a MIME type. +Use the factory method or construct the block directly: + +```csharp +[McpServerTool, Description("Returns a generated image")] +public static ImageContentBlock GenerateImage() +{ + byte[] pngBytes = CreateImage(); // your image generation logic + return ImageContentBlock.FromBytes(pngBytes, "image/png"); +} +``` + +#### Audio content + +Return an with base64-encoded audio data and a MIME type. +The factory method encodes the raw bytes automatically: + +```csharp +[McpServerTool, Description("Returns a synthesized audio clip")] +public static AudioContentBlock Synthesize(string text) +{ + byte[] wavBytes = TextToSpeech(text); // your audio synthesis logic + return AudioContentBlock.FromBytes(wavBytes, "audio/wav"); +} +``` + +Supported audio MIME types include `audio/wav`, `audio/mp3`, `audio/ogg`, and others depending on what the client can handle. + +#### Embedded resources + +Return an to embed a resource directly in a tool result. +The resource can contain either text or binary data through or : + +```csharp +[McpServerTool, Description("Returns a file as an embedded resource")] +public static EmbeddedResourceBlock ReadFile(string path) +{ + return new EmbeddedResourceBlock + { + Resource = new TextResourceContents + { + Uri = $"file:///{path}", + MimeType = "text/plain", + Text = File.ReadAllText(path) + } + }; +} +``` + +For binary resources, use : + +```csharp +[McpServerTool, Description("Returns a binary file as an embedded resource")] +public static EmbeddedResourceBlock ReadBinaryFile(string path) +{ + return new EmbeddedResourceBlock + { + Resource = BlobResourceContents.FromBytes( + File.ReadAllBytes(path), $"file:///{path}", "application/octet-stream") + }; +} +``` + +#### Mixed content + +Tools can return multiple content blocks by returning `IEnumerable`: + +```csharp +[McpServerTool, Description("Returns text and an image")] +public static IEnumerable DescribeImage() +{ + byte[] imageBytes = GetImage(); + return + [ + new TextContentBlock { Text = "Here is the generated image:" }, + ImageContentBlock.FromBytes(imageBytes, "image/png"), + new TextContentBlock { Text = "The image shows a landscape." } + ]; +} +``` + +#### Content annotations + +Any content block can include to provide hints about the intended audience and priority: + +```csharp +new TextContentBlock +{ + Text = "Detailed debug information", + Annotations = new Annotations + { + Audience = [Role.Assistant], // Only for the LLM, not the user + Priority = 0.3f // Low priority (0.0 to 1.0) + } +} +``` + +### Consuming tools on the client + +Clients can discover and call tools using : + +```csharp +// List available tools +IList tools = await client.ListToolsAsync(); + +foreach (var tool in tools) +{ + Console.WriteLine($"{tool.Name}: {tool.Description}"); +} + +// Call a tool +CallToolResult result = await client.CallToolAsync( + "echo", + new Dictionary { ["message"] = "Hello!" }); + +// Process the result content blocks +foreach (var content in result.Content) +{ + switch (content) + { + case TextContentBlock text: + Console.WriteLine(text.Text); + break; + case ImageContentBlock image: + // image.DecodedData contains the raw bytes + File.WriteAllBytes("output.png", image.DecodedData.ToArray()); + break; + case AudioContentBlock audio: + // audio.DecodedData contains the raw bytes + File.WriteAllBytes("output.wav", audio.DecodedData.ToArray()); + break; + case EmbeddedResourceBlock resource: + if (resource.Resource is TextResourceContents textResource) + Console.WriteLine(textResource.Text); + break; + } +} +``` + +### Tool list change notifications + +Servers can dynamically add, remove, or modify tools at runtime. When the tool list changes, the server notifies connected clients so they can refresh their tool list. + +#### Sending notifications from the server + +Inject and call the notification method after modifying the tool list: + +```csharp +// After adding or removing tools dynamically +await server.SendNotificationAsync( + NotificationMethods.ToolListChangedNotification, + new ToolListChangedNotificationParams()); +``` + +#### Handling notifications on the client + +Register a notification handler on the client to respond to tool list changes: + +```csharp +mcpClient.RegisterNotificationHandler( + NotificationMethods.ToolListChangedNotification, + async (notification, cancellationToken) => + { + // Refresh the tool list + var updatedTools = await mcpClient.ListToolsAsync(cancellationToken: cancellationToken); + Console.WriteLine($"Tool list updated. {updatedTools.Count} tools available."); + }); +``` + +### JSON Schema generation + +Tool parameters are described using [JSON Schema 2020-12]. JSON schemas are automatically generated from .NET method signatures when the `[McpServerTool]` attribute is applied. Parameter types are mapped to JSON Schema types: + +[JSON Schema 2020-12]: https://json-schema.org/specification + +| .NET Type | JSON Schema Type | +|-----------|-----------------| +| `string` | `string` | +| `int`, `long` | `integer` | +| `float`, `double` | `number` | +| `bool` | `boolean` | +| `enum` | `string` with `enum` values | +| Complex types | `object` with `properties` | + +Use `[Description]` attributes on parameters to populate the `description` field in the generated schema. This helps LLMs understand what each parameter expects. + +```csharp +[McpServerTool, Description("Searches for items")] +public static string Search( + [Description("The search query string")] string query, + [Description("Maximum results to return (1-100)")] int maxResults = 10) +{ + // Schema will include descriptions and default value for maxResults +} +``` diff --git a/docs/concepts/transports/transports.md b/docs/concepts/transports/transports.md new file mode 100644 index 000000000..b65ab1e59 --- /dev/null +++ b/docs/concepts/transports/transports.md @@ -0,0 +1,173 @@ +--- +title: Transports +author: jeffhandley +description: How to configure stdio, Streamable HTTP, and SSE transports for MCP communication. +uid: transports +--- + +## Transports + +MCP uses a [transport layer] to handle the communication between clients and servers. Three transport mechanisms are supported: **stdio**, **Streamable HTTP**, and **SSE** (Server-Sent Events, legacy). + +[transport layer]: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports + +### stdio transport + +The stdio transport communicates over standard input and output streams. It is best suited for local integrations where the MCP server runs as a child process of the client. + +#### stdio client + +Use to launch a server process and communicate over its stdin/stdout: + +```csharp +var transport = new StdioClientTransport(new StdioClientTransportOptions +{ + Command = "dotnet", + Arguments = ["run", "--project", "path/to/McpServer"], + WorkingDirectory = "/home/user/projects", + EnvironmentVariables = new Dictionary + { + ["API_KEY"] = Environment.GetEnvironmentVariable("API_KEY") + }, + ShutdownTimeout = TimeSpan.FromSeconds(10) +}); + +await using var client = await McpClient.CreateAsync(transport); +``` + +Key properties: + +| Property | Description | +|----------|-------------| +| `Command` | The executable to launch (required) | +| `Arguments` | Command-line arguments for the process | +| `WorkingDirectory` | Working directory for the server process | +| `EnvironmentVariables` | Environment variables (merged with current; `null` values remove variables) | +| `ShutdownTimeout` | Graceful shutdown timeout (default: 5 seconds) | +| `StandardErrorLines` | Callback for stderr output from the server process | +| `Name` | Optional transport identifier for logging | + +#### stdio server + +Use for servers that communicate over stdin/stdout: + +```csharp +var builder = Host.CreateApplicationBuilder(args); + +builder.Services.AddMcpServer() + .WithStdioServerTransport() + .WithTools(); + +await builder.Build().RunAsync(); +``` + +### Streamable HTTP transport + +The [Streamable HTTP] transport uses HTTP for bidirectional communication with optional streaming. This is the recommended transport for remote servers. + +[Streamable HTTP]: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http + +#### Streamable HTTP client + +Use with : + +```csharp +var transport = new HttpClientTransport(new HttpClientTransportOptions +{ + Endpoint = new Uri("https://my-mcp-server.example.com/mcp"), + TransportMode = HttpTransportMode.StreamableHttp, + ConnectionTimeout = TimeSpan.FromSeconds(30), + AdditionalHeaders = new Dictionary + { + ["X-Custom-Header"] = "value" + } +}); + +await using var client = await McpClient.CreateAsync(transport); +``` + +The client also supports automatic transport detection with (the default), which tries Streamable HTTP first and falls back to SSE if the server does not support it: + +```csharp +var transport = new HttpClientTransport(new HttpClientTransportOptions +{ + Endpoint = new Uri("https://my-mcp-server.example.com/mcp"), + // TransportMode defaults to AutoDetect +}); +``` + +#### Resuming sessions + +Streamable HTTP supports session resumption. Save the session ID and reuse it to reconnect: + +```csharp +var transport = new HttpClientTransport(new HttpClientTransportOptions +{ + Endpoint = new Uri("https://my-mcp-server.example.com/mcp"), + KnownSessionId = previousSessionId +}); +``` + +#### Streamable HTTP server (ASP.NET Core) + +Use the `ModelContextProtocol.AspNetCore` package to host an MCP server over HTTP: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddMcpServer() + .WithHttpTransport() + .WithTools(); + +var app = builder.Build(); +app.MapMcp(); +app.Run(); +``` + +See the `ModelContextProtocol.AspNetCore` package [README](https://github.com/modelcontextprotocol/csharp-sdk/blob/main/src/ModelContextProtocol.AspNetCore/README.md) for more configuration options. + +### SSE transport (legacy) + +The [SSE (Server-Sent Events)] transport is a legacy mechanism that uses unidirectional server-to-client streaming with a separate HTTP endpoint for client-to-server messages. New implementations should prefer Streamable HTTP. + +[SSE (Server-Sent Events)]: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#http-with-sse + +> [!NOTE] +> The SSE transport is considered legacy. The [Streamable HTTP](#streamable-http-transport) transport is the recommended approach for HTTP-based communication and supports bidirectional streaming. + +#### SSE client + +Use with : + +```csharp +var transport = new HttpClientTransport(new HttpClientTransportOptions +{ + Endpoint = new Uri("https://my-mcp-server.example.com/sse"), + TransportMode = HttpTransportMode.Sse, + MaxReconnectionAttempts = 5, + DefaultReconnectionInterval = TimeSpan.FromSeconds(1) +}); + +await using var client = await McpClient.CreateAsync(transport); +``` + +SSE-specific configuration options: + +| Property | Description | +|----------|-------------| +| `MaxReconnectionAttempts` | Maximum number of reconnection attempts on stream disconnect (default: 5) | +| `DefaultReconnectionInterval` | Wait time between reconnection attempts (default: 1 second) | + +#### SSE server (ASP.NET Core) + +The ASP.NET Core integration supports SSE transport alongside Streamable HTTP. The same `MapMcp()` endpoint handles both protocols. + +### Transport mode comparison + +| Feature | stdio | Streamable HTTP | SSE (Legacy) | +|---------|-------|----------------|--------------| +| Process model | Child process | Remote HTTP | Remote HTTP | +| Direction | Bidirectional | Bidirectional | Server→client stream + client→server POST | +| Session resumption | N/A | ✓ | ✗ | +| Authentication | Process-level | HTTP auth (OAuth, headers) | HTTP auth (OAuth, headers) | +| Best for | Local tools | Remote servers | Legacy compatibility | From 137f3fee73e91af7afe6d4ae282abe0c9ab7e353 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 01:55:05 -0800 Subject: [PATCH 02/15] Add sampling docs, tools error handling, and SSE server example - New docs/concepts/sampling/sampling.md covering server-side SampleAsync and AsSamplingChatClient, client-side CreateSamplingHandler and custom SamplingHandler delegate, and capability negotiation - Add error handling section to docs/concepts/tools/tools.md with examples for automatic exception handling, McpProtocolException, and client-side IsError checking - Add SSE server code example to docs/concepts/transports/transports.md - Add sampling link to docs/concepts/index.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/concepts/index.md | 1 + docs/concepts/sampling/sampling.md | 122 +++++++++++++++++++++++++ docs/concepts/tools/tools.md | 60 ++++++++++++ docs/concepts/transports/transports.md | 16 +++- 4 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 docs/concepts/sampling/sampling.md diff --git a/docs/concepts/index.md b/docs/concepts/index.md index 14392f9dd..94d1224c3 100644 --- a/docs/concepts/index.md +++ b/docs/concepts/index.md @@ -19,6 +19,7 @@ Welcome to the conceptual documentation for the Model Context Protocol SDK. Here | Title | Description | | - | - | +| [Sampling](sampling/sampling.md) | Learn how servers request LLM completions from the client using the sampling feature. | | [Roots](roots/roots.md) | Learn how clients provide filesystem roots to servers for context-aware operations. | | [Elicitation](elicitation/elicitation.md) | Learn how to request additional information from users during interactions. | diff --git a/docs/concepts/sampling/sampling.md b/docs/concepts/sampling/sampling.md new file mode 100644 index 000000000..26e60edcc --- /dev/null +++ b/docs/concepts/sampling/sampling.md @@ -0,0 +1,122 @@ +--- +title: Sampling +author: jeffhandley +description: How servers request LLM completions from the client using the sampling feature. +uid: sampling +--- + +## Sampling + +MCP [sampling] allows servers to request LLM completions from the client. This enables agentic behaviors where a server-side tool delegates reasoning back to the client's language model — for example, summarizing content, generating text, or making decisions. + +[sampling]: https://modelcontextprotocol.io/specification/2025-11-25/client/sampling + +### How sampling works + +1. The server calls (or uses the adapter) during tool execution. +2. The request is sent to the connected client over MCP. +3. The client's processes the request — typically by forwarding it to an LLM. +4. The client returns the LLM response to the server, which continues tool execution. + +### Server: requesting a completion + +Inject into a tool method and use the extension method to get an that sends requests through the connected client: + +```csharp +[McpServerTool(Name = "SummarizeContent"), Description("Summarizes the given text")] +public static async Task Summarize( + McpServer server, + [Description("The text to summarize")] string text, + CancellationToken cancellationToken) +{ + ChatMessage[] messages = + [ + new(ChatRole.User, "Briefly summarize the following content:"), + new(ChatRole.User, text), + ]; + + ChatOptions options = new() + { + MaxOutputTokens = 256, + Temperature = 0.3f, + }; + + return $"Summary: {await server.AsSamplingChatClient().GetResponseAsync(messages, options, cancellationToken)}"; +} +``` + +Alternatively, use directly for lower-level control: + +```csharp +CreateMessageResult result = await server.SampleAsync( + new CreateMessageRequestParams + { + Messages = + [ + new SamplingMessage + { + Role = Role.User, + Content = [new TextContentBlock { Text = "What is 2 + 2?" }] + } + ], + MaxTokens = 100, + }, + cancellationToken); + +string response = result.Content.OfType().FirstOrDefault()?.Text ?? string.Empty; +``` + +### Client: handling sampling requests + +Set when creating the client. This handler is called when a server sends a `sampling/createMessage` request. + +#### Using an IChatClient + +The simplest approach is to use with any implementation: + +```csharp +IChatClient chatClient = new OllamaChatClient(new Uri("http://localhost:11434"), "llama3"); + +McpClientOptions options = new() +{ + Handlers = new() + { + SamplingHandler = chatClient.CreateSamplingHandler() + } +}; + +await using var client = await McpClient.CreateAsync(transport, options); +``` + +#### Custom handler + +For full control, provide a custom delegate: + +```csharp +McpClientOptions options = new() +{ + Handlers = new() + { + SamplingHandler = async (request, progress, cancellationToken) => + { + // Forward to your LLM, apply content filtering, etc. + string prompt = request?.Messages?.LastOrDefault()?.Content switch + { + TextContentBlock text => text.Text, + _ => string.Empty + }; + + return new CreateMessageResult + { + Model = "my-model", + Role = Role.Assistant, + Content = [new TextContentBlock { Text = $"Response to: {prompt}" }] + }; + } + } +}; +``` + +### Capability negotiation + +Sampling requires the client to advertise the `sampling` capability. This is handled automatically — when a is set, the client includes the sampling capability during initialization. The server can check whether the client supports sampling before calling ; if sampling is not supported, the method throws . diff --git a/docs/concepts/tools/tools.md b/docs/concepts/tools/tools.md index 8afb78b4b..e3bd61461 100644 --- a/docs/concepts/tools/tools.md +++ b/docs/concepts/tools/tools.md @@ -189,6 +189,66 @@ foreach (var content in result.Content) } ``` +### Error handling + +Tool errors in MCP are distinct from protocol errors. When a tool encounters an error during execution, the error is reported inside the with set to `true`, rather than as a protocol-level exception. This allows the LLM to see the error and potentially recover. + +#### Automatic exception handling + +When a tool method throws an exception, the server automatically catches it and returns a `CallToolResult` with `IsError = true`. is re-thrown as a JSON-RPC error, and is re-thrown when the cancellation token was triggered. All other exceptions are returned as tool error results. For subclasses, the exception message is included in the error text; for other exceptions, a generic message is returned to avoid leaking internal details. + +```csharp +[McpServerTool, Description("Divides two numbers")] +public static double Divide(double a, double b) +{ + if (b == 0) + { + // ArgumentException is not an McpException, so the client receives a generic message: + // "An error occurred invoking 'divide'." + throw new ArgumentException("Cannot divide by zero"); + } + + return a / b; +} +``` + +#### Protocol errors + +Throw to signal a protocol-level error (e.g., invalid parameters or unknown tool). These exceptions propagate as JSON-RPC error responses rather than tool error results: + +```csharp +[McpServerTool, Description("Processes the input")] +public static string Process(string input) +{ + if (string.IsNullOrEmpty(input)) + { + // Propagates as a JSON-RPC error with code -32602 (InvalidParams) + // and message "Missing required input" + throw new McpProtocolException("Missing required input", McpErrorCode.InvalidParams); + } + + return $"Processed: {input}"; +} +``` + +#### Checking for errors on the client + +On the client side, inspect the property after calling a tool: + +```csharp +CallToolResult result = await client.CallToolAsync("divide", new Dictionary +{ + ["a"] = 10, + ["b"] = 0 +}); + +if (result.IsError is true) +{ + // Prints: "Tool error: An error occurred invoking 'divide'." + Console.WriteLine($"Tool error: {result.Content.OfType().FirstOrDefault()?.Text}"); +} +``` + ### Tool list change notifications Servers can dynamically add, remove, or modify tools at runtime. When the tool list changes, the server notifies connected clients so they can refresh their tool list. diff --git a/docs/concepts/transports/transports.md b/docs/concepts/transports/transports.md index b65ab1e59..b584f0f63 100644 --- a/docs/concepts/transports/transports.md +++ b/docs/concepts/transports/transports.md @@ -160,7 +160,21 @@ SSE-specific configuration options: #### SSE server (ASP.NET Core) -The ASP.NET Core integration supports SSE transport alongside Streamable HTTP. The same `MapMcp()` endpoint handles both protocols. +The ASP.NET Core integration supports SSE transport alongside Streamable HTTP. The same `MapMcp()` endpoint handles both protocols — clients connecting with SSE are automatically served using the legacy SSE mechanism: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddMcpServer() + .WithHttpTransport() + .WithTools(); + +var app = builder.Build(); +app.MapMcp(); // Handles both Streamable HTTP and SSE clients +app.Run(); +``` + +No additional configuration is needed. When a client connects using the SSE protocol, the server responds with an SSE stream for server-to-client messages and accepts client-to-server messages via a separate POST endpoint. ### Transport mode comparison From db099e0965c4951e26236afb9a4a6cdeebf1f799 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 02:16:28 -0800 Subject: [PATCH 03/15] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/concepts/pagination/pagination.md | 2 +- docs/concepts/resources/resources.md | 23 ++++++++++++++++++++--- docs/concepts/transports/transports.md | 2 +- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/concepts/pagination/pagination.md b/docs/concepts/pagination/pagination.md index 96467250e..60f844d7e 100644 --- a/docs/concepts/pagination/pagination.md +++ b/docs/concepts/pagination/pagination.md @@ -7,7 +7,7 @@ uid: pagination ## Pagination -MCP uses [cursor-based pagination] for list operations that may return large result sets. This applies to listing tools, prompts, resources, and resource templates. +MCP uses [cursor-based pagination] for all list operations that may return large result sets. [cursor-based pagination]: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination diff --git a/docs/concepts/resources/resources.md b/docs/concepts/resources/resources.md index 542b24dcb..f187f5460 100644 --- a/docs/concepts/resources/resources.md +++ b/docs/concepts/resources/resources.md @@ -41,17 +41,34 @@ Template resources use [URI templates (RFC 6570)] with parameters. They are retu [McpServerResourceType] public class FileResources { + // Configure a root directory for all file:// resources + private static readonly string RootDirectory = Path.GetFullPath(AppContext.BaseDirectory); + [McpServerResource(UriTemplate = "file:///{path}", Name = "File Resource")] - [Description("Reads a file by its path")] + [Description("Reads a file by its path within the configured root directory")] public static ResourceContents ReadFile(string path) { - if (File.Exists(path)) + if (string.IsNullOrWhiteSpace(path)) + { + throw new McpException("Path must be provided."); + } + + // Combine the requested path with the root directory and canonicalize it + var fullPath = Path.GetFullPath(Path.Combine(RootDirectory, path)); + + // Ensure the final path is still under the allowed root directory + if (!fullPath.StartsWith(RootDirectory, StringComparison.OrdinalIgnoreCase)) + { + throw new McpException("Requested file path is outside the allowed directory."); + } + + if (File.Exists(fullPath)) { return new TextResourceContents { Uri = $"file:///{path}", MimeType = "text/plain", - Text = File.ReadAllText(path) + Text = File.ReadAllText(fullPath) }; } diff --git a/docs/concepts/transports/transports.md b/docs/concepts/transports/transports.md index b584f0f63..99ba36e0a 100644 --- a/docs/concepts/transports/transports.md +++ b/docs/concepts/transports/transports.md @@ -130,7 +130,7 @@ See the `ModelContextProtocol.AspNetCore` package [README](https://github.com/mo The [SSE (Server-Sent Events)] transport is a legacy mechanism that uses unidirectional server-to-client streaming with a separate HTTP endpoint for client-to-server messages. New implementations should prefer Streamable HTTP. -[SSE (Server-Sent Events)]: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#http-with-sse +[SSE (Server-Sent Events)]: https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse > [!NOTE] > The SSE transport is considered legacy. The [Streamable HTTP](#streamable-http-transport) transport is the recommended approach for HTTP-based communication and supports bidirectional streaming. From a079c0f33ec8afb40ebba8391da924e481f1a270 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 02:38:34 -0800 Subject: [PATCH 04/15] Address PR feedback on conceptual docs - Minimize cancellation doc to focus on MCP protocol behavior, not CancellationToken patterns - Simplify ping doc to a single API call example - Soften capabilities overview to not prescribe initialize handshake steps - Remove enum row from JSON Schema type mapping table - Replace file path examples with ID-based lookups to avoid path traversal - Use BlobResourceContents.FromBytes() factory consistently - Switch inline markdown links to reference-style Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/concepts/cancellation/cancellation.md | 45 ++++------------------ docs/concepts/capabilities/capabilities.md | 12 +----- docs/concepts/ping/ping.md | 35 +---------------- docs/concepts/prompts/prompts.md | 8 ++-- docs/concepts/resources/resources.md | 41 +++++++------------- docs/concepts/tools/tools.md | 17 ++++---- 6 files changed, 35 insertions(+), 123 deletions(-) diff --git a/docs/concepts/cancellation/cancellation.md b/docs/concepts/cancellation/cancellation.md index 8f696df15..cdce47a59 100644 --- a/docs/concepts/cancellation/cancellation.md +++ b/docs/concepts/cancellation/cancellation.md @@ -7,46 +7,18 @@ uid: cancellation ## Cancellation -MCP supports [cancellation] of in-flight requests. Either side can cancel a previously issued request by using .NET's [task cancellation] with a `CancellationToken` to send a cancellation notification. +MCP supports [cancellation] of in-flight requests. Either side can cancel a previously issued request, and `CancellationToken` parameters on MCP methods are wired to send and receive `notifications/cancelled` notifications over the protocol. [cancellation]: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation [task cancellation]: https://learn.microsoft.com/dotnet/standard/parallel-programming/task-cancellation -### Overview +### How cancellation maps to MCP notifications -When a client cancels a pending request, a `notifications/cancelled` notification is sent to the server. The server's `CancellationToken` for that request is then triggered, allowing the server-side handler to stop work gracefully. This same mechanism works in reverse for server-to-client requests. - -### Client-side cancellation - -All client methods accept a `CancellationToken` parameter. Cancel a pending request by canceling the token: - -```csharp -using var cts = new CancellationTokenSource(); - -// Start a long-running tool call -var toolTask = client.CallToolAsync( - "long_running_operation", - new Dictionary { ["duration"] = 60 }, - cancellationToken: cts.Token); - -// Cancel after 5 seconds -cts.CancelAfter(TimeSpan.FromSeconds(5)); - -try -{ - var result = await toolTask; -} -catch (OperationCanceledException) -{ - Console.WriteLine("Tool call was cancelled."); -} -``` - -When the `CancellationToken` is cancelled, a `notifications/cancelled` notification is automatically sent to the server with the request ID, allowing the server to stop processing. +When a `CancellationToken` passed to a client method (such as ) is cancelled, a `notifications/cancelled` notification is sent to the server with the request ID. On the server side, the `CancellationToken` provided to the tool method is then triggered, allowing the handler to stop work gracefully. This same mechanism works in reverse for server-to-client requests. ### Server-side cancellation handling -Server tool methods should accept a `CancellationToken` parameter and check it during long-running operations: +Server tool methods receive a `CancellationToken` that is triggered when the client sends a cancellation notification. Pass this token through to any async operations so they stop promptly: ```csharp [McpServerTool, Description("A long-running computation")] @@ -56,9 +28,6 @@ public static async Task LongComputation( { for (int i = 0; i < iterations; i++) { - // Check for cancellation on each iteration - cancellationToken.ThrowIfCancellationRequested(); - await Task.Delay(1000, cancellationToken); } @@ -66,7 +35,7 @@ public static async Task LongComputation( } ``` -When the client sends a cancellation notification, the `CancellationToken` provided to the tool method is automatically triggered. The `OperationCanceledException` propagates back to the client as a cancellation response. +When the client sends a cancellation notification, the `OperationCanceledException` propagates back to the client as a cancellation response. ### Cancellation notification details @@ -75,9 +44,9 @@ The cancellation notification includes: - **RequestId**: The ID of the request to cancel, allowing the receiver to correlate the cancellation with the correct in-flight request. - **Reason**: An optional human-readable reason for the cancellation. +Cancellation notifications can be observed by registering a handler: + ```csharp -// This is sent automatically when a CancellationToken is cancelled, -// but you can also observe cancellation notifications: mcpClient.RegisterNotificationHandler( NotificationMethods.CancelledNotification, (notification, ct) => diff --git a/docs/concepts/capabilities/capabilities.md b/docs/concepts/capabilities/capabilities.md index a1c464f83..1329072a7 100644 --- a/docs/concepts/capabilities/capabilities.md +++ b/docs/concepts/capabilities/capabilities.md @@ -13,13 +13,7 @@ MCP uses a [capability negotiation] mechanism during connection initialization. ### Overview -When a client connects to a server, the initialization handshake includes: - -1. The **client** sends an `initialize` request with its and protocol version. -2. The **server** responds with its and the negotiated protocol version. -3. The client sends an `initialized` notification to confirm the session is ready. - -Both sides should check the other's capabilities before using optional features. +During connection setup, clients and servers exchange their supported capabilities so each side can adapt its behavior accordingly. After initialization, both sides should check the other's capabilities before using optional features. ### Client capabilities @@ -119,9 +113,7 @@ if (client.ServerCapabilities.Completions is not null) ### Protocol version negotiation -The client specifies the MCP protocol version it supports in the `initialize` request. The server responds with the negotiated protocol version, which may differ from the client's requested version if the server supports an earlier version. - -After initialization, the negotiated version is available on both sides: +During connection setup, the client and server negotiate a mutually supported MCP protocol version. After initialization, the negotiated version is available on both sides: ```csharp // On the client diff --git a/docs/concepts/ping/ping.md b/docs/concepts/ping/ping.md index 6640b42f9..d7eb5d8c1 100644 --- a/docs/concepts/ping/ping.md +++ b/docs/concepts/ping/ping.md @@ -11,45 +11,12 @@ MCP includes a [ping mechanism] that allows either side of a connection to verif [ping mechanism]: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping -### Overview - -The ping operation is a simple request/response exchange. Either the client or the server can initiate a ping, and the other side responds automatically. Ping responses are handled automatically—callers only need to invoke the method to send a ping. - ### Pinging from the client Use the method to verify the server is responsive: ```csharp -await using var client = await McpClient.CreateAsync(transport); - -try -{ - await client.PingAsync(); - Console.WriteLine("Server is responsive."); -} -catch (OperationCanceledException) -{ - Console.WriteLine("Ping timed out - server may be unresponsive."); -} -catch (McpException ex) -{ - Console.WriteLine($"Ping failed: {ex.Message}"); -} -``` - -A timeout can also be specified using a `CancellationToken`: - -```csharp -using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - -try -{ - await client.PingAsync(cancellationToken: cts.Token); -} -catch (OperationCanceledException) -{ - Console.WriteLine("Server did not respond within 5 seconds."); -} +await client.PingAsync(cancellationToken); ``` ### Automatic ping handling diff --git a/docs/concepts/prompts/prompts.md b/docs/concepts/prompts/prompts.md index 3ba8723ac..888b0733e 100644 --- a/docs/concepts/prompts/prompts.md +++ b/docs/concepts/prompts/prompts.md @@ -94,11 +94,11 @@ public static IEnumerable AnalyzeImage( For protocol-specific content types like , use instead of `ChatMessage`. `PromptMessage` has a `Role` property and a single `Content` property of type : ```csharp -[McpServerPrompt, Description("A prompt that includes a file resource")] +[McpServerPrompt, Description("A prompt that includes a document resource")] public static IEnumerable ReviewDocument( - [Description("Path to the document to review")] string path) + [Description("The document ID to review")] string documentId) { - string content = File.ReadAllText(path); + string content = LoadDocument(documentId); // application logic to load by ID return [ new PromptMessage @@ -113,7 +113,7 @@ public static IEnumerable ReviewDocument( { Resource = new TextResourceContents { - Uri = $"file:///{path}", + Uri = $"docs://documents/{documentId}", MimeType = "text/plain", Text = content } diff --git a/docs/concepts/resources/resources.md b/docs/concepts/resources/resources.md index f187f5460..064f16269 100644 --- a/docs/concepts/resources/resources.md +++ b/docs/concepts/resources/resources.md @@ -39,40 +39,25 @@ Template resources use [URI templates (RFC 6570)] with parameters. They are retu ```csharp [McpServerResourceType] -public class FileResources +public class DocumentResources { - // Configure a root directory for all file:// resources - private static readonly string RootDirectory = Path.GetFullPath(AppContext.BaseDirectory); - - [McpServerResource(UriTemplate = "file:///{path}", Name = "File Resource")] - [Description("Reads a file by its path within the configured root directory")] - public static ResourceContents ReadFile(string path) + [McpServerResource(UriTemplate = "docs://articles/{id}", Name = "Article")] + [Description("Returns an article by its ID")] + public static ResourceContents GetArticle(string id) { - if (string.IsNullOrWhiteSpace(path)) - { - throw new McpException("Path must be provided."); - } + string? content = LoadArticle(id); // application logic to load by ID - // Combine the requested path with the root directory and canonicalize it - var fullPath = Path.GetFullPath(Path.Combine(RootDirectory, path)); - - // Ensure the final path is still under the allowed root directory - if (!fullPath.StartsWith(RootDirectory, StringComparison.OrdinalIgnoreCase)) + if (content is null) { - throw new McpException("Requested file path is outside the allowed directory."); + throw new McpException($"Article not found: {id}"); } - if (File.Exists(fullPath)) + return new TextResourceContents { - return new TextResourceContents - { - Uri = $"file:///{path}", - MimeType = "text/plain", - Text = File.ReadAllText(fullPath) - }; - } - - throw new McpException($"File not found: {path}"); + Uri = $"docs://articles/{id}", + MimeType = "text/plain", + Text = content + }; } } ``` @@ -83,7 +68,7 @@ Register resource types when building the server: builder.Services.AddMcpServer() .WithHttpTransport() .WithResources() - .WithResources(); + .WithResources(); ``` ### Reading text resources diff --git a/docs/concepts/tools/tools.md b/docs/concepts/tools/tools.md index e3bd61461..1d392a0c3 100644 --- a/docs/concepts/tools/tools.md +++ b/docs/concepts/tools/tools.md @@ -84,16 +84,16 @@ Return an to embed a The resource can contain either text or binary data through or : ```csharp -[McpServerTool, Description("Returns a file as an embedded resource")] -public static EmbeddedResourceBlock ReadFile(string path) +[McpServerTool, Description("Returns a document as an embedded resource")] +public static EmbeddedResourceBlock GetDocument() { return new EmbeddedResourceBlock { Resource = new TextResourceContents { - Uri = $"file:///{path}", + Uri = "docs://readme", MimeType = "text/plain", - Text = File.ReadAllText(path) + Text = "This is the document content." } }; } @@ -102,13 +102,13 @@ public static EmbeddedResourceBlock ReadFile(string path) For binary resources, use : ```csharp -[McpServerTool, Description("Returns a binary file as an embedded resource")] -public static EmbeddedResourceBlock ReadBinaryFile(string path) +[McpServerTool, Description("Returns a binary resource")] +public static EmbeddedResourceBlock GetBinaryData(string id) { + byte[] data = LoadData(id); // application logic to load data by ID return new EmbeddedResourceBlock { - Resource = BlobResourceContents.FromBytes( - File.ReadAllBytes(path), $"file:///{path}", "application/octet-stream") + Resource = BlobResourceContents.FromBytes(data, $"data://items/{id}", "application/octet-stream") }; } ``` @@ -291,7 +291,6 @@ Tool parameters are described using [JSON Schema 2020-12]. JSON schemas are auto | `int`, `long` | `integer` | | `float`, `double` | `number` | | `bool` | `boolean` | -| `enum` | `string` with `enum` values | | Complex types | `object` with `properties` | Use `[Description]` attributes on parameters to populate the `description` field in the generated schema. This helps LLMs understand what each parameter expects. From edd348a07138879fb0036a24d4ae9f990a04ed8d Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 04:12:22 -0800 Subject: [PATCH 05/15] Remove xref for OperationCanceledException --- docs/concepts/tools/tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/tools/tools.md b/docs/concepts/tools/tools.md index 1d392a0c3..17076ffe2 100644 --- a/docs/concepts/tools/tools.md +++ b/docs/concepts/tools/tools.md @@ -195,7 +195,7 @@ Tool errors in MCP are distinct from protocol errors. When a tool encounters an #### Automatic exception handling -When a tool method throws an exception, the server automatically catches it and returns a `CallToolResult` with `IsError = true`. is re-thrown as a JSON-RPC error, and is re-thrown when the cancellation token was triggered. All other exceptions are returned as tool error results. For subclasses, the exception message is included in the error text; for other exceptions, a generic message is returned to avoid leaking internal details. +When a tool method throws an exception, the server automatically catches it and returns a `CallToolResult` with `IsError = true`. is re-thrown as a JSON-RPC error, and `OperationCanceledException` is re-thrown when the cancellation token was triggered. All other exceptions are returned as tool error results. For subclasses, the exception message is included in the error text; for other exceptions, a generic message is returned to avoid leaking internal details. ```csharp [McpServerTool, Description("Divides two numbers")] From d79aa0fb6ac380403cf77d0b9bfe675816b4fcf8 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 14:53:29 -0800 Subject: [PATCH 06/15] Address PR feedback on conceptual docs - Add bulleted lists of all definition methods for tools, prompts, resources (attribute, Create factory, deriving, custom handler, request filter) - Note DataContent from M.E.AI mapping to content blocks in tools and prompts - Simplify roots example by removing unnecessary explicit capability config - Add special parameter types note (McpServer, IProgress, ClaimsPrincipal, DI) - Mention message filters for cancellation notification interception - Use tool.CallAsync pattern in client tool consumption example - Merge redundant intro in capabilities.md - Simplify elicitation EnumSchemaOption syntax and remove auto-fill claim - Simplify prompts intro text Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/concepts/cancellation/cancellation.md | 2 +- docs/concepts/capabilities/capabilities.md | 6 +---- docs/concepts/elicitation/elicitation.md | 8 +++---- docs/concepts/prompts/prompts.md | 16 +++++++++---- docs/concepts/resources/resources.md | 10 +++++++- docs/concepts/roots/roots.md | 9 +------- docs/concepts/tools/tools.md | 27 +++++++++++++++------- 7 files changed, 46 insertions(+), 32 deletions(-) diff --git a/docs/concepts/cancellation/cancellation.md b/docs/concepts/cancellation/cancellation.md index cdce47a59..50753d259 100644 --- a/docs/concepts/cancellation/cancellation.md +++ b/docs/concepts/cancellation/cancellation.md @@ -44,7 +44,7 @@ The cancellation notification includes: - **RequestId**: The ID of the request to cancel, allowing the receiver to correlate the cancellation with the correct in-flight request. - **Reason**: An optional human-readable reason for the cancellation. -Cancellation notifications can be observed by registering a handler: +Cancellation notifications can be observed by registering a handler. For broader interception of notifications and other messages, delegates can be added to the collection in . ```csharp mcpClient.RegisterNotificationHandler( diff --git a/docs/concepts/capabilities/capabilities.md b/docs/concepts/capabilities/capabilities.md index 1329072a7..b466736a4 100644 --- a/docs/concepts/capabilities/capabilities.md +++ b/docs/concepts/capabilities/capabilities.md @@ -7,14 +7,10 @@ uid: capabilities ## Capabilities -MCP uses a [capability negotiation] mechanism during connection initialization. Clients and servers exchange their supported capabilities, allowing each side to understand what features the other supports and adapt behavior accordingly. +MCP uses a [capability negotiation] mechanism during connection setup. Clients and servers exchange their supported capabilities so each side can adapt its behavior accordingly. Both sides should check the other's capabilities before using optional features. [capability negotiation]: https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization -### Overview - -During connection setup, clients and servers exchange their supported capabilities so each side can adapt its behavior accordingly. After initialization, both sides should check the other's capabilities before using optional features. - ### Client capabilities declares what features the client supports: diff --git a/docs/concepts/elicitation/elicitation.md b/docs/concepts/elicitation/elicitation.md index e484bd3a0..490084c04 100644 --- a/docs/concepts/elicitation/elicitation.md +++ b/docs/concepts/elicitation/elicitation.md @@ -73,8 +73,6 @@ var result = await server.ElicitAsync(new ElicitRequestParams }, cancellationToken); ``` -If the client returns accepted content with missing fields, the server automatically fills those fields with their schema defaults. This ensures tools always receive complete data regardless of client behavior. - #### Enum schema formats Enum schemas allow the server to present a set of choices to the user. @@ -91,9 +89,9 @@ Enum schemas allow the server to present a set of choices to the user. Description = "Task priority", OneOf = [ - new ElicitRequestParams.EnumSchemaOption { Const = "p0", Title = "Critical (P0)" }, - new ElicitRequestParams.EnumSchemaOption { Const = "p1", Title = "High (P1)" }, - new ElicitRequestParams.EnumSchemaOption { Const = "p2", Title = "Normal (P2)" }, + new() { Const = "p0", Title = "Critical (P0)" }, + new() { Const = "p1", Title = "High (P1)" }, + new() { Const = "p2", Title = "Normal (P2)" }, ], Default = "p2" }, diff --git a/docs/concepts/prompts/prompts.md b/docs/concepts/prompts/prompts.md index 888b0733e..e23aee36d 100644 --- a/docs/concepts/prompts/prompts.md +++ b/docs/concepts/prompts/prompts.md @@ -15,11 +15,19 @@ This document covers implementing prompts on the server, consuming them from the ### Defining prompts on the server -Prompts are defined as methods marked with the attribute within a class marked with . Prompts can return `ChatMessage` instances for simple text/image content, or instances when protocol-specific content types like are needed. +Prompts can be defined in several ways: + +- Using the attribute on methods within a class marked with +- Using factory methods from a delegate, `MethodInfo`, or `AIFunction` +- Deriving from or +- Implementing a custom via +- Implementing a low-level + +The attribute-based approach is the most common and is shown throughout this document. Prompts can return `ChatMessage` instances for simple text/image content, or instances when protocol-specific content types like are needed. #### Simple prompts -A prompt without arguments returns a fixed message: +A prompt without arguments: ```csharp [McpServerPromptType] @@ -33,7 +41,7 @@ public class MyPrompts #### Prompts with arguments -Prompts can accept parameters to customize the generated messages. Use `[Description]` attributes to document each parameter: +Prompts can accept parameters to customize the generated messages. Use `[Description]` attributes to document each parameter. In addition to prompt arguments, methods can accept special parameter types that are resolved automatically: , `IProgress`, `ClaimsPrincipal`, and any service registered through dependency injection. ```csharp [McpServerPromptType] @@ -66,7 +74,7 @@ builder.Services.AddMcpServer() ### Rich content in prompts -Prompt messages can contain more than just text. For text and image content, use `ChatMessage` from Microsoft.Extensions.AI. For protocol-specific content types like embedded resources, use instead. +Prompt messages can contain more than just text. For text and image content, use `ChatMessage` from Microsoft.Extensions.AI. `DataContent` is automatically mapped to the appropriate MCP content block: image MIME types become , audio MIME types become , and all other MIME types become with binary resource contents. For text embedded resources specifically, use directly. #### Image content diff --git a/docs/concepts/resources/resources.md b/docs/concepts/resources/resources.md index 064f16269..ba8121152 100644 --- a/docs/concepts/resources/resources.md +++ b/docs/concepts/resources/resources.md @@ -15,7 +15,15 @@ This document covers implementing resources on the server, consuming them from t ### Defining resources on the server -Resources are defined as methods marked with the attribute within a class marked with . The attribute specifies the URI template that identifies the resource. +Resources can be defined in several ways: + +- Using the attribute on methods within a class marked with +- Using factory methods from a delegate, `MethodInfo`, or `AIFunction` +- Deriving from or +- Implementing a custom via +- Implementing a low-level + +The attribute-based approach is the most common and is shown throughout this document. #### Direct resources diff --git a/docs/concepts/roots/roots.md b/docs/concepts/roots/roots.md index 8d1d2097c..1c5144217 100644 --- a/docs/concepts/roots/roots.md +++ b/docs/concepts/roots/roots.md @@ -23,18 +23,11 @@ Each root is represented by a with a U ### Declaring roots capability on the client -Clients advertise their support for roots in the capabilities sent during initialization. To enable roots, configure the property and provide a handler that returns the root list: +Clients advertise their support for roots in the capabilities sent during initialization. The roots capability is created automatically when a roots handler is provided. Configure the handler through : ```csharp var options = new McpClientOptions { - Capabilities = new ClientCapabilities - { - Roots = new RootsCapability - { - ListChanged = true // Notify server when roots change - } - }, Handlers = new McpClientHandlers { RootsHandler = (request, cancellationToken) => diff --git a/docs/concepts/tools/tools.md b/docs/concepts/tools/tools.md index 17076ffe2..6ac2f9a5e 100644 --- a/docs/concepts/tools/tools.md +++ b/docs/concepts/tools/tools.md @@ -15,7 +15,15 @@ This document covers tool content types, change notifications, and schema genera ### Defining tools on the server -Tools are defined as methods marked with the attribute within a class marked with . Parameters are automatically deserialized from JSON and documented using `[Description]` attributes. +Tools can be defined in several ways: + +- Using the attribute on methods within a class marked with +- Using factory methods from a delegate, `MethodInfo`, or `AIFunction` +- Deriving from or +- Implementing a custom via +- Implementing a low-level + +The attribute-based approach is the most common and is shown throughout this document. Parameters are automatically deserialized from JSON and documented using `[Description]` attributes. In addition to tool arguments, methods can accept special parameter types that are resolved automatically: , `IProgress`, `ClaimsPrincipal`, and any service registered through dependency injection. ```csharp [McpServerToolType] @@ -37,7 +45,7 @@ builder.Services.AddMcpServer() ### Content types -Tools can return various content types. The simplest is a `string`, which is automatically wrapped in a . For richer content, tools can return one or more instances. +Tools can return various content types. The simplest is a `string`, which is automatically wrapped in a . For richer content, tools can return one or more instances. Tools can also return `DataContent` from Microsoft.Extensions.AI, which is automatically mapped to the appropriate MCP content block: image MIME types become , audio MIME types become , and all other MIME types become with binary resource contents. #### Text content @@ -160,9 +168,9 @@ foreach (var tool in tools) Console.WriteLine($"{tool.Name}: {tool.Description}"); } -// Call a tool -CallToolResult result = await client.CallToolAsync( - "echo", +// Call a tool by finding it in the list +McpClientTool echoTool = tools.First(t => t.Name == "echo"); +CallToolResult result = await echoTool.CallAsync( new Dictionary { ["message"] = "Hello!" }); // Process the result content blocks @@ -174,11 +182,9 @@ foreach (var content in result.Content) Console.WriteLine(text.Text); break; case ImageContentBlock image: - // image.DecodedData contains the raw bytes File.WriteAllBytes("output.png", image.DecodedData.ToArray()); break; case AudioContentBlock audio: - // audio.DecodedData contains the raw bytes File.WriteAllBytes("output.wav", audio.DecodedData.ToArray()); break; case EmbeddedResourceBlock resource: @@ -195,7 +201,12 @@ Tool errors in MCP are distinct from protocol errors. When a tool encounters an #### Automatic exception handling -When a tool method throws an exception, the server automatically catches it and returns a `CallToolResult` with `IsError = true`. is re-thrown as a JSON-RPC error, and `OperationCanceledException` is re-thrown when the cancellation token was triggered. All other exceptions are returned as tool error results. For subclasses, the exception message is included in the error text; for other exceptions, a generic message is returned to avoid leaking internal details. +When a tool method throws an exception, the server catches it and returns a `CallToolResult` with `IsError = true`, with the following exceptions: + +- is re-thrown as a JSON-RPC error response (not a tool error result). +- `OperationCanceledException` is re-thrown when the cancellation token was triggered. + +For all other exceptions, the error is returned as a tool result. If the exception derives from (excluding `McpProtocolException`, which is re-thrown above), its message is included in the error text; otherwise, a generic message is returned to avoid leaking internal details. ```csharp [McpServerTool, Description("Divides two numbers")] From 08a9f5fe2ae1d7e3877b301baa4dc60246b5dc1e Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 14:55:55 -0800 Subject: [PATCH 07/15] Apply suggestions from code review Co-authored-by: Mike Kistler Co-authored-by: Stephen Toub --- docs/concepts/pagination/pagination.md | 3 +++ docs/concepts/prompts/prompts.md | 10 +++------- docs/concepts/resources/resources.md | 2 +- docs/concepts/transports/transports.md | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/concepts/pagination/pagination.md b/docs/concepts/pagination/pagination.md index 60f844d7e..bb48ffa0b 100644 --- a/docs/concepts/pagination/pagination.md +++ b/docs/concepts/pagination/pagination.md @@ -96,3 +96,6 @@ builder.Services.AddMcpServer() > [!NOTE] > The cursor format is opaque to the client. Servers can use any encoding scheme (numeric offsets, encoded tokens, database cursors, etc.) as long as they can parse their own cursors on subsequent requests. + +> [!NOTE] +> Because the cursor format is opaque to the client, _any_ value specified in the cursor, including the empty string, signals that more results are available. If an MCP server erroneously sends an empty string cursor with the final page of results, clients can implement their own low-level pagination scheme to work around this case. diff --git a/docs/concepts/prompts/prompts.md b/docs/concepts/prompts/prompts.md index e23aee36d..08ceaf9c0 100644 --- a/docs/concepts/prompts/prompts.md +++ b/docs/concepts/prompts/prompts.md @@ -50,14 +50,10 @@ public class CodePrompts [McpServerPrompt, Description("Generates a code review prompt")] public static IEnumerable CodeReview( [Description("The programming language")] string language, - [Description("The code to review")] string code) - { - return + [Description("The code to review")] string code) => [ - new ChatMessage(ChatRole.User, - $"Please review the following {language} code:\n\n```{language}\n{code}\n```"), - new ChatMessage(ChatRole.Assistant, - "I'll review the code for correctness, style, and potential improvements.") + new(ChatRole.User, $"Please review the following {language} code:\n\n```{language}\n{code}\n```"), + new(ChatRole.Assistant, "I'll review the code for correctness, style, and potential improvements.") ]; } } diff --git a/docs/concepts/resources/resources.md b/docs/concepts/resources/resources.md index ba8121152..02d8b6677 100644 --- a/docs/concepts/resources/resources.md +++ b/docs/concepts/resources/resources.md @@ -215,7 +215,7 @@ builder.Services.AddMcpServer() if (ctx.Params?.Uri is { } uri) { // Track the subscription (e.g., in a concurrent dictionary) - subscriptions.TryAdd(uri, ctx.Server.SessionId); + subscriptions[context.Server.SessionId].TryAdd(uri, 0); } return new EmptyResult(); }) diff --git a/docs/concepts/transports/transports.md b/docs/concepts/transports/transports.md index 99ba36e0a..f114ac682 100644 --- a/docs/concepts/transports/transports.md +++ b/docs/concepts/transports/transports.md @@ -13,7 +13,7 @@ MCP uses a [transport layer] to handle the communication between clients and ser ### stdio transport -The stdio transport communicates over standard input and output streams. It is best suited for local integrations where the MCP server runs as a child process of the client. +The stdio transport communicates over standard input and output streams. It is best suited for local integrations, as the MCP server runs as a child process of the client. #### stdio client From 958c783f3f6352b911346e2e2a2eb1ccfac6042e Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 15:02:24 -0800 Subject: [PATCH 08/15] Use dnx NuGet.Mcp.Server in stdio client example Replace dotnet run --project example with dnx NuGet.Mcp.Server to demonstrate connecting to a published NuGet MCP server package. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/concepts/transports/transports.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/concepts/transports/transports.md b/docs/concepts/transports/transports.md index f114ac682..1aa4cde69 100644 --- a/docs/concepts/transports/transports.md +++ b/docs/concepts/transports/transports.md @@ -17,18 +17,15 @@ The stdio transport communicates over standard input and output streams. It is b #### stdio client -Use to launch a server process and communicate over its stdin/stdout: +Use to launch a server process and communicate over its stdin/stdout. This example connects to the [NuGet MCP Server]: + +[NuGet MCP Server]: https://learn.microsoft.com/nuget/concepts/nuget-mcp-server ```csharp var transport = new StdioClientTransport(new StdioClientTransportOptions { - Command = "dotnet", - Arguments = ["run", "--project", "path/to/McpServer"], - WorkingDirectory = "/home/user/projects", - EnvironmentVariables = new Dictionary - { - ["API_KEY"] = Environment.GetEnvironmentVariable("API_KEY") - }, + Command = "dnx", + Arguments = ["NuGet.Mcp.Server"], ShutdownTimeout = TimeSpan.FromSeconds(10) }); From 952ba6c0cf9a313de55739645c319a3965171b60 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 15:28:06 -0800 Subject: [PATCH 09/15] Fix xref namespace for McpClientHandlers.RootsHandler Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/concepts/roots/roots.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/roots/roots.md b/docs/concepts/roots/roots.md index 1c5144217..94b330871 100644 --- a/docs/concepts/roots/roots.md +++ b/docs/concepts/roots/roots.md @@ -23,7 +23,7 @@ Each root is represented by a with a U ### Declaring roots capability on the client -Clients advertise their support for roots in the capabilities sent during initialization. The roots capability is created automatically when a roots handler is provided. Configure the handler through : +Clients advertise their support for roots in the capabilities sent during initialization. The roots capability is created automatically when a roots handler is provided. Configure the handler through : ```csharp var options = new McpClientOptions From 89c744e36f1299d2de6f0d3589ec1f2bfe72f714 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 16:03:09 -0800 Subject: [PATCH 10/15] Fix compilation errors in new docs --- docs/concepts/ping/ping.md | 2 +- docs/concepts/resources/resources.md | 2 +- docs/concepts/sampling/sampling.md | 7 ++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/concepts/ping/ping.md b/docs/concepts/ping/ping.md index d7eb5d8c1..c455cb64e 100644 --- a/docs/concepts/ping/ping.md +++ b/docs/concepts/ping/ping.md @@ -16,7 +16,7 @@ MCP includes a [ping mechanism] that allows either side of a connection to verif Use the method to verify the server is responsive: ```csharp -await client.PingAsync(cancellationToken); +await client.PingAsync(cancellationToken: cancellationToken); ``` ### Automatic ping handling diff --git a/docs/concepts/resources/resources.md b/docs/concepts/resources/resources.md index 02d8b6677..628ecaefb 100644 --- a/docs/concepts/resources/resources.md +++ b/docs/concepts/resources/resources.md @@ -215,7 +215,7 @@ builder.Services.AddMcpServer() if (ctx.Params?.Uri is { } uri) { // Track the subscription (e.g., in a concurrent dictionary) - subscriptions[context.Server.SessionId].TryAdd(uri, 0); + subscriptions[ctx.Server.SessionId].TryAdd(uri, 0); } return new EmptyResult(); }) diff --git a/docs/concepts/sampling/sampling.md b/docs/concepts/sampling/sampling.md index 26e60edcc..6ff7ec6fa 100644 --- a/docs/concepts/sampling/sampling.md +++ b/docs/concepts/sampling/sampling.md @@ -100,11 +100,8 @@ McpClientOptions options = new() SamplingHandler = async (request, progress, cancellationToken) => { // Forward to your LLM, apply content filtering, etc. - string prompt = request?.Messages?.LastOrDefault()?.Content switch - { - TextContentBlock text => text.Text, - _ => string.Empty - }; + string prompt = request?.Messages?.LastOrDefault()?.Content + .OfType().FirstOrDefault()?.Text ?? string.Empty; return new CreateMessageResult { From 84c331d9527466f8fd66f9c5fdf7d2961e5b618f Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 16:04:34 -0800 Subject: [PATCH 11/15] Address feedback with simpler syntax --- docs/concepts/elicitation/elicitation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/elicitation/elicitation.md b/docs/concepts/elicitation/elicitation.md index 490084c04..3f7759843 100644 --- a/docs/concepts/elicitation/elicitation.md +++ b/docs/concepts/elicitation/elicitation.md @@ -100,7 +100,7 @@ Enum schemas allow the server to present a set of choices to the user. ["tags"] = new ElicitRequestParams.UntitledMultiSelectEnumSchema { Description = "Tags to apply", - Items = new ElicitRequestParams.UntitledEnumItemsSchema + Items = new() { Enum = ["bug", "feature", "docs", "test"] }, From a72baf942a06c8ab4874baada8c444fb7f9883a3 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 16:09:51 -0800 Subject: [PATCH 12/15] Remove duplicated sample code Co-authored-by: Stephen Halter --- docs/concepts/transports/transports.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/concepts/transports/transports.md b/docs/concepts/transports/transports.md index 1aa4cde69..d22c0f861 100644 --- a/docs/concepts/transports/transports.md +++ b/docs/concepts/transports/transports.md @@ -159,18 +159,6 @@ SSE-specific configuration options: The ASP.NET Core integration supports SSE transport alongside Streamable HTTP. The same `MapMcp()` endpoint handles both protocols — clients connecting with SSE are automatically served using the legacy SSE mechanism: -```csharp -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddMcpServer() - .WithHttpTransport() - .WithTools(); - -var app = builder.Build(); -app.MapMcp(); // Handles both Streamable HTTP and SSE clients -app.Run(); -``` - No additional configuration is needed. When a client connects using the SSE protocol, the server responds with an SSE stream for server-to-client messages and accepts client-to-server messages via a separate POST endpoint. ### Transport mode comparison From 4cdb9a25cdd1d4abaa43f08234612857e26dc0c3 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 16:10:25 -0800 Subject: [PATCH 13/15] Illustrate using ResumeSessionAsync --- docs/concepts/transports/transports.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/concepts/transports/transports.md b/docs/concepts/transports/transports.md index d22c0f861..6bf13021e 100644 --- a/docs/concepts/transports/transports.md +++ b/docs/concepts/transports/transports.md @@ -95,7 +95,7 @@ var transport = new HttpClientTransport(new HttpClientTransportOptions #### Resuming sessions -Streamable HTTP supports session resumption. Save the session ID and reuse it to reconnect: +Streamable HTTP supports session resumption. Save the session ID, server capabilities, and server info from the original session, then use to reconnect: ```csharp var transport = new HttpClientTransport(new HttpClientTransportOptions @@ -103,6 +103,12 @@ var transport = new HttpClientTransport(new HttpClientTransportOptions Endpoint = new Uri("https://my-mcp-server.example.com/mcp"), KnownSessionId = previousSessionId }); + +await using var client = await McpClient.ResumeSessionAsync(transport, new ResumeClientSessionOptions +{ + ServerCapabilities = previousServerCapabilities, + ServerInfo = previousServerInfo +}); ``` #### Streamable HTTP server (ASP.NET Core) From aa0be802e7420d96329fa4b7bf88c057767f83f8 Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 16:15:19 -0800 Subject: [PATCH 14/15] Add content and example for http/sse endpoints and routes --- docs/concepts/transports/transports.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/concepts/transports/transports.md b/docs/concepts/transports/transports.md index 6bf13021e..24c988a47 100644 --- a/docs/concepts/transports/transports.md +++ b/docs/concepts/transports/transports.md @@ -113,7 +113,7 @@ await using var client = await McpClient.ResumeSessionAsync(transport, new Resum #### Streamable HTTP server (ASP.NET Core) -Use the `ModelContextProtocol.AspNetCore` package to host an MCP server over HTTP: +Use the `ModelContextProtocol.AspNetCore` package to host an MCP server over HTTP. The method maps the Streamable HTTP endpoint at the specified route (root by default). It also maps legacy SSE endpoints at `{route}/sse` and `{route}/message` for backward compatibility. ```csharp var builder = WebApplication.CreateBuilder(args); @@ -127,6 +127,16 @@ app.MapMcp(); app.Run(); ``` +A custom route can be specified. For example, the [AspNetCoreMcpPerSessionTools] sample uses a route parameter: + +[AspNetCoreMcpPerSessionTools]: https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/AspNetCoreMcpPerSessionTools + +```csharp +app.MapMcp("/mcp"); +``` + +When using a custom route, Streamable HTTP clients should connect directly to that route (e.g., `https://host/mcp`), while SSE clients should connect to `{route}/sse` (e.g., `https://host/mcp/sse`). + See the `ModelContextProtocol.AspNetCore` package [README](https://github.com/modelcontextprotocol/csharp-sdk/blob/main/src/ModelContextProtocol.AspNetCore/README.md) for more configuration options. ### SSE transport (legacy) From 0b6f1bdb937dec2e190e956e8ec7b98caf25518a Mon Sep 17 00:00:00 2001 From: Jeff Handley Date: Tue, 24 Feb 2026 16:31:17 -0800 Subject: [PATCH 15/15] Fix subscription removal and broken xref. --- docs/concepts/resources/resources.md | 2 +- docs/concepts/transports/transports.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/concepts/resources/resources.md b/docs/concepts/resources/resources.md index 628ecaefb..5d0268000 100644 --- a/docs/concepts/resources/resources.md +++ b/docs/concepts/resources/resources.md @@ -223,7 +223,7 @@ builder.Services.AddMcpServer() { if (ctx.Params?.Uri is { } uri) { - subscriptions.TryRemove(uri, out _); + subscriptions[ctx.Server.SessionId].TryRemove(uri, out _); } return new EmptyResult(); }); diff --git a/docs/concepts/transports/transports.md b/docs/concepts/transports/transports.md index 24c988a47..976df29d5 100644 --- a/docs/concepts/transports/transports.md +++ b/docs/concepts/transports/transports.md @@ -113,7 +113,7 @@ await using var client = await McpClient.ResumeSessionAsync(transport, new Resum #### Streamable HTTP server (ASP.NET Core) -Use the `ModelContextProtocol.AspNetCore` package to host an MCP server over HTTP. The method maps the Streamable HTTP endpoint at the specified route (root by default). It also maps legacy SSE endpoints at `{route}/sse` and `{route}/message` for backward compatibility. +Use the `ModelContextProtocol.AspNetCore` package to host an MCP server over HTTP. The method maps the Streamable HTTP endpoint at the specified route (root by default). It also maps legacy SSE endpoints at `{route}/sse` and `{route}/message` for backward compatibility. ```csharp var builder = WebApplication.CreateBuilder(args);