diff --git a/docs/README.skills.md b/docs/README.skills.md
index b5a8744b2..33460e405 100644
--- a/docs/README.skills.md
+++ b/docs/README.skills.md
@@ -121,7 +121,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
| [creating-oracle-to-postgres-migration-integration-tests](../skills/creating-oracle-to-postgres-migration-integration-tests/SKILL.md)
`gh skills install github/awesome-copilot creating-oracle-to-postgres-migration-integration-tests` | Creates integration test cases for .NET data access artifacts during Oracle-to-PostgreSQL database migrations. Generates DB-agnostic xUnit tests with deterministic seed data that validate behavior consistency across both database systems. Use when creating integration tests for a migrated project, generating test coverage for data access layers, or writing Oracle-to-PostgreSQL migration validation tests. | None |
| [csharp-async](../skills/csharp-async/SKILL.md)
`gh skills install github/awesome-copilot csharp-async` | Get best practices for C# async programming | None |
| [csharp-docs](../skills/csharp-docs/SKILL.md)
`gh skills install github/awesome-copilot csharp-docs` | Ensure that C# types are documented with XML comments and follow best practices for documentation. | None |
-| [csharp-mcp-server-generator](../skills/csharp-mcp-server-generator/SKILL.md)
`gh skills install github/awesome-copilot csharp-mcp-server-generator` | Generate a complete MCP server project in C# with tools, prompts, and proper configuration | None |
+| [csharp-mcp-server-generator](../skills/csharp-mcp-server-generator/SKILL.md)
`gh skills install github/awesome-copilot csharp-mcp-server-generator` | Deprecated: superseded by dotnet-mcp-builder, which targets the current stable ModelContextProtocol 1.x packages and covers every MCP primitive (tools, prompts, resources, elicitation, sampling, roots, MCP Apps) plus both transports (STDIO and Streamable HTTP). Install dotnet-mcp-builder instead. | None |
| [csharp-mstest](../skills/csharp-mstest/SKILL.md)
`gh skills install github/awesome-copilot csharp-mstest` | Get best practices for MSTest 3.x/4.x unit testing, including modern assertion APIs and data-driven tests | None |
| [csharp-nunit](../skills/csharp-nunit/SKILL.md)
`gh skills install github/awesome-copilot csharp-nunit` | Get best practices for NUnit unit testing, including data-driven tests | None |
| [csharp-tunit](../skills/csharp-tunit/SKILL.md)
`gh skills install github/awesome-copilot csharp-tunit` | Get best practices for TUnit unit testing, including data-driven tests | None |
@@ -141,6 +141,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
| [documentation-writer](../skills/documentation-writer/SKILL.md)
`gh skills install github/awesome-copilot documentation-writer` | Diátaxis Documentation Expert. An expert technical writer specializing in creating high-quality software documentation, guided by the principles and structure of the Diátaxis technical documentation authoring framework. | None |
| [dotnet-best-practices](../skills/dotnet-best-practices/SKILL.md)
`gh skills install github/awesome-copilot dotnet-best-practices` | Ensure .NET/C# code meets best practices for the solution/project. | None |
| [dotnet-design-pattern-review](../skills/dotnet-design-pattern-review/SKILL.md)
`gh skills install github/awesome-copilot dotnet-design-pattern-review` | Review the C#/.NET code for design pattern implementation and suggest improvements. | None |
+| [dotnet-mcp-builder](../skills/dotnet-mcp-builder/SKILL.md)
`gh skills install github/awesome-copilot dotnet-mcp-builder` | Build Model Context Protocol (MCP) servers in C#/.NET against the current ModelContextProtocol 1.x NuGet packages. Especially helps with cases the model often gets wrong without guidance — stale preview versions (it tends to pick 0.3 or 0.4 preview), MCP Apps (interactive UI rendered in the host), elicitation URL mode, per-session HTTP wiring, OAuth and reverse-proxy deploy specifics, and debugging concrete MapMcp / STDIO / Streamable-HTTP errors. Also covers the routine work — STDIO and Streamable HTTP transports (SSE is deprecated), tools, prompts, resources, sampling, roots, completions, logging — and a basic .NET MCP client. Trigger when the user says or implies any .NET MCP server work: ModelContextProtocol, McpServerTool, MapMcp, WithStdioServerTransport, "MCP server in C#", "MCP tool in dotnet", "expose this as MCP", or names a primitive (prompt/resource/elicitation/MCP App) in a .NET context. Skip for MCP work in other languages. | `references/client.md`
`references/elicitation.md`
`references/mcp-apps.md`
`references/packages.md`
`references/prompt-primitive.md`
`references/resource-primitive.md`
`references/roots.md`
`references/sampling.md`
`references/server-features.md`
`references/testing.md`
`references/tool-primitive.md`
`references/transport-http.md`
`references/transport-stdio.md` |
| [dotnet-timezone](../skills/dotnet-timezone/SKILL.md)
`gh skills install github/awesome-copilot dotnet-timezone` | .NET timezone handling guidance for C# applications. Use when working with TimeZoneInfo, DateTimeOffset, NodaTime, UTC conversion, daylight saving time, scheduling across timezones, cross-platform Windows/IANA timezone IDs, or when a .NET user needs the timezone for a city, address, region, or country and copy-paste-ready C# code. | `references/code-patterns.md`
`references/timezone-index.md` |
| [dotnet-upgrade](../skills/dotnet-upgrade/SKILL.md)
`gh skills install github/awesome-copilot dotnet-upgrade` | Ready-to-use prompts for comprehensive .NET framework upgrade analysis and execution | None |
| [doublecheck](../skills/doublecheck/SKILL.md)
`gh skills install github/awesome-copilot doublecheck` | Three-layer verification pipeline for AI output. Extracts verifiable claims, finds supporting or contradicting sources via web search, runs adversarial review for hallucination patterns, and produces a structured verification report with source links for human review. | `assets/verification-report-template.md` |
diff --git a/skills/csharp-mcp-server-generator/SKILL.md b/skills/csharp-mcp-server-generator/SKILL.md
index e36ae2fe5..4651f969e 100644
--- a/skills/csharp-mcp-server-generator/SKILL.md
+++ b/skills/csharp-mcp-server-generator/SKILL.md
@@ -1,59 +1,24 @@
---
name: csharp-mcp-server-generator
-description: 'Generate a complete MCP server project in C# with tools, prompts, and proper configuration'
+description: 'Deprecated: superseded by dotnet-mcp-builder, which targets the current stable ModelContextProtocol 1.x packages and covers every MCP primitive (tools, prompts, resources, elicitation, sampling, roots, MCP Apps) plus both transports (STDIO and Streamable HTTP). Install dotnet-mcp-builder instead.'
---
-# Generate C# MCP Server
+# csharp-mcp-server-generator (deprecated)
-Create a complete Model Context Protocol (MCP) server in C# with the following specifications:
+This skill has been superseded by **`dotnet-mcp-builder`**.
-## Requirements
+`dotnet-mcp-builder` is a strict superset: it targets the current stable `ModelContextProtocol` 1.x NuGet packages (this skill predated 1.0 and referenced the prerelease line, which has breaking API differences), covers both transports (STDIO and Streamable HTTP — the legacy HTTP+SSE transport is deprecated upstream), and covers every primitive in the current MCP spec (2025-11-25): tools, prompts, resources, elicitation (form + URL mode), sampling, roots, completions, logging, and MCP Apps. It also includes a basic .NET MCP client and testing reference, and steers the model away from common pitfalls — STDIO stdout/stderr trap, stateless-vs-stateful HTTP wiring, OAuth and reverse-proxy specifics for remote deployments.
-1. **Project Structure**: Create a new C# console application with proper directory structure
-2. **NuGet Packages**: Include ModelContextProtocol (prerelease) and Microsoft.Extensions.Hosting
-3. **Logging Configuration**: Configure all logs to stderr to avoid interfering with stdio transport
-4. **Server Setup**: Use the Host builder pattern with proper DI configuration
-5. **Tools**: Create at least one useful tool with proper attributes and descriptions
-6. **Error Handling**: Include proper error handling and validation
+## Migration steps
-## Implementation Details
+1. Install the replacement skill:
+ ```
+ gh skills install github/awesome-copilot dotnet-mcp-builder
+ ```
+2. (Optional) Uninstall this skill so the deprecation notice no longer appears in your skill list:
+ ```
+ gh skills uninstall github/awesome-copilot csharp-mcp-server-generator
+ ```
+3. Re-run your existing prompts. The new skill triggers on the same .NET MCP server intents and on a broader set (HTTP transport, prompts/resources, elicitation, MCP Apps, debugging).
-### Basic Project Setup
-- Use .NET 8.0 or later
-- Create a console application
-- Add necessary NuGet packages with --prerelease flag
-- Configure logging to stderr
-
-### Server Configuration
-- Use `Host.CreateApplicationBuilder` for DI and lifecycle management
-- Configure `AddMcpServer()` with stdio transport
-- Use `WithToolsFromAssembly()` for automatic tool discovery
-- Ensure the server runs with `RunAsync()`
-
-### Tool Implementation
-- Use `[McpServerToolType]` attribute on tool classes
-- Use `[McpServerTool]` attribute on tool methods
-- Add `[Description]` attributes to tools and parameters
-- Support async operations where appropriate
-- Include proper parameter validation
-
-### Code Quality
-- Follow C# naming conventions
-- Include XML documentation comments
-- Use nullable reference types
-- Implement proper error handling with McpProtocolException
-- Use structured logging for debugging
-
-## Example Tool Types to Consider
-- File operations (read, write, search)
-- Data processing (transform, validate, analyze)
-- External API integrations (HTTP requests)
-- System operations (execute commands, check status)
-- Database operations (query, update)
-
-## Testing Guidance
-- Explain how to run the server
-- Provide example commands to test with MCP clients
-- Include troubleshooting tips
-
-Generate a complete, production-ready MCP server with comprehensive documentation and error handling.
+The replacement skill lives under `skills/dotnet-mcp-builder/` in this repository.
diff --git a/skills/dotnet-mcp-builder/SKILL.md b/skills/dotnet-mcp-builder/SKILL.md
new file mode 100644
index 000000000..300277af5
--- /dev/null
+++ b/skills/dotnet-mcp-builder/SKILL.md
@@ -0,0 +1,83 @@
+---
+name: dotnet-mcp-builder
+description: 'Build Model Context Protocol (MCP) servers in C#/.NET against the current ModelContextProtocol 1.x NuGet packages. Especially helps with cases the model often gets wrong without guidance — stale preview versions (it tends to pick 0.3 or 0.4 preview), MCP Apps (interactive UI rendered in the host), elicitation URL mode, per-session HTTP wiring, OAuth and reverse-proxy deploy specifics, and debugging concrete MapMcp / STDIO / Streamable-HTTP errors. Also covers the routine work — STDIO and Streamable HTTP transports (SSE is deprecated), tools, prompts, resources, sampling, roots, completions, logging — and a basic .NET MCP client. Trigger when the user says or implies any .NET MCP server work: ModelContextProtocol, McpServerTool, MapMcp, WithStdioServerTransport, "MCP server in C#", "MCP tool in dotnet", "expose this as MCP", or names a primitive (prompt/resource/elicitation/MCP App) in a .NET context. Skip for MCP work in other languages.'
+---
+
+# Building MCP servers in .NET
+
+This skill helps you write production-quality MCP servers and basic clients in C#/.NET against the **official** [`ModelContextProtocol`](https://www.nuget.org/profiles/ModelContextProtocol) NuGet packages, maintained by Microsoft and the MCP project. It targets the **stable 1.x** line and the current spec (2025-11-25).
+
+## When this skill earns its keep
+
+The .NET MCP SDK had years of preview packages (`0.x-preview`) before reaching `1.0`. Without help, the model tends to:
+- Pin a stale preview version that won't compile against current samples.
+- Miss recent spec features (elicitation URL mode, MCP Apps, structured content blocks).
+- Get HTTP transport details wrong (stateful/stateless, proxy buffering, OAuth wiring).
+- Forget the STDIO stdout/stderr trap.
+
+If the task is one of those, *load the matching reference* and follow it. If it's truly trivial (e.g. "rename this tool method"), you don't need to read everything — the cardinal rules below are the minimum.
+
+## Mental model in 30 seconds
+
+A .NET MCP server is an ordinary `Microsoft.Extensions.Hosting` (or `WebApplication`) app that wires an MCP server through DI:
+
+```csharp
+builder.Services
+ .AddMcpServer()
+ .WithStdioServerTransport() // OR .WithHttpTransport(...)
+ .WithToolsFromAssembly() // discover [McpServerToolType] classes
+ .WithPrompts() // optional
+ .WithResources(); // optional
+```
+
+Primitives are plain C# methods on classes marked with attributes (`[McpServerToolType]` + `[McpServerTool]`, `[McpServerPromptType]` + `[McpServerPrompt]`, `[McpServerResourceType]` + `[McpServerResource]`). Parameters bind from JSON-RPC; the SDK builds the JSON Schema from the signature plus `[Description]` attributes.
+
+Server-to-client features (sampling, elicitation, roots, log/progress notifications) are methods on the injected `IMcpServer`.
+
+## Decision tree → which references to load
+
+Always load `references/packages.md` if you're creating a new project or unsure of the current package version.
+
+| Task | Load |
+|---|---|
+| New STDIO server | `references/transport-stdio.md` |
+| New HTTP (Streamable) server | `references/transport-http.md` |
+| Add/modify a tool | `references/tool-primitive.md` |
+| Add/modify a prompt | `references/prompt-primitive.md` |
+| Add/modify a resource | `references/resource-primitive.md` |
+| Ask the user a question mid-tool | `references/elicitation.md` |
+| Call the client's LLM from a tool | `references/sampling.md` |
+| Read the user's project roots | `references/roots.md` |
+| Return an interactive UI | `references/mcp-apps.md` |
+| Argument completions, log/progress notifications, filters, server instructions | `references/server-features.md` |
+| Write a .NET program that **consumes** an MCP server | `references/client.md` |
+| MCP Inspector, in-memory tests, mocks, CI | `references/testing.md` |
+
+For multi-primitive tasks, load several at once. For trivial edits in an existing file, you usually don't need any.
+
+## Cardinal rules (apply always; these prevent the highest-frequency breakages)
+
+1. **Pin the current stable package, not a preview.** Use `ModelContextProtocol` / `ModelContextProtocol.AspNetCore` / `ModelContextProtocol.Core` at the latest **1.x**. If you find yourself writing `0.3-preview` or `0.4-preview`, stop and check NuGet — preview APIs have breaking differences.
+2. **STDIO servers must not write to stdout.** Stdout is the JSON-RPC channel. Configure `LogToStandardErrorThreshold = LogLevel.Trace` before anything else and never `Console.WriteLine` from a tool.
+3. **HTTP defaults to stateful.** For horizontally-scaled deployments without server-initiated traffic, set `options.Stateless = true`. Server-to-client features (sampling, elicitation, roots, unsolicited notifications) require stateful HTTP **or** STDIO — `Stateless = true` will break them at runtime.
+4. **SSE-only is deprecated.** Use Streamable HTTP. Only enable legacy SSE (`EnableLegacySse = true`) for an old client you must support, and call it out.
+5. **Always `[Description]` tools and parameters.** This is what the LLM sees when picking and shaping calls. Vague descriptions are the #1 reason tools don't get used.
+6. **Show the registration line every time you add a primitive.** A new `[McpServerPromptType]` class without `.WithPrompts<...>()` (or `.WithPromptsFromAssembly()`) is invisible.
+7. **Don't invent APIs.** If you're unsure a method exists, say so and check the [API reference](https://csharp.sdk.modelcontextprotocol.io/api/ModelContextProtocol.html) — wrong method names cause silent failures.
+
+## Working style
+
+- **Make minimal, additive changes.** Add a method to the existing tool class rather than restructuring the project.
+- **For non-trivial setups, run `dotnet build`.** Catches missing usings, attribute typos, and TFM mismatches before the user sees them.
+- **Confirm transport + .NET version + primitives before scaffolding** if context doesn't already make them obvious. Default to **.NET 10** for new projects.
+
+## When the user is stuck
+
+Walk this checklist before guessing:
+1. **STDIO:** something is writing to stdout (logger sink, `Console.WriteLine`, library banner).
+2. **HTTP 404:** path mismatch — `app.MapMcp()` is root, `app.MapMcp("/mcp")` puts it under `/mcp`.
+3. **Tool not appearing:** missing `[McpServerToolType]` on the class, or no `.WithToolsFromAssembly()` / `.WithTools()` registered.
+4. **Args not bound:** parameter names must match the JSON-RPC `arguments` keys; complex types bind via `System.Text.Json`.
+5. **Sampling/elicitation/roots failing:** transport is stateless HTTP, or the client doesn't advertise the capability.
+
+Still stuck? Point the user at the [`EverythingServer`](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/EverythingServer) sample — it exercises every feature.
diff --git a/skills/dotnet-mcp-builder/references/client.md b/skills/dotnet-mcp-builder/references/client.md
new file mode 100644
index 000000000..cf5ac4d7f
--- /dev/null
+++ b/skills/dotnet-mcp-builder/references/client.md
@@ -0,0 +1,191 @@
+# Building an MCP client in .NET
+
+A short reference for *consuming* an MCP server from .NET — useful for testing your server, building agent harnesses, or wiring MCP into a Semantic Kernel / Microsoft.Extensions.AI pipeline.
+
+For just *running* a server, ignore this file.
+
+## Packages
+
+```bash
+dotnet add package ModelContextProtocol.Core # minimal: just client + transports
+# or
+dotnet add package ModelContextProtocol # adds DI/hosting helpers
+```
+
+## Connecting via STDIO (launching a server process)
+
+```csharp
+using ModelContextProtocol.Client;
+
+var transport = new StdioClientTransport(new StdioClientTransportOptions
+{
+ Command = "dotnet",
+ Arguments = ["run", "--project", "../MyMcpServer"],
+ EnvironmentVariables = new() { ["MY_API_KEY"] = "..." },
+ ShutdownTimeout = TimeSpan.FromSeconds(10),
+ StandardErrorLines = line => Console.Error.WriteLine($"[server] {line}")
+});
+
+await using var client = await McpClient.CreateAsync(transport);
+```
+
+`StandardErrorLines` is a great debugging aid — you'll see your server's logs as they happen.
+
+## Connecting via HTTP (Streamable)
+
+```csharp
+using ModelContextProtocol.Client;
+
+var transport = new HttpClientTransport(new HttpClientTransportOptions
+{
+ Endpoint = new Uri("https://my-server.example.com/mcp"),
+ TransportMode = HttpTransportMode.StreamableHttp,
+ ConnectionTimeout = TimeSpan.FromSeconds(30),
+ AdditionalHeaders = new Dictionary
+ {
+ ["Authorization"] = "Bearer ..."
+ }
+});
+
+await using var client = await McpClient.CreateAsync(transport);
+```
+
+`TransportMode = AutoDetect` (the default) tries Streamable HTTP first and falls back to SSE — useful for older servers, but pin to `StreamableHttp` for new code so failures are loud.
+
+## Listing and calling tools
+
+```csharp
+IList tools = await client.ListToolsAsync();
+
+foreach (var t in tools)
+ Console.WriteLine($"- {t.Name}: {t.Description}");
+
+var echo = tools.First(t => t.Name == "Echo");
+CallToolResult result = await echo.CallAsync(new Dictionary
+{
+ ["message"] = "hello"
+});
+
+if (result.IsError == true)
+{
+ var msg = result.Content.OfType().FirstOrDefault()?.Text;
+ Console.Error.WriteLine($"Tool failed: {msg}");
+ return;
+}
+
+foreach (var block in result.Content)
+{
+ switch (block)
+ {
+ case TextContentBlock text:
+ Console.WriteLine(text.Text);
+ break;
+ case ImageContentBlock image:
+ File.WriteAllBytes("out.png", image.DecodedData.ToArray());
+ break;
+ }
+}
+```
+
+## Listing prompts and resources
+
+```csharp
+IList prompts = await client.ListPromptsAsync();
+GetPromptResult pr = await client.GetPromptAsync("code_review",
+ new Dictionary { ["language"] = "csharp", ["code"] = "..." });
+
+IList resources = await client.ListResourcesAsync();
+ReadResourceResult rr = await client.ReadResourceAsync("config://app/settings");
+```
+
+## Subscribing to server notifications
+
+```csharp
+client.RegisterNotificationHandler(
+ NotificationMethods.ToolListChangedNotification,
+ async (notification, ct) =>
+ {
+ var updated = await client.ListToolsAsync(cancellationToken: ct);
+ Console.WriteLine($"Tool list changed; now {updated.Count} tools.");
+ });
+```
+
+## Handling server-to-client requests (sampling, elicitation, roots)
+
+If your server uses these features, your client must handle them. Configure handlers when creating the client:
+
+```csharp
+await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
+{
+ Capabilities = new()
+ {
+ Sampling = new()
+ {
+ SamplingHandler = async (req, progress, ct) =>
+ {
+ // Route req.Messages to your IChatClient and return a CreateMessageResult.
+ var response = await myChatClient.GetResponseAsync(/* convert */, ct);
+ return new CreateMessageResult { /* fill in */ };
+ }
+ },
+ Elicitation = new()
+ {
+ ElicitationHandler = async (req, ct) =>
+ {
+ // Show req.Message + req.RequestedSchema to the user; collect input.
+ return new ElicitResult { Action = "accept", Content = collectedValues };
+ }
+ },
+ Roots = new()
+ {
+ RootsHandler = async (req, ct) =>
+ {
+ return new ListRootsResult
+ {
+ Roots = new[] { new Root { Uri = "file:///workspace", Name = "Workspace" } }
+ };
+ }
+ }
+ }
+});
+```
+
+If you don't supply a handler and the server calls the feature, the call fails with a "method not supported" error.
+
+## Using MCP tools as `IChatClient` function tools
+
+If you're plugging MCP into a `Microsoft.Extensions.AI` pipeline, expose tools as `AIFunction`:
+
+```csharp
+using Microsoft.Extensions.AI;
+
+IList mcpTools = await client.ListToolsAsync();
+
+var chatOptions = new ChatOptions
+{
+ Tools = mcpTools.Cast().ToList()
+};
+
+var chatClient = new MyChatClient(...); // any IChatClient
+var response = await chatClient.GetResponseAsync(messages, chatOptions);
+```
+
+`McpClientTool` implements `AIFunction` — function-calling middleware will invoke the right tool and feed the result back to the LLM automatically.
+
+## Resuming a session (HTTP, stateful)
+
+```csharp
+var transport = new HttpClientTransport(new HttpClientTransportOptions
+{
+ Endpoint = new Uri("https://my-server.example.com/mcp"),
+ KnownSessionId = previousSessionId
+});
+
+await using var client = await McpClient.ResumeSessionAsync(transport, new ResumeClientSessionOptions
+{
+ ServerCapabilities = previousServerCapabilities,
+ ServerInfo = previousServerInfo
+});
+```
+
+Useful for long-lived agent processes that survive transient network drops.
diff --git a/skills/dotnet-mcp-builder/references/elicitation.md b/skills/dotnet-mcp-builder/references/elicitation.md
new file mode 100644
index 000000000..f3a901cff
--- /dev/null
+++ b/skills/dotnet-mcp-builder/references/elicitation.md
@@ -0,0 +1,176 @@
+# Elicitation
+
+Elicitation lets a tool **ask the user for input mid-execution**, via the client. The LLM doesn't see the question; the client surfaces it directly to the user. This turns one-shot tool calls into interactive flows — collecting confirmation, missing parameters, credentials (URL mode), etc.
+
+> **Spec version:** 2025-11-25. URL mode is the newer addition (originally 2025-06-18 had only form mode).
+
+## Two modes
+
+| Mode | What it does | When to use |
+|---|---|---|
+| **Form (in-band)** | Server sends a JSON Schema; client renders a form; user submits values back through the same MCP channel. | Confirmations, missing parameters, structured choices. |
+| **URL (out-of-band)** | Server sends a URL; client opens it in a browser; user completes the flow there; server checks state separately. | OAuth, payments, anything the MCP channel must not see. |
+
+## Prerequisite: stateful transport
+
+Elicitation requires the server to send a request *to* the client and wait for a response. That only works on:
+- STDIO (always).
+- Stateful HTTP (`options.Stateless = false`).
+
+In stateless HTTP, `ElicitAsync` will throw — there's no transport channel back.
+
+## Form mode — full example
+
+```csharp
+using System.ComponentModel;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+
+[McpServerToolType]
+public class BookingTools
+{
+ [McpServerTool, Description("Books a meeting room. Asks the user for confirmation.")]
+ public static async Task BookRoom(
+ IMcpServer server,
+ [Description("Room name")] string room,
+ [Description("Start time (ISO 8601)")] DateTime start,
+ CancellationToken ct)
+ {
+ var elicit = await server.ElicitAsync(new ElicitRequestParams
+ {
+ Message = $"Confirm booking '{room}' at {start:HH:mm}?",
+ RequestedSchema = new ElicitRequestParams.RequestSchema
+ {
+ Properties = new Dictionary
+ {
+ ["confirm"] = new ElicitRequestParams.BooleanSchema
+ {
+ Description = "Confirm the booking",
+ Default = true
+ },
+ ["notes"] = new ElicitRequestParams.StringSchema
+ {
+ Description = "Optional notes for the booking"
+ }
+ }
+ }
+ }, ct);
+
+ if (elicit.Action != "accept")
+ return "Booking cancelled by user.";
+
+ var confirmed = elicit.Content?["confirm"].GetBoolean() ?? false;
+ var notes = elicit.Content?["notes"].GetString() ?? "";
+
+ if (!confirmed)
+ return "User declined to confirm.";
+
+ // …perform the booking…
+ return $"Booked '{room}' at {start:O}. Notes: {notes}";
+ }
+}
+```
+
+### Schema primitive types
+
+You can build a `RequestedSchema` from these:
+
+| Type | C# class | Notes |
+|---|---|---|
+| String | `StringSchema` | `Default`, `Description`. Add JSON-Schema validation at server side if you need it. |
+| Number | `NumberSchema` | Use for ints and floats. |
+| Boolean | `BooleanSchema` | Renders as a checkbox / toggle. |
+| Single-select enum (untitled) | `UntitledSingleSelectEnumSchema` | List of values; client renders as dropdown/radio. |
+| Single-select enum (titled) | `TitledSingleSelectEnumSchema` | Each value has a display title. |
+| Multi-select enum | `UntitledMultiSelectEnumSchema` / `TitledMultiSelectEnumSchema` | Multi-select dropdown / checkbox group. |
+
+Each accepts `Description` and `Default`.
+
+### Response shape
+
+`ElicitResult`:
+- `Action` — `"accept"`, `"reject"`, or `"cancel"`. Always check this first.
+- `Content` — `Dictionary?` with the user's submitted values. `null` if the user rejected/cancelled.
+
+Always handle the non-accept paths:
+
+```csharp
+if (elicit.Action == "cancel")
+ return "User cancelled. No changes made.";
+if (elicit.Action == "reject")
+ return "User declined.";
+// Action == "accept" → safe to read elicit.Content
+```
+
+## URL mode — full example
+
+URL mode is for flows where the user must complete something **outside** the MCP channel — typically OAuth.
+
+```csharp
+[McpServerTool, Description("Connects the user's GitHub account.")]
+public static async Task ConnectGitHub(
+ IMcpServer server,
+ IOAuthService oauth,
+ CancellationToken ct)
+{
+ var elicitationId = Guid.NewGuid().ToString();
+ var authUrl = oauth.BuildAuthorizationUrl(state: elicitationId);
+
+ var result = await server.ElicitAsync(new ElicitRequestParams
+ {
+ Mode = "url",
+ ElicitationId = elicitationId,
+ Url = authUrl,
+ Message = "Please authorize access to GitHub in the browser window that just opened."
+ }, ct);
+
+ if (result.Action != "accept")
+ return "Authorization cancelled.";
+
+ // The user has come back. Look up the persisted token by elicitationId.
+ var token = await oauth.GetTokenByStateAsync(elicitationId, ct);
+ return token is not null ? "Connected." : "Authorization did not complete.";
+}
+```
+
+### `UrlElicitationRequiredException`
+
+When a tool is *blocked* on auth (rather than walking the user through it), throw `UrlElicitationRequiredException`. The client surfaces the URL to the user and the call fails cleanly. Useful for retry-after-auth patterns:
+
+```csharp
+if (!oauth.HasValidToken)
+{
+ var id = Guid.NewGuid().ToString();
+ throw new UrlElicitationRequiredException(
+ "Authorization required",
+ new[]
+ {
+ new ElicitRequestParams
+ {
+ Mode = "url",
+ ElicitationId = id,
+ Url = oauth.BuildAuthorizationUrl(state: id),
+ Message = "Sign in to continue."
+ }
+ });
+}
+```
+
+## When NOT to use elicitation
+
+- **Trivial confirmations the LLM can ask in natural language.** If you can phrase "Should I do X?" in your tool's docstring and let the LLM ask, that's lower friction than a modal form.
+- **Branching that the LLM should reason about.** Don't replace the LLM's judgment with a form — only elicit for things the LLM literally cannot decide (user secrets, real-time consent, picking from a list only the user knows).
+- **Stateless deployments.** Doesn't work — see prerequisite above.
+
+## Client capability check
+
+Don't blindly call `ElicitAsync`. Check first:
+
+```csharp
+if (server.ClientCapabilities?.Elicitation is null)
+ return "This client doesn't support elicitation; please pass the value as an argument.";
+
+var elicit = await server.ElicitAsync(...);
+```
+
+This degrades gracefully on older clients.
diff --git a/skills/dotnet-mcp-builder/references/mcp-apps.md b/skills/dotnet-mcp-builder/references/mcp-apps.md
new file mode 100644
index 000000000..bc5d09c92
--- /dev/null
+++ b/skills/dotnet-mcp-builder/references/mcp-apps.md
@@ -0,0 +1,220 @@
+# MCP Apps (interactive UI)
+
+[MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) is the official extension that lets a tool return an **interactive UI** rendered in a sandboxed iframe inside the host (Claude, Claude Desktop, VS Code Copilot, Goose, Postman, MCPJam). Typical use cases: charts, dashboards, multi-step forms, 3D viewers, real-time monitors, PDF/video viewers.
+
+> **Important:** as of early 2026, the C# SDK does **not** ship a typed convenience layer for MCP Apps (tracked in [csharp-sdk#1431](https://github.com/modelcontextprotocol/csharp-sdk/issues/1431)). You implement the spec by hand: serve a `ui://` resource and emit the right `_meta` on the tool. It's not hard — just untyped. This page shows you the pattern.
+
+## How it works (short version)
+
+1. You register a **resource** at a `ui://` URI returning an HTML bundle.
+2. You register a **tool** whose definition includes `_meta.ui.resourceUri` pointing to that URI.
+3. When the LLM calls the tool, the host fetches the UI resource and renders it in a sandboxed iframe in the chat.
+4. The HTML talks to the host over `postMessage` JSON-RPC (use `@modelcontextprotocol/ext-apps` from the bundle, or hand-roll it).
+5. The app can call back into your MCP server (any tool), update the model context, etc.
+
+The full protocol spec is at [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps).
+
+## Step 1: Serve the UI resource
+
+Bundle your HTML/JS/CSS into a single string (or load from `wwwroot`). Serve it at a `ui://` URI.
+
+```csharp
+using System.ComponentModel;
+using System.IO;
+using System.Reflection;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+
+[McpServerResourceType]
+public static class ChartUiResource
+{
+ [McpServerResource(
+ UriTemplate = "ui://charts/interactive",
+ Name = "Interactive chart",
+ MimeType = "text/html+skybridge")] // see "MIME type" note below
+ [Description("UI bundle for the interactive chart MCP App.")]
+ public static TextResourceContents GetUi()
+ {
+ // Load a bundled HTML/JS file from embedded resources or wwwroot.
+ var html = LoadEmbeddedString("MyMcpServer.AppUi.chart.html");
+
+ return new TextResourceContents
+ {
+ Uri = "ui://charts/interactive",
+ MimeType = "text/html+skybridge",
+ Text = html
+ };
+ }
+
+ private static string LoadEmbeddedString(string resourceName)
+ {
+ var asm = Assembly.GetExecutingAssembly();
+ using var stream = asm.GetManifestResourceStream(resourceName)
+ ?? throw new InvalidOperationException($"Missing embedded resource {resourceName}");
+ using var reader = new StreamReader(stream);
+ return reader.ReadToEnd();
+ }
+}
+```
+
+**MIME type note:** the spec uses `text/html+skybridge` for app HTML so hosts can distinguish UI bundles from regular `text/html` previews. Use that, even though plain `text/html` may work today on lenient hosts.
+
+## Step 2: Emit `_meta` on the tool
+
+The C# SDK's `[McpServerTool]` doesn't expose `_meta` in the attribute today, so set it via the lower-level `Tool` definition. Do this once at startup:
+
+```csharp
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+builder.Services.Configure(options =>
+{
+ options.Capabilities ??= new();
+ options.Capabilities.Tools ??= new();
+
+ // Define the tool manually so we can attach _meta.
+ var visualizeTool = new Tool
+ {
+ Name = "visualize_data",
+ Description = "Visualize the user's data as an interactive chart.",
+ InputSchema = JsonDocument.Parse("""
+ {
+ "type": "object",
+ "properties": {
+ "datasetId": { "type": "string", "description": "Dataset to visualize." }
+ },
+ "required": ["datasetId"]
+ }
+ """).RootElement,
+ Meta = new JsonObject
+ {
+ ["ui"] = new JsonObject
+ {
+ ["resourceUri"] = "ui://charts/interactive"
+ // Optionally:
+ // ["csp"] = new JsonObject { ["default-src"] = "'self' https://cdn.example.com" },
+ // ["permissions"] = new JsonArray("clipboard-write")
+ }
+ }
+ };
+
+ // Implement the call handler that returns the data the UI will render.
+ options.Capabilities.Tools.ToolCollection ??= new();
+ options.Capabilities.Tools.ToolCollection.Add(McpServerTool.Create(
+ async (CallToolRequestParams req, CancellationToken ct) =>
+ {
+ var args = req.Arguments ?? new();
+ var datasetId = args["datasetId"]!.GetValue();
+ var data = await LoadDataset(datasetId, ct);
+ return new CallToolResult
+ {
+ Content = [new TextContentBlock { Text = JsonSerializer.Serialize(data) }],
+ StructuredContent = JsonSerializer.SerializeToNode(data)
+ };
+ },
+ visualizeTool));
+});
+```
+
+If you don't need full structured content, the tool can return *just* JSON in a text block — the UI fetches it via `app.callServerTool(...)` after rendering.
+
+### Backwards compatibility key
+
+Some older hosts expect `_meta["ui/resourceUri"]` instead of `_meta.ui.resourceUri`. Set both for safety:
+
+```csharp
+Meta = new JsonObject
+{
+ ["ui"] = new JsonObject { ["resourceUri"] = "ui://charts/interactive" },
+ ["ui/resourceUri"] = "ui://charts/interactive" // legacy
+}
+```
+
+## Step 3: The HTML bundle
+
+A minimum viable bundle: vanilla JS using `@modelcontextprotocol/ext-apps`. The simplest build is a single self-contained HTML file.
+
+```html
+
+
+
+
+ Chart
+
+
+
+ Loading…
+
+
+
+```
+
+**Tip:** for non-trivial UIs, build with Vite (React/Vue/Svelte/Solid — any of the [official starter templates](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples)) and have the build emit a single inlined HTML you embed as a project resource.
+
+## Project layout
+
+A pragmatic layout for an MCP App in .NET:
+
+```
+MyMcpServer/
+├── Program.cs
+├── Tools/
+│ └── VisualizeDataTool.cs # (or registered via Configure as above)
+├── Resources/
+│ └── ChartUiResource.cs # serves the ui:// resource
+├── AppUi/
+│ ├── chart.html # bundled UI (Embedded Resource)
+│ └── package.json + src/... # if you build with Vite, output to chart.html
+└── MyMcpServer.csproj
+```
+
+In the csproj:
+
+```xml
+
+
+
+```
+
+Read it via `Assembly.GetManifestResourceStream("MyMcpServer.AppUi.chart.html")`.
+
+## Testing locally
+
+1. Run your MCP server (STDIO or HTTP).
+2. Use a host that supports MCP Apps — Claude Desktop or VS Code Copilot Chat are the easiest.
+3. Trigger the tool via the LLM. The UI renders inline.
+
+For pure-UI iteration, [MCP Inspector](https://github.com/modelcontextprotocol/inspector) shows resource contents but does not fully render apps; for that, point Claude Desktop at your dev server.
+
+## Pitfalls
+
+- **Wrong MIME type.** Use `text/html+skybridge`. Plain `text/html` may still work but isn't future-proof.
+- **CSP too tight or too loose.** If your UI loads from a CDN, declare it in `_meta.ui.csp`. Otherwise the iframe sandbox blocks it.
+- **Forgetting the `_meta` on the tool.** Without it, the host treats your tool as a regular text-returning tool. The UI never appears.
+- **Trying to use browser APIs outside the sandbox.** No cookies, no localStorage from the parent. Use `app.updateModelContext` and tool calls for state.
+
+## Future-proofing
+
+When the C# SDK ships its typed MCP Apps helpers (issue [#1431](https://github.com/modelcontextprotocol/csharp-sdk/issues/1431)), you'll likely be able to replace the manual `Configure` block with an attribute or fluent builder. The serving of `ui://` resources won't change. Keep your UI HTML as embedded resources so the migration is mechanical.
diff --git a/skills/dotnet-mcp-builder/references/packages.md b/skills/dotnet-mcp-builder/references/packages.md
new file mode 100644
index 000000000..ef0dc7e94
--- /dev/null
+++ b/skills/dotnet-mcp-builder/references/packages.md
@@ -0,0 +1,77 @@
+# NuGet packages and target frameworks
+
+## The three official packages
+
+All packages live under the [`ModelContextProtocol` NuGet profile](https://www.nuget.org/profiles/ModelContextProtocol). The official C# SDK repo is [`modelcontextprotocol/csharp-sdk`](https://github.com/modelcontextprotocol/csharp-sdk), maintained jointly by the MCP project and Microsoft.
+
+| Package | When to use it | Brings in |
+|---|---|---|
+| **`ModelContextProtocol`** | Default for STDIO servers and most projects | `Core` + `Microsoft.Extensions.Hosting` integration, attribute discovery (`AddMcpServer`, `WithToolsFromAssembly`, etc.) |
+| **`ModelContextProtocol.AspNetCore`** | HTTP (Streamable) servers hosted in ASP.NET Core | The above + `WithHttpTransport` and `MapMcp` |
+| **`ModelContextProtocol.Core`** | Pure clients, custom hosts, low-level scenarios where you don't want the `Microsoft.Extensions.*` dependencies | Just the protocol + transports + low-level `McpServer.Create` / `McpClient.CreateAsync` |
+
+**Rule of thumb:**
+- New STDIO server → `ModelContextProtocol` + `Microsoft.Extensions.Hosting`.
+- New HTTP server → `ModelContextProtocol.AspNetCore` only (it transitively pulls in everything you need).
+- Pure client app → `ModelContextProtocol.Core` (or `ModelContextProtocol` if you also want hosting/DI for the client).
+
+## Versions
+
+As of 2026, the stable line is **1.x** (`1.2.0` is current at time of writing). The `0.x` line was preview and has breaking differences — if you find docs or blog posts referencing `0.4`/`0.6`, treat them as out of date.
+
+To check the latest:
+
+```bash
+dotnet search ModelContextProtocol --prerelease
+```
+
+## Target frameworks
+
+The SDK targets **`.NET 8.0`** and **`netstandard2.0`**. That means it runs on:
+- .NET 8 (LTS)
+- .NET 9
+- .NET 10 (current LTS — recommended for new projects)
+- .NET Framework 4.6.2+ via netstandard2.0 (rare; only for legacy hosts)
+
+For HTTP servers you specifically need a TFM that supports ASP.NET Core (so .NET 8/9/10).
+
+## Project setup commands
+
+### STDIO server
+
+```bash
+dotnet new console -n MyMcpServer -f net10.0
+cd MyMcpServer
+dotnet add package ModelContextProtocol
+dotnet add package Microsoft.Extensions.Hosting
+```
+
+### HTTP (Streamable) server
+
+```bash
+dotnet new web -n MyMcpServer -f net10.0
+cd MyMcpServer
+dotnet add package ModelContextProtocol.AspNetCore
+```
+
+(`dotnet new web` gives you a minimal ASP.NET Core project — exactly what `MapMcp` needs.)
+
+### Client
+
+```bash
+dotnet new console -n MyMcpClient -f net10.0
+cd MyMcpClient
+dotnet add package ModelContextProtocol.Core
+```
+
+## Optional but commonly useful
+
+| Package | Why |
+|---|---|
+| `Microsoft.Extensions.AI` | Provides `IChatClient`, `ChatMessage`, `ChatRole`, `ChatOptions` — the abstractions used by `AsSamplingChatClient()` and by prompt return types. |
+| `Microsoft.Extensions.AI.Abstractions` | Pulled in transitively but worth knowing about for types like `DataContent`, `TextContent`. |
+| `OpenTelemetry.Extensions.Hosting` | The SDK emits OTel traces and metrics for tool calls — wire them up if the user has an observability story. |
+
+## What about `dnx`?
+
+Newer Microsoft examples sometimes show launching servers via `dnx PackageName --version 1.2.3`. That's a valid distribution model: publish your server as a NuGet package and let users run it without cloning. It's orthogonal to how the server itself is built — keep your code identical and just change the launch command.
diff --git a/skills/dotnet-mcp-builder/references/prompt-primitive.md b/skills/dotnet-mcp-builder/references/prompt-primitive.md
new file mode 100644
index 000000000..6b6d12204
--- /dev/null
+++ b/skills/dotnet-mcp-builder/references/prompt-primitive.md
@@ -0,0 +1,150 @@
+# Prompts
+
+Prompts are reusable, parameterised message templates that the user (not the LLM) typically picks from a list — think "slash commands" in a chat client. The server defines them; the host renders them as menus.
+
+## Anatomy of a prompt
+
+```csharp
+using System.ComponentModel;
+using Microsoft.Extensions.AI;
+using ModelContextProtocol.Server;
+
+[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) =>
+ [
+ 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.")
+ ];
+}
+```
+
+Register it:
+
+```csharp
+.WithPrompts()
+// or
+.WithPromptsFromAssembly()
+```
+
+## Return types
+
+| Return type | Result |
+|---|---|
+| `ChatMessage` | Single message. |
+| `IEnumerable` | Conversation seed. |
+| `PromptMessage` / `IEnumerable` | Lower-level — use when you need full control over content blocks (embedded resources, multiple typed blocks per message). |
+| `GetPromptResult` | Full control — set `Messages` and `Description`. |
+
+`ChatMessage`/`ChatRole` come from `Microsoft.Extensions.AI`. They're the high-level shape and what you should use 90% of the time. Drop down to `PromptMessage`/`ContentBlock` only when you need embedded resources or fine-grained content typing.
+
+## Arguments
+
+Every parameter (after the special ones the SDK strips out — `IMcpServer`, `CancellationToken`, etc.) becomes a prompt argument visible to the user when they pick the prompt. Use `[Description]` on each to explain what the user should supply.
+
+To mark an argument optional, give it a default value:
+
+```csharp
+[McpServerPrompt, Description("…")]
+public static ChatMessage Greeting(
+ [Description("Their preferred greeting style")] string style = "casual")
+ => new(ChatRole.User, $"Greet me in a {style} style.");
+```
+
+## Image and file content
+
+For prompts that include images:
+
+```csharp
+[McpServerPrompt, Description("Asks the model to analyze an image.")]
+public static IEnumerable AnalyzeImage(
+ [Description("Instructions for the analysis")] string instructions)
+{
+ byte[] imageBytes = LoadSampleImage();
+ return new[]
+ {
+ new ChatMessage(ChatRole.User, new AIContent[]
+ {
+ new TextContent($"Please analyze this image: {instructions}"),
+ new DataContent(imageBytes, "image/png")
+ })
+ };
+}
+```
+
+For embedded text resources (e.g. seeding the conversation with a document the user picked):
+
+```csharp
+[McpServerPrompt, Description("Reviews a referenced document.")]
+public static IEnumerable ReviewDocument(
+ [Description("The document ID to review")] string documentId)
+{
+ string content = LoadDocument(documentId);
+ return new[]
+ {
+ 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 = $"docs://documents/{documentId}",
+ MimeType = "text/plain",
+ Text = content
+ }
+ }
+ }
+ };
+}
+```
+
+## Async prompts
+
+Prompts can be async — useful when you need to look up data to build the messages:
+
+```csharp
+[McpServerPrompt, Description("Drafts a release-notes prompt.")]
+public static async Task> ReleaseNotes(
+ string repo,
+ string fromTag,
+ string toTag,
+ IGitHubClient github,
+ CancellationToken ct)
+{
+ var commits = await github.GetCommitsBetweenAsync(repo, fromTag, toTag, ct);
+ var summary = string.Join("\n", commits.Select(c => $"- {c.Message}"));
+ return new[]
+ {
+ new ChatMessage(ChatRole.User,
+ $"Draft release notes for {repo} {fromTag}→{toTag} from these commits:\n{summary}")
+ };
+}
+```
+
+## Notifying clients of prompt changes
+
+```csharp
+await server.SendNotificationAsync(
+ NotificationMethods.PromptListChangedNotification,
+ new PromptListChangedNotificationParams(),
+ cancellationToken);
+```
+
+## When to use prompts vs. tools
+
+- **Prompt:** the *user* triggers it from a menu, supplying any required arguments. The output is messages, not data. Good for "/summarize", "/code-review", "/draft-email".
+- **Tool:** the *LLM* triggers it (often without explicit user action) to fetch or change data. Good for "get_weather", "create_issue".
+
+If both apply (the user wants a slash command that triggers the same logic the LLM could call), expose both — the same DTO/service can back both.
diff --git a/skills/dotnet-mcp-builder/references/resource-primitive.md b/skills/dotnet-mcp-builder/references/resource-primitive.md
new file mode 100644
index 000000000..8bbe81022
--- /dev/null
+++ b/skills/dotnet-mcp-builder/references/resource-primitive.md
@@ -0,0 +1,183 @@
+# Resources
+
+Resources are server-exposed "things" identified by a URI. Hosts list them so the user can pick which ones to attach to the conversation; tools and prompts can also reference them via `EmbeddedResourceBlock`. Think files, database rows, API objects, settings — anything addressable.
+
+Two flavours:
+- **Static resource** — a fixed URI (`config://app/settings`). Useful for singletons.
+- **Resource template** — a URI with placeholders (`docs://articles/{id}`). The host (or LLM) substitutes parameters; your method receives them.
+
+## Static resource
+
+```csharp
+using System.ComponentModel;
+using System.Text.Json;
+using ModelContextProtocol.Server;
+
+[McpServerResourceType]
+public class AppResources
+{
+ [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" });
+}
+```
+
+Register:
+
+```csharp
+.WithResources()
+// or
+.WithResourcesFromAssembly()
+```
+
+## Templated resource
+
+The placeholders in `UriTemplate` map by name to method parameters. Anything not a placeholder follows the same DI rules as tools (`IMcpServer`, `CancellationToken`, services).
+
+```csharp
+[McpServerResourceType]
+public class DocumentResources
+{
+ [McpServerResource(
+ UriTemplate = "docs://articles/{id}",
+ Name = "Article",
+ MimeType = "text/markdown")]
+ [Description("Returns an article by its ID.")]
+ public static ResourceContents GetArticle(string id)
+ {
+ string? content = LoadArticle(id);
+ if (content is null)
+ throw new McpException($"Article not found: {id}");
+
+ return new TextResourceContents
+ {
+ Uri = $"docs://articles/{id}",
+ MimeType = "text/markdown",
+ Text = content
+ };
+ }
+}
+```
+
+## Return types
+
+| Return | Result |
+|---|---|
+| `string` | Wrapped in `TextResourceContents` with the URI from the template and the declared `MimeType`. |
+| `byte[]` | Wrapped in `BlobResourceContents`. |
+| `TextResourceContents` | Returned as-is — set `Uri`, `MimeType`, `Text`. |
+| `BlobResourceContents` | Returned as-is — use `BlobResourceContents.FromBytes(...)`. |
+| `IEnumerable` | Multi-part resource. |
+
+### Binary resource
+
+```csharp
+[McpServerResource(
+ UriTemplate = "images://photos/{id}",
+ Name = "Photo",
+ MimeType = "image/png")]
+public static BlobResourceContents GetPhoto(int id)
+{
+ byte[] data = LoadPhoto(id);
+ return BlobResourceContents.FromBytes(data, $"images://photos/{id}", "image/png");
+}
+```
+
+### Pointing at the file system
+
+A common pattern is exposing files from disk. Be careful about path traversal — never trust the URI verbatim.
+
+```csharp
+[McpServerResource(
+ UriTemplate = "file://workspace/{*relativePath}",
+ Name = "Workspace file")]
+public static TextResourceContents ReadFile(string relativePath, IOptions opts)
+{
+ var root = opts.Value.RootPath;
+ var fullPath = Path.GetFullPath(Path.Combine(root, relativePath));
+ if (!fullPath.StartsWith(root, StringComparison.Ordinal))
+ throw new McpException("Path traversal blocked.");
+
+ return new TextResourceContents
+ {
+ Uri = $"file://workspace/{relativePath.Replace("\\", "/")}",
+ MimeType = "text/plain",
+ Text = File.ReadAllText(fullPath)
+ };
+}
+```
+
+## Listing dynamic resources
+
+Attribute-based discovery covers the common case (one method per template). When you need to **enumerate** resources that don't fit a template — say, "list every file in the workspace" — implement a low-level handler in `McpServerOptions.Capabilities.Resources`:
+
+```csharp
+builder.Services.Configure(options =>
+{
+ options.Capabilities ??= new();
+ options.Capabilities.Resources ??= new();
+
+ options.Capabilities.Resources.ListResourcesHandler = (ctx, ct) =>
+ {
+ var resources = Directory
+ .EnumerateFiles(WorkspaceRoot, "*.*", SearchOption.AllDirectories)
+ .Select(path => new Resource
+ {
+ Uri = "file://workspace/" + Path.GetRelativePath(WorkspaceRoot, path).Replace('\\', '/'),
+ Name = Path.GetFileName(path),
+ MimeType = "text/plain"
+ })
+ .ToList();
+
+ return ValueTask.FromResult(new ListResourcesResult { Resources = resources });
+ };
+});
+```
+
+You can mix attribute-based and handler-based — the SDK merges both.
+
+## Resource subscriptions (server-pushed updates)
+
+If a client subscribes to a resource and it changes, push a notification:
+
+```csharp
+await server.SendNotificationAsync(
+ NotificationMethods.ResourceUpdatedNotification,
+ new ResourceUpdatedNotificationParams { Uri = "docs://articles/42" },
+ cancellationToken);
+```
+
+For wholesale list changes:
+
+```csharp
+await server.SendNotificationAsync(
+ NotificationMethods.ResourceListChangedNotification,
+ new ResourceListChangedNotificationParams(),
+ cancellationToken);
+```
+
+Both require a stateful transport.
+
+## Reading resources from a client
+
+```csharp
+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)
+ File.WriteAllBytes("out.bin", blob.DecodedData.ToArray());
+}
+```
+
+## Resources vs. tools — when to pick which
+
+- **Resource:** the user (or LLM) wants to *attach context* to the conversation. Read-only, addressable, listable. The host controls when/whether to load it. Ideal for documents, configs, schemas.
+- **Tool:** the LLM wants to *do something* (which may include reading data). Side-effects, actions, parameters that don't fit a URI.
+
+If you have something the LLM might want to *search* over, expose both: a `search_articles` tool and `docs://articles/{id}` resource template. The tool returns a list of URIs; the host fetches the content via the resource.
diff --git a/skills/dotnet-mcp-builder/references/roots.md b/skills/dotnet-mcp-builder/references/roots.md
new file mode 100644
index 000000000..7d01b77e2
--- /dev/null
+++ b/skills/dotnet-mcp-builder/references/roots.md
@@ -0,0 +1,109 @@
+# Roots
+
+Roots are filesystem (or URI) locations the **client** advertises to the server, scoping what the server is allowed to look at. Think "open workspace folders" in an IDE — the user has implicitly approved the server reading from these places. The server pulls the list when it needs it.
+
+## When you'd use roots
+
+- Building a tool that scans/edits the user's project. Use roots to know which directories are in scope.
+- Resolving relative paths in a way that respects the user's open workspace.
+- Restricting file access to the advertised roots (defence in depth).
+
+## Prerequisite
+
+Same as sampling/elicitation: server-to-client request → needs STDIO or stateful HTTP. Plus the client must advertise the `roots` capability.
+
+## Reading roots from a tool
+
+```csharp
+using System.ComponentModel;
+using System.Text;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+
+[McpServerToolType]
+public class WorkspaceTools
+{
+ [McpServerTool, Description("Lists the user's project roots.")]
+ public static async Task ListProjectRoots(
+ IMcpServer server,
+ CancellationToken cancellationToken)
+ {
+ if (server.ClientCapabilities?.Roots is null)
+ return "Client does not support roots.";
+
+ var result = await server.RequestRootsAsync(
+ new ListRootsRequestParams(),
+ cancellationToken);
+
+ var sb = new StringBuilder();
+ foreach (var root in result.Roots)
+ sb.AppendLine($"- {root.Name ?? root.Uri}: {root.Uri}");
+
+ return sb.ToString();
+ }
+}
+```
+
+`Root` has `Uri` (string, often a `file://...`) and optional `Name` (display label).
+
+## Reacting to root changes
+
+Clients send `notifications/roots/list_changed` when the user opens or closes a workspace folder. Subscribe:
+
+```csharp
+builder.Services.Configure(options =>
+{
+ options.Capabilities ??= new();
+
+ // The client tells us its roots changed; refresh whatever cache we have.
+ options.Capabilities.NotificationHandlers ??= [];
+ options.Capabilities.NotificationHandlers[NotificationMethods.RootsListChangedNotification] =
+ async (notification, ct) =>
+ {
+ // Trigger your refresh — typically pull RequestRootsAsync again.
+ };
+});
+```
+
+## A useful pattern: cache + refresh
+
+Roots don't change often, but refetching on every tool call is wasteful. Cache them per session and refresh on `roots/list_changed`:
+
+```csharp
+public class RootsCache
+{
+ private IReadOnlyList _roots = Array.Empty();
+
+ public IReadOnlyList Current => _roots;
+
+ public async Task RefreshAsync(IMcpServer server, CancellationToken ct)
+ {
+ if (server.ClientCapabilities?.Roots is null) return;
+ var result = await server.RequestRootsAsync(new ListRootsRequestParams(), ct);
+ _roots = result.Roots;
+ }
+}
+```
+
+Register as singleton (per-session in stateful HTTP, naturally singleton in STDIO).
+
+## Validating paths against roots
+
+Defence in depth: even if a tool argument *looks* like a path under a root, validate.
+
+```csharp
+public static bool IsUnderAnyRoot(string absolutePath, IReadOnlyList roots)
+{
+ foreach (var root in roots)
+ {
+ if (!Uri.TryCreate(root.Uri, UriKind.Absolute, out var uri)) continue;
+ if (!uri.IsFile) continue;
+ var rootPath = Path.GetFullPath(uri.LocalPath);
+ if (absolutePath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase))
+ return true;
+ }
+ return false;
+}
+```
+
+If a tool receives a path outside the advertised roots, refuse with a clear message — don't silently expand scope.
diff --git a/skills/dotnet-mcp-builder/references/sampling.md b/skills/dotnet-mcp-builder/references/sampling.md
new file mode 100644
index 000000000..148f2a610
--- /dev/null
+++ b/skills/dotnet-mcp-builder/references/sampling.md
@@ -0,0 +1,140 @@
+# Sampling
+
+Sampling lets a tool **call the LLM through the client** instead of bringing its own model. The server says "summarise this for me" and the client routes the request to whatever model the user has configured (Claude, GPT, local model, anything). Costs and rate limits live with the client, not the server.
+
+## When to use sampling
+
+- The tool needs an LLM step (summarise, classify, draft, extract) and you don't want to ship/configure your own model in the server.
+- You want to respect the user's model choice, key, and cost preferences.
+- You're building a "meta" tool that orchestrates LLM work as part of its job (e.g. multi-step agents).
+
+If you already have a deterministic algorithm, don't add a sampling call "for flavour" — it adds latency and cost.
+
+## Prerequisite: stateful transport
+
+Like elicitation, sampling needs the server to call back to the client. STDIO works always; HTTP needs `options.Stateless = false`.
+
+## Recommended: `IChatClient` adapter
+
+The cleanest API wraps the sampling channel as `Microsoft.Extensions.AI.IChatClient`, so you write code that looks like normal LLM-calling .NET:
+
+```csharp
+using System.ComponentModel;
+using Microsoft.Extensions.AI;
+using ModelContextProtocol.Server;
+
+[McpServerToolType]
+public class SummaryTools
+{
+ [McpServerTool(Name = "SummarizeContent"), Description("Summarises arbitrary text using the client's LLM.")]
+ public static async Task Summarize(
+ IMcpServer server,
+ [Description("The text to summarize")] string text,
+ CancellationToken cancellationToken)
+ {
+ ChatMessage[] messages =
+ [
+ new(ChatRole.User, "Briefly summarize the following content:"),
+ new(ChatRole.User, text),
+ ];
+
+ var options = new ChatOptions
+ {
+ MaxOutputTokens = 256,
+ Temperature = 0.3f,
+ };
+
+ var response = await server.AsSamplingChatClient()
+ .GetResponseAsync(messages, options, cancellationToken);
+
+ return $"Summary: {response}";
+ }
+}
+```
+
+Why this is nice:
+- Same `IChatClient` API the rest of the .NET AI ecosystem uses.
+- Works with `Microsoft.Extensions.AI` middleware (rate limiting, retries, telemetry, function calling).
+- You can swap to a direct provider in tests by injecting a different `IChatClient`.
+
+## Lower-level: `SampleAsync`
+
+When you need full control over the request shape:
+
+```csharp
+using ModelContextProtocol.Protocol;
+
+CreateMessageResult result = await server.SampleAsync(
+ new CreateMessageRequestParams
+ {
+ Messages =
+ [
+ new SamplingMessage
+ {
+ Role = Role.User,
+ Content = [new TextContentBlock { Text = "What is 2 + 2?" }]
+ }
+ ],
+ MaxTokens = 100,
+ Temperature = 0.0f,
+ SystemPrompt = "You are a precise calculator.",
+ // ModelPreferences, StopSequences, IncludeContext...
+ },
+ cancellationToken);
+
+string answer = result.Content
+ .OfType()
+ .FirstOrDefault()?.Text ?? string.Empty;
+```
+
+`ModelPreferences` lets you hint at model selection (cost vs. speed vs. intelligence priority); the *client* decides the actual model.
+
+```csharp
+ModelPreferences = new ModelPreferences
+{
+ Hints = [new ModelHint { Name = "claude" }], // soft preference
+ CostPriority = 0.2, // 0..1
+ SpeedPriority = 0.4,
+ IntelligencePriority = 0.9,
+}
+```
+
+## `IncludeContext`
+
+Sampling requests can ask the client to include context from the current conversation:
+
+```csharp
+IncludeContext = ContextInclusion.ThisServer // include this server's prior messages
+// or AllServers, or None (default)
+```
+
+Useful when you need the LLM to consider what's happened in the chat so far without you re-supplying it.
+
+## Capability check
+
+Always confirm the client supports sampling — many do not:
+
+```csharp
+if (server.ClientCapabilities?.Sampling is null)
+ throw new McpException(
+ "This client does not support sampling. " +
+ "Configure a model in the host or use a different MCP client.");
+```
+
+## Performance notes
+
+- Sampling calls are network round-trips (client → its provider → back). Expect 100ms–multiple seconds. Don't loop tightly.
+- Token costs are paid by the *user* (their API key/quota). Be conservative with `MaxTokens`.
+- Cancellation propagates: if the user kills the tool call, the sampling request is cancelled too.
+
+## Sampling vs. doing it server-side
+
+| Sampling (via client) | Direct LLM call (server-side) |
+|---|---|
+| Uses the user's model + key | Uses your service's key |
+| Respects user's policy/quota | Your responsibility to bill/track |
+| Works in any host the user has | Locked to the model you ship with |
+| Higher latency (extra hop) | Lower latency, direct |
+| No secrets to manage | You manage the API key |
+
+For "smart" servers shipped to many users, prefer sampling. For internal corporate servers where you want consistent behaviour and you're already paying for the model, direct is fine.
diff --git a/skills/dotnet-mcp-builder/references/server-features.md b/skills/dotnet-mcp-builder/references/server-features.md
new file mode 100644
index 000000000..ba31b2332
--- /dev/null
+++ b/skills/dotnet-mcp-builder/references/server-features.md
@@ -0,0 +1,200 @@
+# Other server features (completions, logging, progress, filters)
+
+A quick reference for the smaller MCP server features beyond the core primitives. Each section is short — load this file when one of these comes up.
+
+## Argument completions
+
+Completions let the host autocomplete prompt arguments and resource template parameters. The user starts typing; the client asks the server "what are valid values?".
+
+Implement via the low-level handler (no high-level attribute exists yet):
+
+```csharp
+builder.Services.Configure(options =>
+{
+ options.Capabilities ??= new();
+ options.Capabilities.Completions ??= new();
+
+ options.Capabilities.Completions.CompleteHandler = async (ctx, ct) =>
+ {
+ // ctx.Params.Ref tells us what they're completing (a prompt or resource).
+ // ctx.Params.Argument has the partial value typed so far.
+ var partial = ctx.Params.Argument.Value ?? "";
+ var matches = MyDataSource
+ .Where(x => x.StartsWith(partial, StringComparison.OrdinalIgnoreCase))
+ .Take(100)
+ .ToArray();
+
+ return new CompleteResult
+ {
+ Completion = new()
+ {
+ Values = matches,
+ HasMore = false,
+ Total = matches.Length
+ }
+ };
+ };
+});
+```
+
+Useful for: project IDs, file names, enum values that depend on dynamic data.
+
+## Logging
+
+Servers can emit log messages that hosts surface in their UI (and the LLM can sometimes see). Use the standard `ILogger` injected via DI — the SDK plumbs it through.
+
+```csharp
+public class WeatherTools
+{
+ private readonly ILogger _log;
+ public WeatherTools(ILogger log) => _log = log;
+
+ [McpServerTool, Description("…")]
+ public string GetWeather(string city)
+ {
+ _log.LogInformation("Looking up weather for {City}", city);
+ return "...";
+ }
+}
+```
+
+For STDIO servers, remember: console logging **must** go to stderr (`LogToStandardErrorThreshold = LogLevel.Trace`) — otherwise it corrupts the JSON-RPC stream. See [`transport-stdio.md`](./transport-stdio.md).
+
+To send a log specifically over the MCP channel (so the *host UI* sees it, not just your container logs):
+
+```csharp
+await server.SendNotificationAsync(
+ NotificationMethods.LoggingMessageNotification,
+ new LoggingMessageNotificationParams
+ {
+ Level = LoggingLevel.Info,
+ Logger = "weather",
+ Data = JsonSerializer.SerializeToElement(new { city, latency_ms = 123 })
+ },
+ ct);
+```
+
+The client may have set a `setLevel` filter — don't spam levels below it.
+
+## Progress notifications
+
+For long-running tools, send progress updates so the host can display a spinner with text:
+
+```csharp
+[McpServerTool, Description("Processes a large dataset.")]
+public static async Task Process(
+ IMcpServer server,
+ RequestContext ctx,
+ string datasetId,
+ CancellationToken ct)
+{
+ var progressToken = ctx.Params.Meta?.ProgressToken;
+
+ for (int i = 0; i < 100; i++)
+ {
+ await Task.Delay(50, ct);
+
+ if (progressToken is not null)
+ {
+ await server.SendNotificationAsync(
+ NotificationMethods.ProgressNotification,
+ new ProgressNotificationParams
+ {
+ ProgressToken = progressToken,
+ Progress = i + 1,
+ Total = 100,
+ Message = $"Processing item {i + 1} of 100"
+ },
+ ct);
+ }
+ }
+
+ return "Done.";
+}
+```
+
+Only send progress if the client passed a `progressToken` in the request meta — otherwise the host isn't listening.
+
+## Notification handlers (server-side)
+
+Servers can react to notifications the client sends:
+
+```csharp
+options.Capabilities ??= new();
+options.Capabilities.NotificationHandlers ??= [];
+
+options.Capabilities.NotificationHandlers[NotificationMethods.RootsListChangedNotification] =
+ async (notification, ct) =>
+ {
+ // Refresh root cache, etc.
+ };
+
+options.Capabilities.NotificationHandlers[NotificationMethods.CancelledNotification] =
+ async (notification, ct) =>
+ {
+ // The client cancelled a request; if you have side-effects in flight, abort them.
+ };
+```
+
+## Filters / middleware
+
+The SDK supports filters that wrap tool calls (think ASP.NET Core middleware for MCP). Use them for cross-cutting concerns: auth checks, telemetry, rate limiting, audit logging.
+
+```csharp
+builder.Services
+ .AddMcpServer()
+ .WithStdioServerTransport()
+ .WithToolsFromAssembly()
+ .WithCallToolFilter(async (ctx, next) =>
+ {
+ var sw = Stopwatch.StartNew();
+ try
+ {
+ return await next(ctx);
+ }
+ finally
+ {
+ sw.Stop();
+ ctx.Server.Services?
+ .GetRequiredService>()
+ .LogInformation("Tool {Tool} took {Ms}ms",
+ ctx.Params.Name, sw.ElapsedMilliseconds);
+ }
+ });
+```
+
+Similar `With*Filter` helpers exist for resources, prompts, and other capabilities — check the SDK API reference for the current set.
+
+## Server instructions (system-prompt-ish)
+
+You can supply instructions sent to the client at initialise time. Hosts may include them in the LLM's system prompt.
+
+```csharp
+builder.Services.AddMcpServer(options =>
+{
+ options.ServerInstructions =
+ "Use the booking tools to schedule meetings. " +
+ "Always confirm with the user before booking via elicitation.";
+});
+```
+
+Keep this short — every token here costs the user.
+
+## Capabilities advertising
+
+If you want to *not* advertise a capability you happen to have code for, you can mute it:
+
+```csharp
+builder.Services.AddMcpServer(options =>
+{
+ options.Capabilities = new()
+ {
+ Tools = new(), // advertise tools
+ Prompts = new(), // advertise prompts
+ Resources = null, // do NOT advertise resources, even if some are registered
+ Logging = new()
+ };
+});
+```
+
+By default, the SDK advertises everything you've registered — usually the right behaviour.
diff --git a/skills/dotnet-mcp-builder/references/testing.md b/skills/dotnet-mcp-builder/references/testing.md
new file mode 100644
index 000000000..b369b2e96
--- /dev/null
+++ b/skills/dotnet-mcp-builder/references/testing.md
@@ -0,0 +1,166 @@
+# Testing and local debugging
+
+Three workflows: interactive testing with MCP Inspector, in-process integration tests, and CI-friendly unit tests.
+
+## MCP Inspector (interactive)
+
+[MCP Inspector](https://github.com/modelcontextprotocol/inspector) is the go-to tool for trying out a server by hand. It launches your server, connects via STDIO or HTTP, and gives you a UI to list/call tools, view resources, fire elicitations, see logs, and inspect raw JSON-RPC frames.
+
+### STDIO
+
+```bash
+npx @modelcontextprotocol/inspector dotnet run --project ./MyMcpServer
+```
+
+Pass env vars or args after `--`:
+
+```bash
+npx @modelcontextprotocol/inspector \
+ dotnet run --project ./MyMcpServer -- \
+ --some-flag value
+```
+
+### HTTP
+
+Start the server normally (`dotnet run`), then in Inspector pick "Streamable HTTP" and enter the URL (e.g. `http://localhost:3001`).
+
+### Use it for
+
+- Verifying tool descriptions are clear (Inspector renders them like the LLM would consume them).
+- Walking through elicitation flows without a real LLM.
+- Capturing the exact JSON-RPC payloads when filing bug reports.
+
+## In-process integration tests (recommended)
+
+The cleanest test setup uses `InMemoryTransport` (or the lower-level `StreamServerTransport` / `StreamClientTransport`) to wire a real server and a real client together in the same process. No subprocesses, no network.
+
+```csharp
+using System.IO.Pipelines;
+using ModelContextProtocol;
+using ModelContextProtocol.Client;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+using Xunit;
+
+public class WeatherToolsTests
+{
+ [Fact]
+ public async Task GetWeather_returns_text()
+ {
+ var clientToServer = new Pipe();
+ var serverToClient = new Pipe();
+
+ await using var server = McpServer.Create(
+ new StreamServerTransport(
+ clientToServer.Reader.AsStream(),
+ serverToClient.Writer.AsStream()),
+ new McpServerOptions
+ {
+ ToolCollection =
+ [
+ McpServerTool.Create(
+ (string city) => $"{city}: 18°C",
+ new() { Name = "GetWeather" })
+ ]
+ });
+
+ var serverTask = server.RunAsync();
+
+ await using var client = await McpClient.CreateAsync(
+ new StreamClientTransport(
+ clientToServer.Writer.AsStream(),
+ serverToClient.Reader.AsStream()));
+
+ var tools = await client.ListToolsAsync();
+ var tool = tools.Single(t => t.Name == "GetWeather");
+
+ var result = await tool.CallAsync(new Dictionary
+ {
+ ["city"] = "Brussels"
+ });
+
+ Assert.False(result.IsError);
+ var text = result.Content.OfType().Single().Text;
+ Assert.Equal("Brussels: 18°C", text);
+ }
+}
+```
+
+This style lets you assert on the *exposed* behaviour (what a real client sees), not internal details.
+
+## Testing tools that use sampling/elicitation/roots
+
+Inject the MCP server, but supply mock client capabilities. With the in-memory pattern above, register handlers on the client:
+
+```csharp
+await using var client = await McpClient.CreateAsync(clientTransport, new McpClientOptions
+{
+ Capabilities = new()
+ {
+ Sampling = new()
+ {
+ SamplingHandler = (req, progress, ct) =>
+ Task.FromResult(new CreateMessageResult
+ {
+ Content = [new TextContentBlock { Text = "MOCK SUMMARY" }]
+ })
+ },
+ Elicitation = new()
+ {
+ ElicitationHandler = (req, ct) =>
+ Task.FromResult(new ElicitResult
+ {
+ Action = "accept",
+ Content = JsonSerializer.SerializeToNode(new { confirm = true })
+ .AsObject().ToDictionary(kv => kv.Key, kv => JsonDocument.Parse(kv.Value!.ToJsonString()).RootElement)
+ })
+ }
+ }
+});
+```
+
+Now your tool's `server.SampleAsync` / `server.ElicitAsync` calls hit deterministic mocks.
+
+## Unit tests at the DI layer
+
+For pure logic with no MCP-specific behaviour, just test the class:
+
+```csharp
+[Fact]
+public void Echo_prepends_hello()
+{
+ Assert.Equal("hello world", EchoTool.Echo("world"));
+}
+```
+
+The `[McpServerTool]` attribute doesn't affect runtime behaviour outside MCP wiring — your methods are just methods.
+
+## Running it from Claude Desktop / VS Code during development
+
+For end-to-end "feels-like-the-real-thing" testing:
+
+1. Run `dotnet publish -c Release` (or just `dotnet build` and use `dotnet run`).
+2. Point Claude Desktop / VS Code at the binary or `dotnet run --project ...`. See [`transport-stdio.md`](./transport-stdio.md) for the config snippets.
+3. Restart the host.
+4. Trigger the tool from chat.
+
+When iterating, set up `dotnet watch run --project ...` so the server restarts on edit; the host typically reconnects on the next tool call.
+
+## CI
+
+A typical CI pipeline:
+
+```yaml
+- run: dotnet restore
+- run: dotnet build --no-restore
+- run: dotnet test --no-build --logger "trx;LogFileName=test-results.trx"
+```
+
+Nothing MCP-specific. The in-memory transport tests run anywhere `dotnet test` runs — no Node, no Docker.
+
+## Common diagnostic tricks
+
+- **"Tool isn't showing up":** call `client.ListToolsAsync()` in a quick test and dump the names. If your tool isn't there, the registration is wrong.
+- **"LLM keeps misusing the tool":** open Inspector and look at the schema/description as the LLM sees it. Most "the model is dumb" issues are actually missing `[Description]`.
+- **"Sampling/elicitation throws 'method not supported'":** the client doesn't advertise the capability. Either you're testing against a host that doesn't support it (Inspector supports both), or your in-memory client is missing the handler.
+- **"HTTP returns 404 for /":** check `app.MapMcp()` is called *and* you're hitting the right path. `MapMcp("/mcp")` means the URL is `http://host/mcp`, not `http://host/`.
diff --git a/skills/dotnet-mcp-builder/references/tool-primitive.md b/skills/dotnet-mcp-builder/references/tool-primitive.md
new file mode 100644
index 000000000..23c7043bb
--- /dev/null
+++ b/skills/dotnet-mcp-builder/references/tool-primitive.md
@@ -0,0 +1,225 @@
+# Tools
+
+Tools are functions the LLM can call. In the C# SDK they're plain methods on a class marked `[McpServerToolType]`, with each method marked `[McpServerTool]`. The SDK generates the JSON Schema from the method signature and `[Description]` attributes.
+
+## Anatomy of a tool
+
+```csharp
+using System.ComponentModel;
+using ModelContextProtocol.Server;
+
+[McpServerToolType]
+public class WeatherTools
+{
+ // Static or instance — both work. Instance methods get DI for the containing class.
+ [McpServerTool, Description("Returns the current weather for a city.")]
+ public static string GetWeather(
+ [Description("City name, e.g. 'Brussels'")] string city,
+ [Description("Units: 'celsius' or 'fahrenheit'")] string units = "celsius")
+ {
+ return $"{city}: 18°{units[0]}";
+ }
+}
+```
+
+Register it (one of):
+
+```csharp
+.WithToolsFromAssembly() // discovers all [McpServerToolType] in the calling assembly
+.WithTools() // explicit, single class
+```
+
+The tool name shown to the LLM is `GetWeather` (PascalCase converted to snake_case is **not** automatic — what you see is what you get unless you set `Name` explicitly).
+
+## Attribute options
+
+```csharp
+[McpServerTool(
+ Name = "get_weather", // override the tool name
+ Title = "Get current weather", // human-readable display name
+ Destructive = false, // hint: tool modifies state irreversibly
+ Idempotent = true, // hint: same args ⇒ same result
+ OpenWorld = true, // hint: interacts with external systems
+ ReadOnly = true // hint: doesn't mutate any state
+)]
+[Description("Returns the current weather for a city.")]
+public static string GetWeather(...) { ... }
+```
+
+The behaviour hints (`Destructive`, `Idempotent`, `OpenWorld`, `ReadOnly`) are advisory — clients use them to decide things like auto-approval. They don't change runtime behaviour.
+
+## Async, cancellation, DI
+
+```csharp
+[McpServerTool, Description("Fetches the latest commits for a repo.")]
+public async Task> GetCommits(
+ string owner,
+ string repo,
+ IGitHubClient github, // injected from DI
+ CancellationToken cancellationToken) // injected by the SDK
+{
+ return await github.GetCommitsAsync(owner, repo, cancellationToken);
+}
+```
+
+The SDK recognises and special-cases these parameter types — they don't appear in the tool schema:
+- `IMcpServer` / `McpServer` — the current server (used for `ElicitAsync`, `SampleAsync`, `RequestRootsAsync`, sending notifications).
+- `CancellationToken` — propagated from the JSON-RPC request.
+- `RequestContext` — full request context if you need it.
+- `IServiceProvider` — request-scoped service provider.
+- Anything resolvable from DI that the SDK can recognise as not a primitive payload.
+
+Everything else is treated as a JSON-RPC argument and goes into the schema.
+
+## Return types
+
+The SDK serialises whatever you return into the appropriate content blocks. Practical guidance:
+
+| Return type | What the LLM sees |
+|---|---|
+| `string` | Single text content block. |
+| `int`, `bool`, `double`, etc. | Stringified into a text content block. |
+| Any DTO (record/class) | Serialized to JSON in a text content block, plus structured content for clients that support it. |
+| `IEnumerable` of DTOs | JSON array. |
+| `ContentBlock` / `ImageContentBlock` / `AudioContentBlock` / `EmbeddedResourceBlock` | That single block, untouched. |
+| `IEnumerable` | Multiple blocks in order. |
+| `CallToolResult` | Full control — set `Content`, `StructuredContent`, `IsError`. |
+
+### Returning structured data the LLM can act on
+
+```csharp
+public record Forecast(string City, double TempC, string Conditions);
+
+[McpServerTool, Description("Returns a 3-day forecast.")]
+public static Forecast[] GetForecast(string city) =>
+ new[]
+ {
+ new Forecast(city, 18.0, "sunny"),
+ new Forecast(city, 16.5, "cloudy"),
+ new Forecast(city, 14.2, "rain"),
+ };
+```
+
+The SDK emits the array as both a JSON text block (for older clients) and `structuredContent` (for newer ones), and infers an output schema from `Forecast`.
+
+### Returning images / audio
+
+```csharp
+[McpServerTool, Description("Generates a chart and returns it as a PNG.")]
+public static ImageContentBlock RenderChart(string title)
+{
+ byte[] png = Renderer.Render(title);
+ return ImageContentBlock.FromBytes(png, "image/png");
+}
+
+[McpServerTool, Description("Synthesises speech.")]
+public static AudioContentBlock Speak(string text)
+{
+ byte[] wav = Tts.Synthesize(text);
+ return AudioContentBlock.FromBytes(wav, "audio/wav");
+}
+```
+
+### Mixing content blocks
+
+```csharp
+[McpServerTool, Description("Returns the chart and a caption.")]
+public static IEnumerable RenderAnnotatedChart(string title)
+{
+ byte[] png = Renderer.Render(title);
+ return new ContentBlock[]
+ {
+ new TextContentBlock { Text = $"Chart for: {title}" },
+ ImageContentBlock.FromBytes(png, "image/png"),
+ new TextContentBlock { Text = "Generated at " + DateTime.UtcNow.ToString("u") }
+ };
+}
+```
+
+### Returning an embedded resource
+
+Useful when the tool result *is* a document the user might want to reuse:
+
+```csharp
+[McpServerTool, Description("Looks up a contract.")]
+public static EmbeddedResourceBlock GetContract(string id)
+{
+ return new EmbeddedResourceBlock
+ {
+ Resource = new TextResourceContents
+ {
+ Uri = $"contracts://{id}",
+ MimeType = "text/markdown",
+ Text = LoadContract(id)
+ }
+ };
+}
+```
+
+## Errors
+
+There are two flavours of error a tool can produce:
+
+### Tool-level errors (the LLM can read and recover from these)
+
+Throw any exception — the SDK catches it and returns a `CallToolResult` with `IsError = true` and the exception message in a text block:
+
+```csharp
+[McpServerTool, Description("Divides a by b.")]
+public static double Divide(double a, double b)
+{
+ if (b == 0)
+ throw new ArgumentException("Cannot divide by zero.");
+ return a / b;
+}
+```
+
+You can also build the result explicitly:
+
+```csharp
+[McpServerTool, Description("…")]
+public static CallToolResult Foo(...)
+{
+ return new CallToolResult
+ {
+ IsError = true,
+ Content = [new TextContentBlock { Text = "Detailed error explanation for the LLM." }]
+ };
+}
+```
+
+### Protocol-level errors (the call is rejected before the LLM sees a result)
+
+Use `McpException` (or `McpProtocolException` with an explicit error code) for things like bad arguments:
+
+```csharp
+[McpServerTool, Description("…")]
+public static string Process(string input)
+{
+ if (string.IsNullOrWhiteSpace(input))
+ throw new McpProtocolException("Missing required input", McpErrorCode.InvalidParams);
+ return $"Processed: {input}";
+}
+```
+
+**Heuristic:** if the LLM should *try again* with different arguments, throw a regular exception so it gets a tool error. If the call is malformed in a way the LLM can't fix, throw `McpProtocolException`.
+
+## Notifying clients of tool list changes
+
+If your tools come and go at runtime (e.g. plugin loaded, user logged in), notify the client:
+
+```csharp
+await server.SendNotificationAsync(
+ NotificationMethods.ToolListChangedNotification,
+ new ToolListChangedNotificationParams(),
+ cancellationToken);
+```
+
+Requires a stateful transport (STDIO or stateful HTTP).
+
+## Common pitfalls
+
+- **Forgetting `[McpServerToolType]` on the class.** The method-level `[McpServerTool]` alone won't be discovered by `WithToolsFromAssembly`.
+- **Vague descriptions.** `[Description("Gets data")]` makes the LLM guess. Spend a sentence describing what the tool does, when to call it, and what it returns.
+- **Big payloads.** Tools that return megabytes of JSON eat the model's context. Trim or paginate. For binary blobs, return an `EmbeddedResourceBlock` so the host can decide how to render it.
+- **Hiding errors.** Returning `"failed"` as a string looks like success to the SDK. Throw the exception or set `IsError = true`.
diff --git a/skills/dotnet-mcp-builder/references/transport-http.md b/skills/dotnet-mcp-builder/references/transport-http.md
new file mode 100644
index 000000000..1f68e5505
--- /dev/null
+++ b/skills/dotnet-mcp-builder/references/transport-http.md
@@ -0,0 +1,189 @@
+# Streamable HTTP transport (ASP.NET Core)
+
+Streamable HTTP is the modern remote transport. A single endpoint accepts JSON-RPC over HTTP POST and (optionally) streams responses back as Server-Sent Events when the server has more than one message to send.
+
+> **SSE-only is deprecated.** The legacy "HTTP+SSE" transport (separate POST endpoint + GET SSE endpoint) is gone from new clients. Use Streamable HTTP. Only enable legacy SSE (`EnableLegacySse = true`) if you must support a known-old client, and document why.
+
+## When to choose HTTP
+
+- Multi-tenant or remote-hosted server.
+- Auth via OAuth / API gateway in front.
+- Horizontally scaled deployments (with `Stateless = true`).
+- Containers, Azure Container Apps, Kubernetes, etc.
+
+For local single-user scenarios, [STDIO](./transport-stdio.md) is simpler.
+
+## Minimal server
+
+```bash
+dotnet new web -n MyHttpServer -f net10.0
+cd MyHttpServer
+dotnet add package ModelContextProtocol.AspNetCore
+```
+
+```csharp
+// Program.cs
+using ModelContextProtocol.Server;
+using System.ComponentModel;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services
+ .AddMcpServer()
+ .WithHttpTransport(options =>
+ {
+ // Stateless = true: each request is independent, no Mcp-Session-Id tracking.
+ // Required for horizontal scaling without sticky sessions.
+ // Disables server-to-client features (sampling, elicitation, roots, unsolicited notifications).
+ options.Stateless = true;
+ })
+ .WithToolsFromAssembly();
+
+var app = builder.Build();
+
+app.MapMcp(); // mounts the MCP endpoints at "/"
+// app.MapMcp("/mcp"); // or under a path prefix
+
+app.Run("http://localhost:3001");
+
+[McpServerToolType]
+public static class EchoTool
+{
+ [McpServerTool, Description("Echoes the message back to the client.")]
+ public static string Echo(string message) => $"hello {message}";
+}
+```
+
+## Stateless vs. stateful — the most important decision
+
+| Mode | `options.Stateless` | Behaviour | Use when |
+|---|---|---|---|
+| **Stateless** | `true` | No `Mcp-Session-Id`. Each POST is independent. | Horizontal scaling, simple tool servers, no server-initiated traffic. |
+| **Stateful** | `false` (default) | Server assigns and tracks `Mcp-Session-Id`. Long-lived session. | You need elicitation, sampling, roots, log notifications, or anything that pushes from server to client. Requires session affinity at the load balancer. |
+
+**Rule:** if the user wants any of `ElicitAsync`, `SampleAsync`, `RequestRootsAsync`, or to push log/notification messages, **do not** set `Stateless = true`. The calls will fail at runtime with no transport to deliver them on.
+
+## Endpoint shape
+
+`MapMcp(pattern = "")` creates a route group at `pattern` and maps:
+- **POST** — accepts JSON-RPC requests/responses/notifications. Returns either a JSON response or an SSE stream depending on `Accept` header and whether multiple messages need to flow back.
+- **GET** — used by stateful sessions for the server-to-client SSE channel.
+- **DELETE** — terminates a stateful session.
+
+Default pattern is the root (`/`). To put MCP under `/mcp/v1`:
+
+```csharp
+app.MapMcp("/mcp/v1");
+```
+
+Match this on the client side (`Endpoint = new Uri("https://host/mcp/v1")`).
+
+## Per-session configuration (HttpContext access)
+
+When you need to vary server behaviour per HTTP request (auth, tenant, headers), use the `ConfigureSessionOptions` callback:
+
+```csharp
+builder.Services
+ .AddMcpServer()
+ .WithHttpTransport(options =>
+ {
+ options.ConfigureSessionOptions = async (httpContext, mcpOptions, ct) =>
+ {
+ var tenantId = httpContext.Request.Headers["X-Tenant"].ToString();
+ mcpOptions.ServerInstructions = $"Tenant: {tenantId}";
+ // mutate any McpServerOptions fields per-session
+ };
+ });
+```
+
+Inside a tool, you can also inject `IHttpContextAccessor` if `AddHttpContextAccessor()` is registered. See the [`AspNetCoreMcpPerSessionTools` sample](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/AspNetCoreMcpPerSessionTools).
+
+## Authentication
+
+The MCP endpoint is just an ASP.NET Core endpoint — apply standard middleware:
+
+```csharp
+builder.Services
+ .AddAuthentication("Bearer")
+ .AddJwtBearer(/* configure */);
+builder.Services.AddAuthorization();
+
+var app = builder.Build();
+
+app.UseAuthentication();
+app.UseAuthorization();
+
+app.MapMcp().RequireAuthorization(); // protect the endpoint
+```
+
+For OAuth flows where the *MCP server* is the resource server, follow the [MCP authorization spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization). The [`ProtectedMcpServer` sample](https://github.com/modelcontextprotocol/csharp-sdk/tree/main/samples/ProtectedMcpServer) shows a working setup with discovery endpoints.
+
+For machine-to-machine, an API key middleware is fine:
+
+```csharp
+app.Use(async (ctx, next) =>
+{
+ if (ctx.Request.Headers["X-Api-Key"] != Configuration["ApiKey"])
+ {
+ ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
+ return;
+ }
+ await next();
+});
+```
+
+## CORS (when the client is in-browser)
+
+```csharp
+builder.Services.AddCors(o => o.AddDefaultPolicy(p =>
+ p.WithOrigins("https://my-host.example.com")
+ .AllowAnyHeader()
+ .AllowAnyMethod()
+ .AllowCredentials()));
+// ...
+app.UseCors();
+app.MapMcp();
+```
+
+## Health checks and observability
+
+Add the standard ASP.NET Core probes; the MCP endpoint shouldn't be the liveness check.
+
+```csharp
+builder.Services.AddHealthChecks();
+// ...
+app.MapHealthChecks("/healthz");
+```
+
+The SDK emits OpenTelemetry traces (`Activity` per tool call) and metrics. Wire them up if the user has an OTel pipeline:
+
+```csharp
+builder.Services
+ .AddOpenTelemetry()
+ .WithTracing(t => t.AddSource("ModelContextProtocol").AddOtlpExporter())
+ .WithMetrics(m => m.AddMeter("ModelContextProtocol").AddOtlpExporter());
+```
+
+## Deployment notes
+
+- **Containerise normally.** No special MCP-specific Dockerfile — it's just an ASP.NET Core app.
+- **Behind a reverse proxy** (nginx, Azure Front Door, AWS ALB), make sure SSE buffering is **disabled** for the MCP path. nginx: `proxy_buffering off;`. Without this, streaming responses are batched into one slow blob.
+- **Timeouts.** The client may keep an SSE connection open for a long time. Set proxy idle timeout high (e.g. 5+ minutes) for stateful deployments; less critical for stateless.
+- **Azure Container Apps / App Service** work out of the box; both support long-lived HTTP responses.
+
+## Enabling legacy SSE (compatibility only)
+
+```csharp
+builder.Services
+ .AddMcpServer()
+ .WithHttpTransport(options =>
+ {
+ options.EnableLegacySse = true;
+#pragma warning disable MCP9004
+ options.Stateless = false; // SSE requires stateful mode
+#pragma warning restore MCP9004
+ })
+ .WithToolsFromAssembly();
+```
+
+Only do this if the user has a documented client that hasn't migrated. New deployments should not enable it.
diff --git a/skills/dotnet-mcp-builder/references/transport-stdio.md b/skills/dotnet-mcp-builder/references/transport-stdio.md
new file mode 100644
index 000000000..186a8425f
--- /dev/null
+++ b/skills/dotnet-mcp-builder/references/transport-stdio.md
@@ -0,0 +1,162 @@
+# STDIO transport
+
+STDIO is the right choice when the server runs as a child process of the client (Claude Desktop, VS Code, MCP Inspector, a custom CLI). The client launches your executable; you read JSON-RPC frames from stdin and write them to stdout.
+
+## When to choose STDIO
+
+- Local-first server (file-system access, dev tools, CLI integrations).
+- Distributing as a single executable or a `dnx`-runnable NuGet package.
+- You want the simplest possible deployment story (no network, no auth).
+- You need server-to-client features (sampling, elicitation, roots) — STDIO always supports them, no `Stateless` flag to worry about.
+
+If the user wants a remote/multi-tenant server, use [HTTP Streamable](./transport-http.md) instead.
+
+## Minimal server
+
+```bash
+dotnet new console -n MyStdioServer -f net10.0
+cd MyStdioServer
+dotnet add package ModelContextProtocol
+dotnet add package Microsoft.Extensions.Hosting
+```
+
+```csharp
+// Program.cs
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using ModelContextProtocol.Server;
+using System.ComponentModel;
+
+var builder = Host.CreateApplicationBuilder(args);
+
+// CRITICAL: stdout is the JSON-RPC channel. Send all logs to stderr.
+builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
+
+builder.Services
+ .AddMcpServer()
+ .WithStdioServerTransport()
+ .WithToolsFromAssembly();
+
+await builder.Build().RunAsync();
+
+[McpServerToolType]
+public static class EchoTool
+{
+ [McpServerTool, Description("Echoes the message back to the client.")]
+ public static string Echo(string message) => $"hello {message}";
+}
+```
+
+## The stdout/stderr trap
+
+The single most common bug in STDIO servers is something writing to stdout that isn't a JSON-RPC frame. The client will then drop the connection with a parse error.
+
+**Things that silently break STDIO:**
+- `Console.WriteLine(...)` anywhere in your code.
+- A logger configured with the default console sink (writes to stdout).
+- `Trace.WriteLine(...)` if a default trace listener is attached.
+- Third-party libraries that print banners on startup.
+
+**Defensive checklist:**
+1. Configure logging to stderr **before** anything else (the snippet above does this).
+2. Don't `Console.Write*` from tools or startup code. Use `ILogger` injected into the tool class.
+3. If a dependency is noisy, redirect its logs through `ILogger` or suppress them at startup.
+
+## Server identity
+
+The SDK sends `serverInfo` (name + version) in the `initialize` response. By default it derives them from your assembly. To override:
+
+```csharp
+builder.Services
+ .AddMcpServer(options =>
+ {
+ options.ServerInfo = new()
+ {
+ Name = "my-stdio-server",
+ Version = "1.0.0",
+ Title = "My STDIO MCP Server" // optional human-readable name
+ };
+ })
+ .WithStdioServerTransport()
+ .WithToolsFromAssembly();
+```
+
+## Reading args/env from the client
+
+Clients (e.g. Claude Desktop config) typically launch your server with arguments and environment variables. Read them like any other .NET app:
+
+```csharp
+string apiKey = Environment.GetEnvironmentVariable("MY_API_KEY")
+ ?? throw new InvalidOperationException("MY_API_KEY not set");
+
+string configPath = args.ElementAtOrDefault(0)
+ ?? Path.Combine(Environment.CurrentDirectory, "config.json");
+```
+
+Document the expected vars/args in the README so users know what to put in their client config.
+
+## Wiring to Claude Desktop
+
+In `claude_desktop_config.json`:
+
+```json
+{
+ "mcpServers": {
+ "my-server": {
+ "command": "dotnet",
+ "args": ["run", "--project", "C:/path/to/MyStdioServer"],
+ "env": {
+ "MY_API_KEY": "..."
+ }
+ }
+ }
+}
+```
+
+For a published self-contained executable, replace `command`/`args` with the executable path. For a NuGet-distributed server using `dnx`:
+
+```json
+"command": "dnx",
+"args": ["MyMcpServer", "--version", "1.2.3"]
+```
+
+## Wiring to VS Code (GitHub Copilot Chat)
+
+In `.vscode/mcp.json`:
+
+```json
+{
+ "servers": {
+ "my-server": {
+ "type": "stdio",
+ "command": "dotnet",
+ "args": ["run", "--project", "${workspaceFolder}/src/MyMcpServer"]
+ }
+ }
+}
+```
+
+## Local debugging
+
+The cleanest workflow is [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
+
+```bash
+npx @modelcontextprotocol/inspector dotnet run --project ./MyStdioServer
+```
+
+Inspector launches your server, opens a UI, and lets you call tools / list resources / fire elicitations interactively. See [`testing.md`](./testing.md) for more.
+
+## Graceful shutdown
+
+`builder.Build().RunAsync()` already handles SIGINT/SIGTERM. If you have background work to flush, use `IHostApplicationLifetime`:
+
+```csharp
+var host = builder.Build();
+var lifetime = host.Services.GetRequiredService();
+lifetime.ApplicationStopping.Register(() =>
+{
+ // flush, close handles, etc. — keep it fast (<5s) so the client doesn't hang.
+});
+await host.RunAsync();
+```