diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index eba0c0b2a..e16e20331 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,9 +1,24 @@ name: Publish Docs +# Publishes the versioned docs site to GitHub Pages. +# +# Discovers the versions to publish from the repository's GitHub releases (the +# latest release, by date, for each major >= 1), builds each release tag into +# its own sub-path (e.g. /v1/ and /v2/) with make generate-docs, injects a +# version-picker widget into each page, adds a root redirect to the default +# version, and deploys the combined site. A manual dispatch can also rebuild +# the matching major-version path from a specified branch, tag, or commit. +# Triggers on release publish and manual dispatch. + on: release: types: [published] workflow_dispatch: + inputs: + docs_ref: + description: Branch, tag, or commit whose docs should refresh its matching major version + required: false + type: string # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: @@ -11,11 +26,12 @@ permissions: pages: write id-token: write # Required for actions/deploy-pages -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +# Allow only one concurrent deployment. Cancel any in-progress run when a newer one +# starts: every run rediscovers the current releases and rebuilds the whole site from +# scratch, so a newer run fully supersedes the work of the one it cancels. concurrency: group: "pages" - cancel-in-progress: false + cancel-in-progress: true jobs: publish-docs: @@ -26,8 +42,14 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout docs orchestration uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + # A release event otherwise checks out the released tag. Keep the + # orchestration scripts and picker assets current with main. + ref: main + # Full history + tags so we can add a worktree for each version's release tag. + fetch-depth: 0 - name: .NET Setup uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 @@ -36,13 +58,97 @@ jobs: 10.0.x 9.0.x - - name: Generate documentation - run: make generate-docs + - name: Discover versions from GitHub releases + shell: bash + env: + GH_TOKEN: ${{ github.token }} + # For every major version >= 1, take the most recently published (by date) + # non-draft release tagged v{major}.*. The vN prefix becomes the URL slug; + # the site root redirects to the newest release, including prereleases. + run: | + set -euo pipefail + gh release list --repo "${{ github.repository }}" --limit 200 \ + --json tagName,isPrerelease,isDraft,publishedAt \ + --jq ' + [ .[] + | select((.isDraft | not) and (.tagName | test("^v[1-9][0-9]*\\."))) + | { slug: (.tagName | match("^v[0-9]+").string), + ref: .tagName, label: .tagName, + prerelease: .isPrerelease, published: .publishedAt } + ] + | group_by(.slug) + | map(max_by(.published)) + | sort_by(.published) | reverse + | { default: .[0].slug, + versions: map({ slug, ref, label, prerelease }) } + ' | tee "$RUNNER_TEMP/docs-versions.json" + + - name: Build all versions + shell: bash + env: + CONTENT_REF: ${{ inputs.docs_ref }} + run: | + set -euo pipefail + ROOT="$PWD" + COMBINED="$ROOT/combined" + rm -rf "$COMBINED" + mkdir -p "$COMBINED" + + # Orchestration (discovered docs-versions.json, scripts, picker assets) + # always comes from THIS branch. Normally each version's HTML is produced + # by its release tag. A manual docs_ref replaces the matching major's + # HTML with content built from that ref, allowing content-only refreshes + # without minting a product release. + git fetch --tags --force origin + CONTENT_REF="${CONTENT_REF:-}" + CONTENT_WORKTREE="" + CONTENT_SLUG="" + if [[ -n "$CONTENT_REF" ]]; then + CONTENT_WORKTREE="$ROOT/../work-content" + git worktree add --force --detach "$CONTENT_WORKTREE" "$CONTENT_REF" + CONTENT_SLUG="$(node "$ROOT/scripts/get-docs-version-slug.mjs" "$CONTENT_WORKTREE/src/Directory.Build.props")" + if ! node "$ROOT/scripts/list-versions.mjs" | cut -f1 | grep -Fxq "$CONTENT_SLUG"; then + echo "::error::The docs ref '$CONTENT_REF' has major '$CONTENT_SLUG', which has no published release." + exit 1 + fi + echo "Refreshing $CONTENT_SLUG docs from $CONTENT_REF" + fi + + while IFS=$'\t' read -r slug ref; do + if [[ "$slug" == "$CONTENT_SLUG" ]]; then + echo "::group::Build $slug (from ref $CONTENT_REF)" + wt="$CONTENT_WORKTREE" + else + echo "::group::Build $slug (from tag $ref)" + wt="$ROOT/../work-$slug" + git worktree add --force --detach "$wt" "refs/tags/$ref" + fi + + make -C "$wt" generate-docs + + mkdir -p "$COMBINED/$slug" + cp -a "$wt/artifacts/_site/." "$COMBINED/$slug/" + node "$ROOT/scripts/inject-version-picker.mjs" "$COMBINED/$slug" "$slug" --base / + + if [[ "$wt" != "$CONTENT_WORKTREE" ]]; then + git worktree remove --force "$wt" + fi + echo "::endgroup::" + done < <(node "$ROOT/scripts/list-versions.mjs") + + if [[ -n "$CONTENT_WORKTREE" ]]; then + git worktree remove --force "$CONTENT_WORKTREE" + fi + + node "$ROOT/scripts/finalize-docs-site.mjs" "$COMBINED" + + echo "Combined site contents:" + ls -la "$COMBINED" - name: Upload Pages artifact uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: - path: 'artifacts/_site' + path: 'combined' - name: Deploy to GitHub Pages id: deployment diff --git a/docs/concepts/elicitation/elicitation.md b/docs/concepts/elicitation/elicitation.md index 88566284c..841c8a664 100644 --- a/docs/concepts/elicitation/elicitation.md +++ b/docs/concepts/elicitation/elicitation.md @@ -14,13 +14,13 @@ The protocol supports two modes of elicitation: - **Form (In-Band)**: The server requests structured data (strings, numbers, Booleans, enums) which the client collects via a form interface and returns to the server. - **URL Mode**: The server provides a URL for the user to visit (for example, for OAuth, payments, or sensitive data entry). The interaction happens outside the MCP client. -### Server Support for Elicitation +### Server support for elicitation Servers request information from users with the extension method on . The C# SDK registers an instance of with the dependency injection container, so tools can simply add a parameter of type to their method signature to access it. -#### Form Mode Elicitation (In-Band) +#### Form mode elicitation (in-band) For form-based elicitation, the MCP Server must specify the schema of each input value it's requesting from the user. Primitive types (string, number, Boolean) and enum types are supported for elicitation requests. @@ -115,7 +115,7 @@ The following example demonstrates how a server could request a Boolean response [!code-csharp[](samples/server/Tools/InteractiveTools.cs?name=snippet_GuessTheNumber)] -#### URL Mode Elicitation (Out-of-Band) +#### URL mode elicitation (out-of-band) For URL mode elicitation, the server provides a URL that the user must visit to complete an action. This is useful for scenarios like OAuth flows, payment processing, or collecting sensitive credentials that should not be exposed to the MCP client. @@ -134,7 +134,7 @@ var result = await server.ElicitAsync( cancellationToken); ``` -### Client Support for Elicitation +### Client support for elicitation Clients declare their support for elicitation in their capabilities as part of the `initialize` request. Clients can support `Form` (in-band), `Url` (out-of-band), or both. @@ -170,7 +170,7 @@ Here's an example implementation of how a console application might handle elici [!code-csharp[](samples/client/Program.cs?name=snippet_ElicitationHandler)] -### Multi Round-Trip Requests (MRTR) +### Multi round-trip requests (MRTR) [MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `2026-07-28`. In that revision, the server-to-client `elicitation/create` request method is removed; the recommended way to ask the user for input from a server handler is to throw and let the SDK emit an on the wire. @@ -196,7 +196,7 @@ public static string ElicitWithMrtr( if (!server.IsMrtrSupported) { - return "This tool requires MRTR support (2026-07-28, or a stateful current-protocol session)."; + return "This tool requires MRTR support (2026-07-28, or a stateful session using protocol revision 2025-11-25)."; } // First call — request user input @@ -225,11 +225,11 @@ public static string ElicitWithMrtr( > [!TIP] > For the full protocol details, including multiple round trips, concurrent input requests, and the compatibility matrix, see [Multi Round-Trip Requests (MRTR)](xref:mrtr). -### URL Elicitation Required Error +### URL elicitation required error When a tool cannot proceed without first completing a URL-mode elicitation (for example, when third-party OAuth authorization is needed), and calling `ElicitAsync` is not practical (for example, in [stateless](xref:stateless) mode where server-to-client requests are disabled), the server might throw a . This is a specialized error (JSON-RPC error code `-32042`) that signals to the client that one or more URL-mode elicitations must be completed before the original request can be retried. -#### Throwing UrlElicitationRequiredException on the Server +#### Throwing `UrlElicitationRequiredException` on the server A server tool can throw `UrlElicitationRequiredException` when it detects that authorization or other out-of-band interaction is required: @@ -267,7 +267,7 @@ public async Task AccessThirdPartyResource(McpServer server, Cancellatio The exception can include multiple elicitations if the operation requires authorization from multiple services. -#### Catching UrlElicitationRequiredException on the Client +#### Catching `UrlElicitationRequiredException` on the client When the client calls a tool and receives a `UrlElicitationRequiredException`, it should: @@ -317,7 +317,7 @@ catch (UrlElicitationRequiredException ex) } ``` -#### Listening for Elicitation Completion Notifications +#### Listening for elicitation completion notifications Servers can optionally send a `notifications/elicitation/complete` notification when the out-of-band interaction is complete. Clients can register a handler to receive these notifications: diff --git a/docs/concepts/filters.md b/docs/concepts/filters.md index 043e17b06..aac23ea2a 100644 --- a/docs/concepts/filters.md +++ b/docs/concepts/filters.md @@ -14,7 +14,7 @@ The MCP Server provides two levels of filters for intercepting and modifying req The filters are stored in `McpServerOptions.Filters`. -## Available Request-Specific Filter Methods +## Available request-specific filter methods The following request filter methods are available on `IMcpRequestFilterBuilder` inside `WithRequestFilters(...)`: @@ -32,23 +32,13 @@ The following request filter methods are available on `IMcpRequestFilterBuilder` | `AddUnsubscribeFromResourcesFilter` | Resource unsubscription handlers | | `AddSetLoggingLevelFilter` | Logging level handlers | -The Tasks extension and ASP.NET Core tool authorization use alternate-result `tools/call` filters. -Alternate-result filters run in registration order outside the ordinary `AddCallToolFilter` -pipeline. The Tasks filter creates a task at its position in that order and runs its remaining -pipeline in the background. Consequently, alternate-result filters registered before Tasks run -before task creation, while those registered after Tasks run in the background before the -ordinary filters and tool. ASP.NET Core authorization is registered before Tasks, so an -unauthorized call is rejected without creating a task. +The Tasks extension and ASP.NET Core tool authorization use alternate-result `tools/call` filters. Alternate-result filters run in registration order outside the ordinary `AddCallToolFilter` pipeline. The Tasks filter creates a task at its position in that order and runs its remaining pipeline in the background. Consequently, alternate-result filters registered before Tasks run before task creation, while those registered after Tasks run in the background before the ordinary filters and tool. ASP.NET Core authorization is registered before Tasks, so an unauthorized call is rejected without creating a task. -Ordinary call-tool filters run exactly once after all alternate-result filters and before the tool. -For task-backed calls, ordinary filters execute in the background after task creation. An explicit -`CallToolWithAlternateHandler` remains a full replacement and cannot be combined with ordinary -call-tool filters. +Ordinary call-tool filters run exactly once after all alternate-result filters and before the tool. For task-backed calls, ordinary filters execute in the background after task creation. An explicit `CallToolWithAlternateHandler` remains a full replacement and cannot be combined with ordinary call-tool filters. -Configure `WithTasks` before adding ordinary call-tool filters. Tasks validates this ordering when -its filter is installed because it must execute outside the ordinary call-tool pipeline. +Configure `WithTasks` before adding ordinary call-tool filters. Tasks validates this ordering when its filter is installed because it must execute outside the ordinary call-tool pipeline. -## Message Filters +## Message filters In addition to the request-specific filters above, there are low-level message filters that intercept all JSON-RPC messages before they are routed to specific handlers. Configure these on `IMcpMessageFilterBuilder` inside `WithMessageFilters(...)`: @@ -56,7 +46,7 @@ Configure these on `IMcpMessageFilterBuilder` inside `WithMessageFilters(...)`: - `AddIncomingFilter` - Filter for all incoming JSON-RPC messages (requests and notifications) - `AddOutgoingFilter` - Filter for all outgoing JSON-RPC messages (responses and notifications) -### When to Use Message Filters +### When to use message filters Message filters operate at a lower level than request-specific filters and are useful when you need to: @@ -66,7 +56,7 @@ Message filters operate at a lower level than request-specific filters and are u - Modify or skip messages before they reach handlers - Send additional messages in response to specific events -### Incoming Message Filter +### Incoming message filter `AddIncomingFilter` intercepts all incoming JSON-RPC messages before they are dispatched to request-specific handlers: @@ -100,7 +90,7 @@ Inside an incoming message filter, you have access to: - `context.Services` - The request's service provider - `context.Items` - A dictionary for passing data between filters -#### Skipping Default Handlers +#### Skipping default handlers You can skip the default handler by not calling `next`. This is useful for implementing custom protocol methods: @@ -126,7 +116,7 @@ You can skip the default handler by not calling `next`. This is useful for imple }) ``` -### Outgoing Message Filter +### Outgoing message filter `AddOutgoingFilter` intercepts all outgoing JSON-RPC messages before they are sent to the client: @@ -155,7 +145,7 @@ services.AddMcpServer() .WithTools(); ``` -#### Skipping Outgoing Messages +#### Skipping outgoing messages You can suppress outgoing messages by not calling `next`: @@ -176,7 +166,7 @@ You can suppress outgoing messages by not calling `next`: }) ``` -#### Sending Additional Messages +#### Sending additional messages Outgoing message filters can send additional messages by calling `next` with a new `MessageContext`: @@ -207,7 +197,7 @@ Outgoing message filters can send additional messages by calling `next` with a n }) ``` -### Message Filter Execution Order +### Message filter execution order Message filters execute in registration order, with the first registered filter being the outermost: @@ -253,7 +243,7 @@ OutgoingFilter2 (after next) OutgoingFilter1 (after next) ``` -### Passing Data Between Filters +### Passing data between filters The `Items` dictionary allows you to pass data between filters processing the same message: @@ -309,7 +299,7 @@ services.AddMcpServer() }); ``` -## Filter Execution Order +## Filter execution order ```csharp services.AddMcpServer() @@ -324,7 +314,7 @@ services.AddMcpServer() Execution flow: `filter1 -> filter2 -> filter3 -> baseHandler -> filter3 -> filter2 -> filter1` -## Common Use Cases +## Common use cases Filters are commonly used for [logging](#logging), [error handling](#error-handling), [performance monitoring](#performance-monitoring), and [caching](#caching). @@ -345,7 +335,7 @@ Filters are commonly used for [logging](#logging), [error handling](#error-handl }); ``` -### Error Handling +### Error handling ```csharp .WithRequestFilters(requestFilters => @@ -371,7 +361,7 @@ Filters are commonly used for [logging](#logging), [error handling](#error-handl }); ``` -### Performance Monitoring +### Performance monitoring ```csharp .WithRequestFilters(requestFilters => @@ -411,11 +401,11 @@ Filters are commonly used for [logging](#logging), [error handling](#error-handl }); ``` -## Built-in Authorization Request Filters +## Built-in authorization request filters When using the ASP.NET Core integration (`ModelContextProtocol.AspNetCore`), you can add authorization filters to support `[Authorize]` and `[AllowAnonymous]` attributes on MCP server tools, prompts, and resources by calling `AddAuthorizationFilters()` on your MCP server builder. -### Enabling Authorization Request Filters +### Enabling authorization request filters To enable authorization support, call `AddAuthorizationFilters()` when configuring your MCP server: @@ -428,7 +418,7 @@ services.AddMcpServer() **Important**: If you want to use authorization attributes like `[Authorize]` on your MCP server tools, prompts, or resources, you should always call `AddAuthorizationFilters()` when using ASP.NET Core integration. -### Authorization Attributes Support +### Authorization attributes support The MCP server automatically respects the following authorization attributes: @@ -437,7 +427,7 @@ The MCP server automatically respects the following authorization attributes: - **`[Authorize(Policy = "PolicyName")]`** - Requires specific authorization policies - **`[AllowAnonymous]`** - Explicitly allows anonymous access (overrides `[Authorize]`) -### Tool Authorization +### Tool authorization Tools can be decorated with authorization attributes to control access: @@ -467,7 +457,7 @@ public class WeatherTools } ``` -### Class-Level Authorization +### Class-level authorization You can apply authorization at the class level, which affects all tools in the class: @@ -491,19 +481,19 @@ public class RestrictedTools } ``` -### How Authorization Filters Work +### How authorization filters work The authorization filters work differently for list operations versus individual operations: -#### List Operations (ListTools, ListPrompts, ListResources) +#### List operations (`ListTools`, `ListPrompts`, `ListResources`) For list operations, the filters automatically remove unauthorized items from the results. Users only see tools, prompts, or resources they have permission to access. -#### Individual Operations (CallTool, GetPrompt, ReadResource) +#### Individual operations (`CallTool`, `GetPrompt`, `ReadResource`) For individual operations, the filters throw an `McpException` with "Access forbidden" message. These get turned into JSON-RPC errors if uncaught by middleware. -### Filter Execution Order and Authorization +### Filter execution order and authorization Authorization filters are applied automatically when you call `AddAuthorizationFilters()`. These filters run at a specific point in the filter pipeline, which means: @@ -551,7 +541,7 @@ services.AddMcpServer() .WithTools(); ``` -### Setup Requirements +### Setup requirements To use authorization features, you must configure authentication and authorization in your ASP.NET Core application and call `AddAuthorizationFilters()`: @@ -585,7 +575,7 @@ app.MapMcp(); app.Run(); ``` -### Custom Authorization Filters +### Custom authorization filters You can also create custom authorization filters using the filter methods: diff --git a/docs/concepts/getting-started.md b/docs/concepts/getting-started.md index dae220153..5ff681603 100644 --- a/docs/concepts/getting-started.md +++ b/docs/concepts/getting-started.md @@ -5,7 +5,7 @@ description: Install the MCP C# SDK and build your first MCP client and server. uid: getting-started --- -## Getting Started +## Getting started This guide walks you through installing the MCP C# SDK and building a minimal MCP client and server. diff --git a/docs/concepts/identity/identity.md b/docs/concepts/identity/identity.md index 5c21adbd6..313523b1f 100644 --- a/docs/concepts/identity/identity.md +++ b/docs/concepts/identity/identity.md @@ -9,7 +9,7 @@ uid: identity When building production MCP servers, you often need to know _who_ is calling a tool so you can enforce permissions, filter data, or audit access. The MCP C# SDK provides built-in support for propagating the caller's identity from the transport layer into your tool, prompt, and resource handlers — no custom headers or workarounds required. -## How Identity Flows Through the SDK +## How identity flows through the SDK When a client sends a request over an authenticated HTTP transport (Streamable HTTP or SSE), the ASP.NET Core authentication middleware populates `HttpContext.User` with a `ClaimsPrincipal`. The SDK's transport layer automatically copies this `ClaimsPrincipal` into `JsonRpcMessage.Context.User`, which then flows through message filters, request filters, and finally into the handler or tool method. @@ -24,7 +24,7 @@ HTTP Request (with auth token) This means you can access the authenticated user's identity at every stage of request processing. -## Direct `ClaimsPrincipal` Parameter Injection (Recommended) +## Direct `ClaimsPrincipal` parameter injection (recommended) The simplest and recommended approach is to declare a `ClaimsPrincipal` parameter on your tool method. The SDK automatically injects the authenticated user without including it in the tool's input schema: @@ -56,7 +56,7 @@ public class UserAwarePrompts } ``` -### Why This Works +### Why this works The SDK registers `ClaimsPrincipal` as one of the built-in services available during request processing. When a tool, prompt, or resource method declares a `ClaimsPrincipal` parameter, the SDK: @@ -66,7 +66,7 @@ The SDK registers `ClaimsPrincipal` as one of the built-in services available du This behavior is transport-agnostic. For HTTP transports, the `ClaimsPrincipal` comes from ASP.NET Core authentication. For other transports (like stdio), it's `null` unless you set it explicitly via a message filter. -## Accessing Identity in Filters +## Accessing identity in filters Both message filters and request-specific filters expose the user via `context.User`: @@ -87,7 +87,7 @@ services.AddMcpServer() .WithTools(); ``` -## Role-Based Access with `[Authorize]` Attributes +## Role-based access with `[Authorize]` attributes For declarative authorization, you can use standard ASP.NET Core `[Authorize]` attributes on your tools, prompts, and resources. This requires calling `AddAuthorizationFilters()` during server configuration: @@ -134,7 +134,7 @@ When authorization fails, the SDK automatically: For more details on authorization filters and their execution order, see [Filters](xref:filters). -## Using `IHttpContextAccessor` (HTTP-Only Alternative) +## Using `IHttpContextAccessor` (HTTP-only alternative) If you need access to the full `HttpContext` (not just the user), you can inject `IHttpContextAccessor` into your tool class. This gives you access to HTTP headers, query strings, and other request metadata: @@ -158,7 +158,7 @@ public class HttpContextTools(IHttpContextAccessor contextAccessor) For more details, including important caveats about stale `HttpContext` with the legacy SSE transport, see [HTTP Context](xref:httpcontext). -## Transport Considerations +## Transport considerations | Transport | Identity Source | Notes | | --- | --- | --- | @@ -166,7 +166,7 @@ For more details, including important caveats about stale `HttpContext` with the | SSE | Same as Streamable HTTP, but the `HttpContext` is tied to the long-lived SSE connection. | The `ClaimsPrincipal` parameter injection still works correctly, but `IHttpContextAccessor` may return stale claims if the client's token was refreshed after the SSE connection was established. | | Stdio | No built-in authentication. `ClaimsPrincipal` is `null` unless set via a message filter. | For process-level identity, you can set the user in a message filter based on environment variables or other process-level context. | -### Setting Identity for Stdio Transport +### Setting identity for stdio transport For stdio-based servers where the caller's identity comes from the process environment rather than HTTP authentication, you can set the user in a message filter: @@ -188,7 +188,7 @@ services.AddMcpServer() .WithTools(); ``` -## Full Example: Protected HTTP Server +## Full example: protected HTTP server For a complete example of an MCP server with JWT authentication, OAuth resource metadata, and protected tools, see the [ProtectedMcpServer sample](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/ProtectedMcpServer). diff --git a/docs/concepts/index.md b/docs/concepts/index.md index 26b270a0f..5817426af 100644 --- a/docs/concepts/index.md +++ b/docs/concepts/index.md @@ -4,7 +4,7 @@ Welcome to the conceptual documentation for the Model Context Protocol SDK. Here ## Contents -### Getting Started +### Getting started To install the SDK and build your first MCP client and server, see [Getting started](getting-started.md). @@ -14,7 +14,7 @@ To install the SDK and build your first MCP client and server, see [Getting star | - | - | | [Docker deployment](deployment/docker.md) | Learn how to package and run ASP.NET Core MCP servers in Docker containers using Streamable HTTP transport. | -### Base Protocol +### Base protocol | Title | Description | | - | - | @@ -23,10 +23,9 @@ To install the SDK and build your first MCP client and server, see [Getting star | [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. | -| [Tasks](tasks/tasks.md) | Learn how to use task-based execution for long-running operations that can be polled for status and results. | | [Multi Round-Trip Requests (MRTR)](mrtr/mrtr.md) | Learn how servers request client input during tool execution using input-required results and retries. | -### Client Features +### Client features | Title | Description | | - | - | @@ -34,7 +33,7 @@ To install the SDK and build your first MCP client and server, see [Getting star | [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 +### Server features | Title | Description | | - | - | @@ -53,4 +52,5 @@ To install the SDK and build your first MCP client and server, see [Getting star | Title | Description | | - | - | | [MCP Apps](apps/apps.md) | Learn how to use the MCP Apps extension to deliver interactive UIs from MCP servers. | +| [Tasks](tasks/tasks.md) | Learn how to use task-based execution for long-running operations that can be polled for status and results. | | [Identity and Roles](identity/identity.md) | Learn how to access caller identity and roles in MCP tool, prompt, and resource handlers. | diff --git a/docs/concepts/logging/logging.md b/docs/concepts/logging/logging.md index 1830f2dea..8d705cf88 100644 --- a/docs/concepts/logging/logging.md +++ b/docs/concepts/logging/logging.md @@ -13,7 +13,7 @@ MCP servers can expose log messages to clients through the [Logging utility]. This document describes how to implement logging in MCP servers and how clients can consume log messages. -### Logging Levels +### Logging levels MCP uses the logging levels defined in [RFC 5424](https://datatracker.ietf.org/doc/html/rfc5424). diff --git a/docs/concepts/mrtr/mrtr.md b/docs/concepts/mrtr/mrtr.md index 66f7ae6ab..5a498dac4 100644 --- a/docs/concepts/mrtr/mrtr.md +++ b/docs/concepts/mrtr/mrtr.md @@ -7,10 +7,6 @@ uid: mrtr # Multi Round-Trip Requests (MRTR) - -> [!WARNING] -> MRTR is part of the **`2026-07-28`** revision of the MCP specification ([SEP-2322](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322)). The wire format and API surface might change before the revision is ratified. For details on working with experimental APIs, see [Experimental APIs](../../experimental.md). - Multi Round-Trip Requests (MRTR) let a server tool request input from the client — such as [elicitation](xref:elicitation), [sampling](xref:sampling), or [roots](xref:roots) — as part of a single tool call, without requiring a separate server-to-client JSON-RPC request for each interaction. Instead of returning a final result, the server returns an **incomplete result** containing one or more input requests. The client fulfills those requests and retries the original tool call with the responses attached. ## Overview @@ -47,9 +43,9 @@ var clientOptions = new McpClientOptions }; ``` -Under `2026-07-28`, MRTR is the recommended way to obtain client input from a server handler. The spec removes the legacy server-to-client `elicitation/create`, `sampling/createMessage`, and `roots/list` request methods, so any code that needs to work on a `2026-07-28` Streamable HTTP server (where Streamable HTTP no longer supports sessions) must use `InputRequiredException` rather than , , or . The legacy methods still work on stateful sessions — that's how stdio servers keep working on `2026-07-28` today — but they throw `InvalidOperationException("X is not supported in stateless mode.")` on any stateless session, current or `2026-07-28`. +Under `2026-07-28`, MRTR is the recommended way to obtain client input from a server handler. The spec removes the legacy server-to-client `elicitation/create`, `sampling/createMessage`, and `roots/list` request methods, so any code that needs to work on a `2026-07-28` Streamable HTTP server (where Streamable HTTP no longer supports sessions) must use `InputRequiredException` rather than , , or . The legacy methods still work on stateful sessions — including `2026-07-28` stdio sessions — but they throw `InvalidOperationException("X is not supported in stateless mode.")` on every stateless session. -Under the current protocol revision (`2025-06-18` and earlier), `InputRequiredException` is still supported in stateful sessions via a backward-compatibility resolver — see the [Compatibility](#compatibility) section. +Under `2025-11-25` and earlier, `InputRequiredException` is still supported in stateful sessions via a backward-compatibility resolver — see the [Compatibility](#compatibility) section. ## Authoring an MRTR tool @@ -60,7 +56,7 @@ A tool participates in MRTR by throwing before throwing `InputRequiredException`. The property returns `true` when either: - The negotiated protocol revision is `2026-07-28` (MRTR is native), or -- The session is stateful under the current protocol (the SDK can resolve input requests via legacy JSON-RPC and retry the handler). +- The session is stateful under protocol revision `2025-11-25` (the SDK can resolve input requests via legacy JSON-RPC and retry the handler). ```csharp [McpServerTool, Description("A tool that uses MRTR")] @@ -71,7 +67,7 @@ public static string MyTool( if (!server.IsMrtrSupported) { return "This tool requires a client that negotiates 2026-07-28, " - + "or a stateful current-protocol session."; + + "or a stateful session using protocol revision 2025-11-25."; } // ... MRTR logic @@ -260,8 +256,8 @@ if (!server.IsMrtrSupported) { return "This tool requires interactive input. To use it:\n" + "1. Connect with a client that negotiates MCP protocol revision 2026-07-28, or\n" - + "2. Use a stateful current-protocol session so the server can resolve the input requests for you.\n" - + "\nStateless current-protocol sessions cannot resolve MRTR input requests."; + + "2. Use a stateful session using protocol revision 2025-11-25 so the server can resolve the input requests for you.\n" + + "\nStateless sessions using protocol revision 2025-11-25 and earlier cannot resolve MRTR input requests."; } ``` @@ -269,12 +265,12 @@ if (!server.IsMrtrSupported) The SDK supports `InputRequiredException` across two protocol revisions and two session modes: -| Negotiated protocol | Session mode | Behavior | -|---------------------|--------------|-------------------------------------------------------------------------| -| `2026-07-28` | Stateful | Native MRTR — `InputRequiredResult` is serialized directly to the wire. | -| `2026-07-28` | Stateless | Native MRTR — `InputRequiredResult` is serialized directly to the wire. No server-side handler state needed. | -| Current (`2025-06-18` and earlier) | Stateful | Backward-compatibility resolver — the SDK sends standard `elicitation/create` / `sampling/createMessage` / `roots/list` JSON-RPC requests to the client, collects the responses, and retries the handler with `inputResponses` populated. Up to 10 retry rounds. | -| Current (`2025-06-18` and earlier) | Stateless | **Not supported** — `InputRequiredException` raises an `McpException`. The client doesn't speak MRTR, and the server can't resolve input requests via JSON-RPC without a persistent session. | +| Negotiated protocol | Session mode | Behavior | +|----------------------------------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| `2026-07-28` | Stateful | Native MRTR — `InputRequiredResult` is serialized directly to the wire. | +| `2026-07-28` | Stateless | Native MRTR — `InputRequiredResult` is serialized directly to the wire. No server-side handler state needed. | +| `2025-11-25` and earlier | Stateful | Backward-compatibility resolver — the SDK sends standard `elicitation/create` / `sampling/createMessage` / `roots/list` JSON-RPC requests to the client, collects the responses, and retries the handler with `inputResponses` populated. Up to 10 retry rounds. | +| `2025-11-25` and earlier | Stateless | **Not supported** — `InputRequiredException` raises an `McpException`. The client doesn't speak MRTR, and the server can't resolve input requests via JSON-RPC without a persistent session. | > [!NOTE] > The backcompat resolver is intentionally limited to 10 retry rounds. Tools that need more rounds should require `2026-07-28` (check `IsMrtrSupported`). @@ -283,6 +279,6 @@ The SDK supports `InputRequiredException` across two protocol revisions and two `ElicitAsync` / `SampleAsync` / `RequestRootsAsync` issue a JSON-RPC request to the client and wait for the response on the same session. Stateless servers don't have a persistent session to wait on, so the SDK fails fast with `InvalidOperationException("X is not supported in stateless mode.")` (the check is `McpServer.ClientCapabilities is null`, which is the SDK's proxy for stateless). -Under the current protocol revision (`2025-06-18` and earlier), stdio and stateful Streamable HTTP keep `ClientCapabilities` populated, so the legacy methods work normally and remain the recommended way to do one-shot client interactions. Under `2026-07-28`, the spec removes those request methods from Streamable HTTP entirely; the SDK still allows the legacy methods on `2026-07-28` stdio sessions because stdio is implicitly single-process / stateful and the client handler is wired up regardless of negotiated revision. `InputRequiredException` is the way to write tools that work on every supported configuration. +Under `2025-11-25` and earlier, stdio and stateful Streamable HTTP keep `ClientCapabilities` populated, so the legacy methods work normally and remain the recommended way to do one-shot client interactions. Under `2026-07-28`, the spec removes those request methods from Streamable HTTP entirely; the SDK still allows the legacy methods on `2026-07-28` stdio sessions because stdio is implicitly single-process / stateful and the client handler is wired up regardless of negotiated revision. `InputRequiredException` is the way to write tools that work on every supported configuration. Because `2026-07-28` removes `Mcp-Session-Id` (SEP-2567) and the `initialize` handshake (SEP-2575), Streamable HTTP can serve that revision only through the stateless path. The `Stateful` row for `2026-07-28` in the compatibility matrix above therefore applies to stdio and other non-HTTP stateful sessions; an HTTP server explicitly set to `Stateless = false` refuses `2026-07-28` with `UnsupportedProtocolVersion` and creates a session only when an older client falls back to `initialize`. diff --git a/docs/concepts/progress/progress.md b/docs/concepts/progress/progress.md index 272317d2b..b46bbdbd5 100644 --- a/docs/concepts/progress/progress.md +++ b/docs/concepts/progress/progress.md @@ -18,7 +18,7 @@ This project illustrates the common case of a server tool that performs a long-r > [!NOTE] > Progress notifications are sent inline as part of the response to a request — they are not unsolicited. Progress tracking works in both [stateless and stateful](xref:stateless) modes as well as stdio. -### Server Implementation +### Server implementation When processing a request, the server can use the extension method of to send progress updates, specifying `"notifications/progress"` as the notification method name. @@ -30,7 +30,7 @@ The server must verify that the caller provided a `progressToken` in the request [!code-csharp[](samples/server/Tools/LongRunningTools.cs?name=snippet_SendProgress)] -### Client Implementation +### Client implementation Clients request progress updates by including a `progressToken` in the parameters of a request. Note that servers aren't required to support progress tracking, so clients should not depend on receiving progress updates. diff --git a/docs/concepts/roots/roots.md b/docs/concepts/roots/roots.md index 894e1450f..ec26d435d 100644 --- a/docs/concepts/roots/roots.md +++ b/docs/concepts/roots/roots.md @@ -104,7 +104,7 @@ server.RegisterNotificationHandler( }); ``` -### Multi Round-Trip Requests (MRTR) +### Multi round-trip requests (MRTR) [MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `2026-07-28`. In that revision, the server-to-client `roots/list` request method is removed; the recommended way to ask the client for its roots from a server handler is to throw and let the SDK emit an on the wire. @@ -128,7 +128,7 @@ public static string ListRootsWithMrtr( if (!server.IsMrtrSupported) { - return "This tool requires MRTR support (2026-07-28, or a stateful current-protocol session)."; + return "This tool requires MRTR support (2026-07-28, or a stateful session using protocol revision 2025-11-25)."; } // First call — request the client's root list diff --git a/docs/concepts/sampling/sampling.md b/docs/concepts/sampling/sampling.md index a768a3c43..bdeeaf910 100644 --- a/docs/concepts/sampling/sampling.md +++ b/docs/concepts/sampling/sampling.md @@ -121,7 +121,7 @@ McpClientOptions options = new() 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 . -### Multi Round-Trip Requests (MRTR) +### Multi round-trip requests (MRTR) [MRTR](xref:mrtr) is the SEP-2322 mechanism for server-driven input requests, finalized in protocol revision `2026-07-28`. In that revision, the server-to-client `sampling/createMessage` request method is removed; the recommended way to ask the client to sample from a server handler is to throw and let the SDK emit an on the wire. @@ -146,7 +146,7 @@ public static string SampleWithMrtr( if (!server.IsMrtrSupported) { - return "This tool requires MRTR support (2026-07-28, or a stateful current-protocol session)."; + return "This tool requires MRTR support (2026-07-28, or a stateful session using protocol revision 2025-11-25)."; } // First call — request LLM completion from the client diff --git a/docs/concepts/stateless/stateless.md b/docs/concepts/stateless/stateless.md index 027fcbf88..899fd1864 100644 --- a/docs/concepts/stateless/stateless.md +++ b/docs/concepts/stateless/stateless.md @@ -28,11 +28,11 @@ When sessions are enabled (`Stateless = false`), the server creates and tracks a ## Forward and backward compatibility -The `Stateless` property is the single most important setting for forward-proofing your MCP server. The default is now `Stateless = true` (sessions disabled), which is the forward-compatible setting for the `2026-07-28` protocol revision and beyond. Stateless servers still respond to clients on `2025-11-25` and earlier — the SDK keeps the `initialize` + `Mcp-Session-Id` handshake available for those clients — but they cannot use the session-dependent features ([unsolicited notifications](#how-streamable-http-delivers-messages), resource subscriptions, per-client isolation). Server-to-client requests are the exception: [elicitation](xref:elicitation) — and the now-deprecated [sampling](xref:sampling) and [roots](xref:roots) — can run statelessly through [MRTR](xref:mrtr), though MRTR requires the unratified `2026-07-28` revision and is far less widely supported than session-based requests. We recommend every server set `Stateless` explicitly rather than relying on the default: +The `Stateless` property is the single most important setting for forward-proofing your MCP server. The default is now `Stateless = true` (sessions disabled), which is the forward-compatible setting for the `2026-07-28` protocol revision and beyond. Stateless servers still respond to clients on `2025-11-25` and earlier — the SDK keeps the `initialize` + `Mcp-Session-Id` handshake available for those clients — but they cannot use the session-dependent features ([unsolicited notifications](#how-streamable-http-delivers-messages), resource subscriptions, per-client isolation). Server-to-client requests are the exception: [elicitation](xref:elicitation) — and the now-deprecated [sampling](xref:sampling) and [roots](xref:roots) — can run statelessly through [MRTR](xref:mrtr) when both peers support `2026-07-28`. We recommend every server set `Stateless` explicitly rather than relying on the default: - **`Stateless = true`** — the current default and the forward-compatible choice. Your server opts out of sessions entirely and the `Mcp-Session-Id` header is never sent or used. The `2026-07-28` protocol revision drops the `initialize` handshake and `Mcp-Session-Id` from the wire format entirely, so this is the only configuration that lets the server respond to `2026-07-28` clients without falling back to initialize-handshake handling. If you don't need [unsolicited notifications](#how-streamable-http-delivers-messages), server-to-client requests, or session-scoped state, this is the setting to use today. -- **`Stateless = false`** — the right choice when your server depends on sessions for [unsolicited notifications](#how-streamable-http-delivers-messages), resource subscriptions, or per-client isolation, none of which work without a session. Setting this explicitly protects your server from a future default change, and the [MCP specification requires](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) that clients use sessions when a server's `initialize` response includes an `Mcp-Session-Id` header, so compliant clients always honor your server's session. Server-to-client requests no longer force a session: [elicitation](xref:elicitation) — and the now-deprecated [sampling](xref:sampling) and [roots](xref:roots) — can run statelessly through [MRTR](xref:mrtr) (see [Stateless alternatives for server-to-client interactions](#stateless-alternatives-for-server-to-client-interactions)). MRTR is only as available as the unratified `2026-07-28` revision, however, so keep a session if you need server-to-client requests against clients that don't speak it. Note that with `Stateless = false`, a `2026-07-28` request is refused with `UnsupportedProtocolVersion`; the stateful path activates only when a client falls back to an initialize-capable revision. +- **`Stateless = false`** — the right choice when your server depends on sessions for [unsolicited notifications](#how-streamable-http-delivers-messages), resource subscriptions, or per-client isolation, none of which work without a session. Setting this explicitly protects your server from a future default change, and the [MCP specification requires](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) that clients use sessions when a server's `initialize` response includes an `Mcp-Session-Id` header, so compliant clients always honor your server's session. Server-to-client requests no longer force a session: [elicitation](xref:elicitation) — and the now-deprecated [sampling](xref:sampling) and [roots](xref:roots) — can run statelessly through [MRTR](xref:mrtr) (see [Stateless alternatives for server-to-client interactions](#stateless-alternatives-for-server-to-client-interactions)). Keep a session if you need server-to-client requests against clients that do not support `2026-07-28`. Note that with `Stateless = false`, a `2026-07-28` request is refused with `UnsupportedProtocolVersion`; the stateful path activates only when a client falls back to an initialize-capable revision. > [!TIP] @@ -141,7 +141,7 @@ Most MCP servers fall into this category. Tools that call APIs, query databases, The traditional approach to server-to-client interactions (elicitation, sampling, roots) requires sessions because the server must hold an open connection to send JSON-RPC requests back to the client. [Multi Round-Trip Requests (MRTR)](xref:mrtr) is a stateless alternative — instead of sending a request, the server returns an **incomplete result** that tells the client what input is needed. The client fulfills the requests and retries the tool call with the responses attached. -This means servers that need user confirmation ([elicitation](xref:elicitation)) — or the now-deprecated (SEP-2577) [sampling](xref:sampling) and [roots](xref:roots) — can run in stateless mode when both sides support MRTR. Because MRTR rides on the unratified `2026-07-28` revision, it is far less broadly supported than session-based requests; keep a session if you must interact with clients that don't speak it. +This means servers that need user confirmation ([elicitation](xref:elicitation)) — or the now-deprecated (SEP-2577) [sampling](xref:sampling) and [roots](xref:roots) — can run in stateless mode when both sides support MRTR. Keep a session if you must interact with clients that do not support `2026-07-28`. ## Stateful mode (sessions) @@ -595,13 +595,13 @@ In stateless mode, each HTTP request creates and disposes a short-lived `McpServ ### Stateless mode -Tasks are a natural fit for stateless servers. The client sends a task-augmented `tools/call` request, receives a task ID immediately, and polls for completion with `tasks/get` or `tasks/result` on subsequent independent HTTP requests. Because each request creates an ephemeral `McpServer` that shares the same `IMcpTaskStore`, all task operations work without any persistent session. +Tasks are a natural fit for stateless servers. The client sends a task-augmented `tools/call` request, receives a task ID immediately, and polls for completion with `tasks/get` on subsequent independent HTTP requests. Because each request creates an ephemeral `McpServer` that shares the same `IMcpTaskStore`, all task operations work without any persistent session. In stateless mode, there is no `SessionId`, so the task store does not apply session-based isolation. All tasks are accessible from any request to the same server. This is typically fine for single-purpose servers or when authentication middleware already identifies the caller. ### Stateful mode -In stateful mode, the `IMcpTaskStore` receives the session's `SessionId` on every operation: `CreateTaskAsync`, `GetTaskAsync`, `ListTasksAsync`, `CancelTaskAsync`, etc. The built-in enforces session isolation: tasks created in one session cannot be accessed from another. +In stateful mode, the `IMcpTaskStore` tracks tasks created for that server instance. The built-in keeps tasks in memory; implement a custom store when your scenario requires durable storage or isolation across server instances. Tasks can outlive individual HTTP requests because the tool executes in the background after returning the initial `CreateTaskResult`. Task cleanup is governed by the task's TTL (time-to-live), not by session termination. However, the `InMemoryMcpTaskStore` loses all tasks if the server process restarts. For durable tasks, implement a custom backed by an external store. For guidance, see [Implementing a custom task store](xref:tasks#implementing-a-custom-task-store). @@ -653,9 +653,9 @@ When a handler calls `EnablePollingAsync()`: The `EventStreamStore` itself has TTL-based limits (default: 2-hour event expiration, 30-minute sliding window) that govern event retention, but these do not limit handler concurrency. If you enable `EventStreamStore` on a public-facing server, apply **HTTP rate-limiting middleware** and **reverse proxy limits** to compensate for the loss of stream-level backpressure. -### With tasks (experimental) +### With tasks -[Tasks](xref:tasks) are an experimental feature that enables a "call-now, fetch-later" pattern for long-running tool calls. When a client sends a task-augmented `tools/call` request, the server creates a task record in the , starts the tool handler as a fire-and-forget background task, and returns the task ID immediately, so the `POST` response completes **before the handler starts its real work**. +[Tasks](xref:tasks) enable a "call-now, fetch-later" pattern for long-running tool calls. When a client sends a task-augmented `tools/call` request, the server creates a task record in the , starts the tool handler as a fire-and-forget background task, and returns the task ID immediately, so the `POST` response completes **before the handler starts its real work**. This means: diff --git a/docs/concepts/tasks/tasks.md b/docs/concepts/tasks/tasks.md index 77d26fef0..1bbcfea1a 100644 --- a/docs/concepts/tasks/tasks.md +++ b/docs/concepts/tasks/tasks.md @@ -10,9 +10,28 @@ Tasks let an MCP server run a request asynchronously and report its result to th primary use case today is long-running tool invocations: the tool is offloaded to a background task, and the client polls for status, optionally exchanging additional input along the way. -> **Status**: Experimental — diagnostic ID `MCPEXP001`. The implementation tracks -> [SEP-2663 (Tasks Extension)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md). -> See [Experimental APIs](xref:experimental) for how to opt in. +Tasks are provided by the `ModelContextProtocol.Extensions.Tasks` package and require MCP protocol +version `2026-07-28` or later. The implementation follows +[SEP-2663 (Tasks Extension)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md). + +### Compatibility with v1 experimental Tasks + +The Tasks extension in v2.0.0 replaces the experimental Tasks implementation shipped in v1.3.0 and +v1.4.x. The implementations are not compatible at either the API or protocol level. A v2 Tasks +client or server must use a connection negotiated to `2026-07-28` or later; it cannot fall back to +the down-level implementation. + +On a connection negotiated to the down-level `2025-11-25` protocol: + +- A v2 client calling a v1 server receives an ordinary tool result. The v2 client does not opt in + to the down-level Tasks protocol, and `GetTaskAsync` rejects use before a `2026-07-28` + connection is negotiated. +- A v1 client calling a v2 server likewise receives an ordinary tool result. The v2 server does + not create Tasks on a down-level connection, and its `tasks/get` endpoint rejects the legacy + request with a method-not-found error. + +Upgrade both peers to the v2 Tasks extension before using Tasks. The extension provides no +compatibility bridge for the previous experimental API. ### Overview @@ -50,14 +69,12 @@ and `"complete"` for ordinary results. #### Using the task store -Tasks support lives in the `ModelContextProtocol.Extensions.Tasks` package. The easiest way to -enable tasks is to call -on the server builder, passing an . +The easiest way to enable the package's task store integration is to call + on the server builder, +passing an . The SDK ships for development and tests: ```csharp -#pragma warning disable MCPEXP001 - using ModelContextProtocol.Extensions.Tasks; builder.Services.AddMcpServer() @@ -79,16 +96,9 @@ When tasks are enabled with `WithTasks` the SDK automatically: - Plumbs a `CancellationToken` through to the tool that fires when the client invokes `tasks/cancel`, so cancellation propagates cooperatively. -Alternate-result `tools/call` filters run in registration order, with the Tasks filter creating a -task at its position in that order. Filters before Tasks run before task creation. Filters after -Tasks run in the background before the ordinary filter pipeline. ASP.NET Core tool authorization -uses an alternate-result filter registered before Tasks, so an unauthorized call does not create -a task. +Alternate-result `tools/call` filters run in registration order, with the Tasks filter creating a task at its position in that order. Filters before Tasks run before task creation. Filters after Tasks run in the background before the ordinary filter pipeline. ASP.NET Core tool authorization uses an alternate-result filter registered before Tasks, so an unauthorized call does not create a task. -Ordinary `tools/call` filters still run exactly once for task-backed calls. They execute in the -background after the task record is created and before the tool body, so validation and telemetry -continue to apply. Each background invocation gets an independent DI scope that remains alive -until the tool pipeline completes. +Ordinary `tools/call` filters still run exactly once for task-backed calls. They execute in the background after the task record is created and before the tool body, so validation and telemetry continue to apply. Each background invocation gets an independent DI scope that remains alive until the tool pipeline completes. For production scenarios that need durability, session isolation, multi-process routing, or TTL-based cleanup, implement yourself diff --git a/docs/concepts/toc.yml b/docs/concepts/toc.yml index 574bface6..62c196825 100644 --- a/docs/concepts/toc.yml +++ b/docs/concepts/toc.yml @@ -21,8 +21,6 @@ items: uid: progress - name: Cancellation uid: cancellation - - name: Tasks - uid: tasks - name: Multi Round-Trip Requests (MRTR) uid: mrtr - name: Client Features @@ -55,5 +53,7 @@ items: items: - name: MCP Apps uid: apps + - name: Tasks + uid: tasks - name: Identity and Roles uid: identity diff --git a/docs/concepts/tools/tools.md b/docs/concepts/tools/tools.md index 58c86020a..f943d5270 100644 --- a/docs/concepts/tools/tools.md +++ b/docs/concepts/tools/tools.md @@ -339,7 +339,7 @@ Rules and constraints: - The header name must contain only visible ASCII characters (0x21–0x7E) excluding colon (`:`). - Values containing non-ASCII characters, control characters, or leading/trailing whitespace are Base64-encoded using the `=?base64?{value}?=` wrapper. - Header names must be case-insensitively unique within the tool's input schema. -- Header validation is enforced only for protocol versions that support the HTTP Standardization feature (currently `2026-07-28` and later). +- Header validation is enforced only for protocol versions that support the HTTP Standardization feature (`2026-07-28` and later). ### Pre-loading tool definitions on the client diff --git a/docs/concepts/transports/transports.md b/docs/concepts/transports/transports.md index eab01514f..1d924808e 100644 --- a/docs/concepts/transports/transports.md +++ b/docs/concepts/transports/transports.md @@ -382,7 +382,7 @@ Console.WriteLine(await echo.InvokeAsync(new() { ["arg"] = "Hello World" })); Like [stdio](#stdio-transport), the in-memory transport is inherently single-session — there is no `Mcp-Session-Id` header, and server-to-client requests (sampling, elicitation, roots) work naturally over the bidirectional pipe. This makes it ideal for testing servers that depend on these features. For information about how session behavior varies across transports, see [Sessions](xref:stateless). -## Cross-Application Access +## Cross-application access The SDK provides built-in support for the [Identity Assertion Authorization Grant (ID-JAG) flow](https://github.com/modelcontextprotocol/ext-auth/blob/main/specification/stable/enterprise-managed-authorization.mdx) via `IdentityAssertionGrantProvider`. This enables non-interactive enterprise SSO scenarios where users authenticate once via their enterprise Identity Provider (IdP) and access MCP servers without per-server authorization prompts. diff --git a/docs/experimental.md b/docs/experimental.md index 3849f38f1..0704fb9cb 100644 --- a/docs/experimental.md +++ b/docs/experimental.md @@ -26,9 +26,9 @@ Add the diagnostic ID to `` in your project file: Use `#pragma warning disable` around specific call sites: ```csharp -#pragma warning disable MCPEXP001 // The Extensions feature is part of a future MCP specification version that has not yet been ratified and is subject to change. -capabilities.Extensions = new Dictionary { ... }; -#pragma warning restore MCPEXP001 +#pragma warning disable MCPEXP002 // RunSessionHandler is experimental and may change. +options.RunSessionHandler = static (_, _, _) => Task.CompletedTask; +#pragma warning restore MCPEXP002 ``` For a full list of experimental diagnostic IDs and their descriptions, see the [list of diagnostics](list-of-diagnostics.md#experimental-apis). diff --git a/docs/index.md b/docs/index.md index 3e3d88c48..107c28d6a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,7 +25,7 @@ For how-to guides, tutorials, and additional guidance, see the [official MCP doc ## Official SDK packages -The official C# SDK packages for stable and prerelease versions are published to the [NuGet Gallery](https://www.nuget.org) under the [ModelContextProtocol](https://www.nuget.org/profiles/ModelContextProtocol) profile. _Prior to v0.5.0, packages were published using the [ModelContextProtocolOfficial](https://www.nuget.org/profiles/ModelContextProtocolOfficial) profile._ +The official C# SDK packages for stable and prerelease versions are published to the [NuGet Gallery](https://www.nuget.org) under the [ModelContextProtocol](https://www.nuget.org/profiles/ModelContextProtocol) profile. Continuous integration builds are published to the modelcontextprotocol organization's [GitHub NuGet package registry](https://github.com/orgs/modelcontextprotocol/packages?ecosystem=nuget). diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index 5f3592310..284f24f94 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -6,7 +6,7 @@ uid: list-of-diagnostics This document provides information about each of the diagnostics produced by the MCP C# SDK analyzers and source generators. -## Analyzer Diagnostics +## Analyzer diagnostics Analyzer diagnostic IDs are in the format `MCP###`. diff --git a/docs/roadmap.md b/docs/roadmap.md index 105a039d0..af0f075c4 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -4,20 +4,22 @@ author: jeffhandley description: ModelContextProtocol C# SDK roadmap and spec implementation tracking uid: roadmap --- -## Spec Implementation Tracking +## Spec implementation tracking -The C# SDK tracks implementation of MCP spec components using the [modelcontextprotocol project boards](https://github.com/orgs/modelcontextprotocol/projects?query=is%3Aopen), with a dedicated project board for each spec revision. For example, see the [2025-11-25 spec revision board](https://github.com/orgs/modelcontextprotocol/projects/26). +The C# SDK tracks implementation of MCP spec components using the [modelcontextprotocol project boards](https://github.com/orgs/modelcontextprotocol/projects?query=is%3Aopen), with a dedicated project board for each spec revision. See the [2026-07-28 spec revision board](https://github.com/orgs/modelcontextprotocol/projects/41/views/1). -## Current Focus Areas +## Current focus areas -### Next Spec Revision +### Next spec revision The next MCP specification revision is being developed in the [protocol repository](https://github.com/modelcontextprotocol/modelcontextprotocol). -### Feedback and End-to-End Scenarios +### Feedback and end-to-end scenarios The C# SDK team is actively responding to feedback and continuing to explore end-to-end scenarios for opportunities to add more APIs that implement common patterns. +After the 2.0.0 stable release that aligns with the 2026-07-28 specification version, we will turn our focus to the collection of [`area-auth` issues](https://github.com/modelcontextprotocol/csharp-sdk/issues?q=is%3Aissue%20label%3Aarea-auth). End-to-end auth experiences and integration with various auth servers have stood out as a theme of feedback that we aim to address in minor versions within [the 2.x milestone](https://github.com/modelcontextprotocol/csharp-sdk/milestone/9). + ## Milestones See the [milestones page](https://github.com/modelcontextprotocol/csharp-sdk/milestones) to see which issues and features are being planned for future versions. diff --git a/docs/version-picker/version-picker.css b/docs/version-picker/version-picker.css new file mode 100644 index 000000000..3377ad951 --- /dev/null +++ b/docs/version-picker/version-picker.css @@ -0,0 +1,102 @@ +/* + * Version picker widget for the multi-version MCP C# SDK docs site. + * Injected into every generated page by scripts/inject-version-picker.mjs. + * Two placements: inline in the DocFX "modern" navbar, or a fixed floating + * fallback when the navbar cannot be found. + */ +.dv-picker { + --dv-fg: #24292f; + --dv-bg: #ffffff; + --dv-border: #d0d7de; + --dv-accent: #0969da; + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-size: 0.85rem; + line-height: 1; + color: var(--dv-fg); +} + +.dv-picker__label { + opacity: 0.75; + white-space: nowrap; +} + +.dv-picker__select { + appearance: none; + -webkit-appearance: none; + background-color: var(--dv-bg); + color: var(--dv-fg); + border: 1px solid var(--dv-border); + border-radius: 6px; + padding: 0.25rem 1.55rem 0.25rem 0.55rem; + font: inherit; + cursor: pointer; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23636c76'%3E%3Cpath d='M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.45rem center; + background-size: 0.72rem; +} + +.dv-picker__select:hover { + border-color: var(--dv-accent); +} + +.dv-picker__badge { + display: inline-block; + padding: 0.1rem 0.4rem; + border-radius: 999px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.02em; + color: #9a6700; + background: #fff8c5; + border: 1px solid #eac54f; +} + +.dv-prerelease-banner { + display: flex; + justify-content: center; + align-items: center; + gap: 0.35rem; + min-height: 2.5rem; + padding: 0.4rem 1rem; + color: #24292f; + background: #fff8c5; + border-bottom: 1px solid #eac54f; + font-size: 0.9rem; + text-align: center; +} + +/* Inline in the navbar (preferred placement). */ +.dv-picker--navbar { + margin-left: 0.75rem; +} + +/* Fixed fallback when no navbar brand was found. */ +.dv-picker--floating { + position: fixed; + right: 1rem; + bottom: 1rem; + z-index: 2147483000; + padding: 0.45rem 0.6rem; + background: var(--dv-bg); + border: 1px solid var(--dv-border); + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15); +} + +/* DocFX's explicit Bootstrap theme controls the widget appearance. */ +[data-bs-theme="dark"] .dv-picker { + --dv-fg: #e6edf3; + --dv-bg: #161b22; + --dv-border: #30363d; + --dv-accent: #4493f8; +} + +[data-bs-theme="dark"] .dv-prerelease-banner { + color: #e6edf3; + background: #3d2e00; + border-color: #9a6700; +} diff --git a/docs/version-picker/version-picker.js b/docs/version-picker/version-picker.js new file mode 100644 index 000000000..6fbe1c449 --- /dev/null +++ b/docs/version-picker/version-picker.js @@ -0,0 +1,159 @@ +/* + * Version picker widget for the multi-version MCP C# SDK docs site. + * + * Configuration is provided per-page via a `window.__DOCS__` object that + * scripts/inject-version-picker.mjs writes into each page's : + * + * window.__DOCS__ = { + * version: "2.0", // slug of the version this page belongs to + * base: "/", // site base path ("/" for a custom domain) + * default: "1.x", // slug the site root redirects to + * versions: [ // newest first; drives the dropdown + * { slug: "2.0", label: "2.0 (preview)", prerelease: true }, + * { slug: "1.x", label: "1.x (latest)", prerelease: false } + * ] + * }; + * + * The pure helpers (joinPath / stripVersionPrefix / targetPath) are exported so + * they can be unit-tested in Node without a DOM. + */ + +export function joinPath(a, b) { + const left = String(a || "").replace(/\/+$/, ""); + const right = String(b == null ? "" : b).replace(/^\/+/, ""); + return (left + "/" + right).replace(/\/{2,}/g, "/"); +} + +// Given the current pathname, return the part after "//". +export function stripVersionPrefix(pathname, base, currentSlug) { + const prefix = joinPath(joinPath(base, currentSlug), ""); + if (pathname.startsWith(prefix)) return pathname.slice(prefix.length); + // Tolerate a missing trailing slash (e.g. "/2.0"). + const bare = joinPath(base, currentSlug); + if (pathname === bare) return ""; + return ""; +} + +// Derive the deployment base from the page URL. This keeps picker navigation +// within project Pages sites, whose content is hosted below "//". +export function getBasePath(cfg, pathname) { + const marker = `/${cfg.version}/`; + const markerIndex = pathname.indexOf(marker); + if (markerIndex !== -1) return pathname.slice(0, markerIndex + 1) || "/"; + + const bareMarker = `/${cfg.version}`; + if (pathname.endsWith(bareMarker)) { + return pathname.slice(0, -bareMarker.length + 1) || "/"; + } + + return cfg.base || "/"; +} + +// Compute the equivalent path in another version, preserving the sub-page. +export function targetPath(cfg, targetSlug, pathname) { + const base = getBasePath(cfg, pathname); + const sub = stripVersionPrefix(pathname, base, cfg.version); + return joinPath(joinPath(base, targetSlug), sub); +} + +function h(tag, attrs, children) { + const el = document.createElement(tag); + for (const k in attrs || {}) { + if (k === "class") el.className = attrs[k]; + else el.setAttribute(k, attrs[k]); + } + for (const c of children || []) { + el.appendChild(typeof c === "string" ? document.createTextNode(c) : c); + } + return el; +} + +function versionRoot(cfg, slug, pathname) { + return joinPath(joinPath(getBasePath(cfg, pathname), slug), ""); +} + +async function navigate(cfg, slug) { + const candidate = targetPath(cfg, slug, location.pathname); + const home = versionRoot(cfg, slug, location.pathname); + // Best effort: keep the reader on the same page if it exists in the target + // version; otherwise fall back to that version's landing page. + try { + const res = await fetch(candidate, { method: "HEAD" }); + if (res.ok) { + location.assign(candidate + location.hash); + return; + } + } catch (_) { + /* HEAD may be blocked (e.g. file://) -- fall through to the version home. */ + } + location.assign(home); +} + +function previewBadge() { + return h("span", { class: "dv-picker__badge" }, ["preview"]); +} + +function buildPicker(cfg) { + const current = cfg.versions.find((v) => v.slug === cfg.version); + + const select = h("select", { + class: "dv-picker__select", + "aria-label": "Select documentation version", + }); + for (const v of cfg.versions) { + const opt = h("option", { value: v.slug }, [v.label || v.slug]); + if (v.slug === cfg.version) opt.selected = true; + select.appendChild(opt); + } + select.addEventListener("change", () => navigate(cfg, select.value)); + + const children = [h("span", { class: "dv-picker__label" }, ["Version"]), select]; + if (current && current.prerelease) { + children.push(previewBadge()); + } + return h("div", { class: "dv-picker" }, children); +} + +function buildPrereleaseBanner() { + return h("div", { + class: "dv-prerelease-banner", + role: "status", + }, [ + "You are currently viewing", + previewBadge(), + "documentation.", + ]); +} + +function mount(picker) { + const brand = document.querySelector(".navbar-brand"); + if (brand && brand.parentNode) { + picker.classList.add("dv-picker--navbar"); + brand.parentNode.insertBefore(picker, brand.nextSibling); + return; + } + picker.classList.add("dv-picker--floating"); + document.body.appendChild(picker); +} + +function init() { + const cfg = window.__DOCS__; + if (!cfg || !Array.isArray(cfg.versions) || cfg.versions.length < 2) return; + if (document.querySelector(".dv-picker, .dv-prerelease-banner")) return; // idempotent + + const current = cfg.versions.find((v) => v.slug === cfg.version); + if (!current) return; + if (current.prerelease) { + document.body.prepend(buildPrereleaseBanner()); + } + + mount(buildPicker(cfg)); +} + +if (typeof document !== "undefined" && typeof document.createElement === "function") { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +} diff --git a/docs/versioning.md b/docs/versioning.md index 68f147acb..1305e5fa7 100644 --- a/docs/versioning.md +++ b/docs/versioning.md @@ -16,7 +16,7 @@ Given a version number MAJOR.MINOR.PATCH, the package versions increment the: ## Supported versions -Beginning with the 1.0.0 release, the following support policy will be applied for the official C# ModelContextProtocol SDK packages: +The following support policy applies to stable C# ModelContextProtocol SDK packages: 1. New functionality and additive APIs will be introduced in MINOR releases within the current MAJOR version only. * New functionality will not be added to an earlier MAJOR version. @@ -33,9 +33,21 @@ MAJOR or MINOR version updates might introduce or alter APIs annotated as [`[Exp Experimental APIs require suppression of diagnostic codes specific to the MCP SDK APIs, using an `MCP` prefix. +## MCP specification compatibility + +The 2.0.0 SDK implements the `2026-07-28` MCP specification revision while retaining compatibility with peers that negotiate [`2025-11-25`](https://modelcontextprotocol.io/specification/2025-11-25) and earlier. A v2 client automatically uses the legacy `initialize` handshake when it connects to a down-level server, and a v2 server continues to accept that handshake from a down-level client. Stable, non-deprecated 1.x APIs continue to work without modification on those connections. + +### Tasks exception + +For protocol-level compatibility, Tasks are the sole documented exception. The v2 +[`Tasks`](xref:tasks) extension replaces the experimental Tasks implementation from v1.3.0 and +v1.4.x and is available only after negotiating `2026-07-28` or later. It has no API or wire +compatibility with the down-level implementation: a v2 Tasks client or server does not use +`tasks/*` on a `2025-11-25` connection. + ## Breaking changes -Prior to the release of a stable 1.0.0 set of NuGet packages, the SDK remains in preview and breaking changes can be introduced without prior notice. Thereafter, the SDK follows Semantic Versioning and breaking changes against stable releases require increments to the MAJOR version. +The 2.0.0 SDK is a stable release. The SDK follows Semantic Versioning, and breaking changes against stable releases require increments to the MAJOR version. If feasible, the SDK will support all versions of the MCP spec. However, if breaking changes to the spec make this infeasible, preference will be given to the most recent version of the MCP spec. This would be considered a breaking change necessitating a new MAJOR version. diff --git a/scripts/finalize-docs-site.mjs b/scripts/finalize-docs-site.mjs new file mode 100644 index 000000000..d054952bb --- /dev/null +++ b/scripts/finalize-docs-site.mjs @@ -0,0 +1,138 @@ +// Finalize the combined multi-version docs site: +// * copy the picker assets to /assets/ +// * publish a cleaned docs-versions.json at the site root +// * write a root index.html that redirects to the default version +// * generate unversioned redirect pages for the deployed v1 documentation +// +// Usage: +// node scripts/finalize-docs-site.mjs [--versions ] + +import { readFile, writeFile, mkdir, copyFile, readdir } from "node:fs/promises"; +import path from "node:path"; +import { manifestPath } from "./manifest-path.mjs"; + +function parseArgs(argv) { + const positional = []; + const opts = {}; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a.startsWith("--")) opts[a.slice(2)] = argv[++i]; + else positional.push(a); + } + return { positional, opts }; +} + +function escapeHtml(s) { + return String(s).replace(/[&<>"']/g, (c) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]) + ); +} + +async function* htmlFiles(dir) { + for (const entry of await readdir(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) yield* htmlFiles(full); + else if (entry.isFile() && /\.html?$/i.test(entry.name)) yield full; + } +} + +function redirectPage(target, title) { + return ` + + + +${escapeHtml(title)} + + + + + +

Redirecting to ${escapeHtml(title)}

+ + +`; +} + +async function generateV1Redirects(combinedDir) { + const v1Dir = path.join(combinedDir, "v1"); + const redirects = new Map(); + + for await (const sourceFile of htmlFiles(v1Dir)) { + const relativePath = path.relative(v1Dir, sourceFile).split(path.sep).join("/"); + if (relativePath === "index.html") continue; + + redirects.set(relativePath, `v1/${relativePath}`); + } + + const publicMap = {}; + const redirectEntries = [...redirects].sort(([left], [right]) => left.localeCompare(right)); + for (const [sourcePath, targetPath] of redirectEntries) { + const destination = path.join(combinedDir, sourcePath); + const relativeTarget = path.relative(path.dirname(destination), path.join(combinedDir, targetPath)) + .split(path.sep) + .join("/"); + + await mkdir(path.dirname(destination), { recursive: true }); + await writeFile(destination, redirectPage(relativeTarget, "MCP C# SDK documentation")); + publicMap[`/${sourcePath}`] = `/${targetPath}`; + } + + await writeFile( + path.join(combinedDir, "v1-redirects.json"), + JSON.stringify(publicMap, null, 2) + "\n" + ); + + return redirects.size; +} + +async function main() { + const { positional, opts } = parseArgs(process.argv.slice(2)); + const combinedDir = positional[0]; + if (!combinedDir) { + console.error("usage: finalize-docs-site.mjs [--versions ]"); + process.exit(2); + } + + const pickerDir = new URL("../docs/version-picker/", import.meta.url); + const versionsPath = opts.versions + ? path.resolve(opts.versions) + : manifestPath; + const manifest = JSON.parse(await readFile(versionsPath, "utf8")); + + // 1. Copy picker assets. + const assetsDir = path.join(combinedDir, "assets"); + await mkdir(assetsDir, { recursive: true }); + for (const name of ["version-picker.js", "version-picker.css"]) { + await copyFile(new URL(name, pickerDir), path.join(assetsDir, name)); + } + + // 2. Public docs-versions.json (drop build-only fields like `ref` and `$comment`). + const publicManifest = { + default: manifest.default, + versions: manifest.versions.map((v) => ({ + slug: v.slug, + label: v.label, + prerelease: !!v.prerelease, + })), + }; + await writeFile( + path.join(combinedDir, "docs-versions.json"), + JSON.stringify(publicManifest, null, 2) + "\n" + ); + + // 3. Root redirect to the default version (relative -> works under any base). + const def = manifest.default; + const target = `./${def}/`; + await writeFile( + path.join(combinedDir, "index.html"), + redirectPage(target, "MCP C# SDK documentation") + ); + + const redirectCount = await generateV1Redirects(combinedDir); + console.log(`[finalize] assets + docs-versions.json written; root redirects to ${target}; ${redirectCount} v1 redirects written`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/get-docs-version-slug.mjs b/scripts/get-docs-version-slug.mjs new file mode 100644 index 000000000..14aeebd7b --- /dev/null +++ b/scripts/get-docs-version-slug.mjs @@ -0,0 +1,21 @@ +// Print the docs URL slug for a source tree's VersionPrefix. +// +// Usage: +// node scripts/get-docs-version-slug.mjs + +import { readFile } from "node:fs/promises"; + +const propsPath = process.argv[2]; +if (!propsPath) { + console.error("usage: get-docs-version-slug.mjs "); + process.exit(2); +} + +const props = await readFile(propsPath, "utf8"); +const match = props.match(/\s*([1-9][0-9]*)\.\d+\.\d+\s*<\/VersionPrefix>/); +if (!match) { + console.error(`error: unable to determine VersionPrefix from ${propsPath}`); + process.exit(1); +} + +process.stdout.write(`v${match[1]}\n`); diff --git a/scripts/inject-version-picker.mjs b/scripts/inject-version-picker.mjs new file mode 100644 index 000000000..23623f182 --- /dev/null +++ b/scripts/inject-version-picker.mjs @@ -0,0 +1,116 @@ +// Inject the version-picker widget into every .html page of a built docs site. +// +// Usage: +// node scripts/inject-version-picker.mjs [--base /] [--versions ] +// +// is a single version's built output (e.g. combined/2.0). +// is that version's slug (must match an entry in docs-versions.json). +// The widget config is derived from docs-versions.json and written into each page's +// . The picker assets are referenced relative to each generated page so they +// work from both a custom domain and a project Pages subpath. + +import { readFile, writeFile, readdir } from "node:fs/promises"; +import { createHash } from "node:crypto"; +import path from "node:path"; +import { manifestPath } from "./manifest-path.mjs"; + +const MARKER = ""; + +function parseArgs(argv) { + const positional = []; + const opts = {}; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a.startsWith("--")) opts[a.slice(2)] = argv[++i]; + else positional.push(a); + } + return { positional, opts }; +} + +function normalizeBase(base) { + let b = (base || "/").trim(); + if (!b.startsWith("/")) b = "/" + b; + if (!b.endsWith("/")) b += "/"; + return b.replace(/\/{2,}/g, "/"); +} + +async function* htmlFiles(dir) { + for (const entry of await readdir(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) yield* htmlFiles(full); + else if (entry.isFile() && /\.html?$/i.test(entry.name)) yield full; + } +} + +async function assetRevision(name) { + const contents = await readFile(new URL(`../docs/version-picker/${name}`, import.meta.url)); + return createHash("sha256").update(contents).digest("hex").slice(0, 12); +} + +async function main() { + const { positional, opts } = parseArgs(process.argv.slice(2)); + const [siteDir, slug] = positional; + if (!siteDir || !slug) { + console.error("usage: inject-version-picker.mjs [--base /] [--versions ]"); + process.exit(2); + } + + const base = normalizeBase(opts.base); + const versionsPath = opts.versions + ? path.resolve(opts.versions) + : manifestPath; + const manifest = JSON.parse(await readFile(versionsPath, "utf8")); + + if (!manifest.versions.some((v) => v.slug === slug)) { + console.error(`error: slug "${slug}" is not present in docs-versions.json`); + process.exit(1); + } + + const config = { + version: slug, + base, + default: manifest.default, + versions: manifest.versions.map((v) => ({ + slug: v.slug, + label: v.label, + prerelease: !!v.prerelease, + })), + }; + + const json = JSON.stringify(config).replace(//i); + if (idx === -1) { + skipped++; + continue; + } + const assetBase = path.relative(path.dirname(file), assetsDir).split(path.sep).join("/") + "/"; + const snippet = + `\n${MARKER}\n` + + `\n` + + `\n` + + `\n`; + await writeFile(file, html.slice(0, idx) + snippet + html.slice(idx)); + injected++; + } + + console.log(`[inject] ${slug}: injected into ${injected} page(s), skipped ${skipped}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/list-versions.mjs b/scripts/list-versions.mjs new file mode 100644 index 000000000..8c1ccfba2 --- /dev/null +++ b/scripts/list-versions.mjs @@ -0,0 +1,17 @@ +// Print the versions to build, one per line, as "\t". +// Consumed by the multi-version docs workflow to drive its build loop. +// +// Usage: node scripts/list-versions.mjs [--versions ] + +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { manifestPath } from "./manifest-path.mjs"; + +const arg = process.argv.slice(2); +const i = arg.indexOf("--versions"); +const versionsPath = i !== -1 ? path.resolve(arg[i + 1]) : manifestPath; + +const manifest = JSON.parse(await readFile(versionsPath, "utf8")); +for (const v of manifest.versions) { + process.stdout.write(`${v.slug}\t${v.ref}\n`); +} diff --git a/scripts/manifest-path.mjs b/scripts/manifest-path.mjs new file mode 100644 index 000000000..485509f91 --- /dev/null +++ b/scripts/manifest-path.mjs @@ -0,0 +1,16 @@ +// Location of the docs-versions manifest. +// +// This manifest is a build-time artifact produced during docs publishing, which +// only ever runs in CI -- never on a developer's machine. It therefore lives in +// the runner's temporary directory (RUNNER_TEMP on GitHub Actions, falling back +// to the OS temp dir for local testing) so it never touches the repository tree +// and needs no .gitignore entry. Every script agrees on this path, so the +// workflow does not need to thread it between steps. + +import os from "node:os"; +import path from "node:path"; + +export const manifestPath = path.join( + process.env.RUNNER_TEMP || os.tmpdir(), + "docs-versions.json" +);