dotnet-mcp
Build or consume Model Context Protocol (MCP) servers and clients in .NET using the official MCP C# SDK, including stdio, Streamable HTTP, tools, prompts, resources, and capability negotiation.
Install
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-mcp
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
MCP C# SDK for .NET
Trigger On
- building or consuming MCP servers from a .NET application or library
- choosing between stdio and HTTP transport for MCP
- exposing tools, resources, prompts, completions, or logging to an MCP host
- connecting a .NET app to an existing MCP server and passing discovered tools into
IChatClient - bootstrapping a minimal MCP client/server from the
.NET AIquickstarts or publishing a server to the MCP Registry - implementing capability-aware flows such as roots, sampling, elicitation, subscriptions, or session resumption
Use This Skill Instead Of
- Use
dotnet-mcpwhen protocol interoperability is the requirement. - Use
dotnet-microsoft-extensions-aiwhen you only need model/provider abstraction or local tool orchestration without the MCP wire protocol. - Use
dotnet-microsoft-agent-frameworkwhen the main problem is agent orchestration; combine it withdotnet-mcponly when those agents must consume or expose MCP endpoints. - Use the
.NET AIquickstarts for the very first vertical slice, then come back here to harden transport, capability negotiation, publishing, and host interoperability.
Documentation
- MCP C# SDK overview
- Getting Started
- API reference
- Conceptual docs
- Versioning policy
- Experimental APIs
- MCP C# SDK repository
- Model Context Protocol specification
References
Load only what the task needs:
references/patterns.md- current server/client patterns, transports, capabilities, filters, and chat-client integrationreferences/security.md- safe error handling, auth boundaries, stdio logging hygiene, and defensive tool/resource patterns
Package Selection
| Package | Choose when |
|---|---|
ModelContextProtocol.Core |
You only need a client or low-level server APIs and want the smallest dependency set. |
ModelContextProtocol |
You want the main SDK package with hosting, DI, attribute discovery, and stdio server support. Start here for most projects. |
ModelContextProtocol.AspNetCore |
You are hosting a remote MCP server in ASP.NET Core over HTTP. This includes the main package. |
Transport Selection
| Transport | Use when | Notes |
|---|---|---|
StdioClientTransport / WithStdioServerTransport() |
The MCP server should run as a local child process. | Best for local tooling and editor/agent integrations. |
HttpClientTransport + HttpTransportMode.StreamableHttp |
The server is remote or should be reachable over HTTP. | Recommended HTTP transport; supports streaming and session resumption. |
HttpTransportMode.Sse |
You must connect to an older SSE-only server. | Legacy compatibility only; do not choose this for new servers. |
flowchart LR
A["Need MCP interoperability in .NET"] --> B{"Role?"}
B -->|"Expose MCP surface"| C{"Where will it run?"}
B -->|"Consume an MCP server"| D{"Transport?"}
C -->|"Local child process"| E["ModelContextProtocol\nAddMcpServer()\nWithStdioServerTransport()"]
C -->|"Remote HTTP endpoint"| F["ModelContextProtocol.AspNetCore\nAddMcpServer()\nWithHttpTransport()\nMapMcp()"]
D -->|"stdio"| G["StdioClientTransport\nMcpClient.CreateAsync()"]
D -->|"HTTP"| H["HttpClientTransport\nAutoDetect or StreamableHttp"]
E --> I["Register tools/resources/prompts"]
F --> I
G --> J["Check ServerCapabilities\nbefore optional features"]
H --> J
Workflow
Pick the package and transport first.
- Local child-process server:
ModelContextProtocol+WithStdioServerTransport(). - Remote server:
ModelContextProtocol.AspNetCore+WithHttpTransport()+MapMcp(). - Client-only app: start with
ModelContextProtocolorModelContextProtocol.Core. - Registry distribution: pair a minimal server with the MCP Registry publishing flow only after the server contract is stable.
- Local child-process server:
Model the MCP surface explicitly.
- Tools:
[McpServerToolType]+[McpServerTool] - Resources:
[McpServerResourceType]+[McpServerResource] - Prompts:
[McpServerPromptType]+[McpServerPrompt] - Use custom handlers or filters only for cross-cutting behavior, protocol extensions, or advanced routing.
- Tools:
Prefer attribute discovery for straightforward servers.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using System.ComponentModel;
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(options =>
{
options.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}";
}
- For HTTP servers, use the ASP.NET Core transport and map the endpoint directly.
using ModelContextProtocol.Server;
using System.ComponentModel;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddMcpServer()
.WithHttpTransport()
.WithToolsFromAssembly();
var app = builder.Build();
app.MapMcp("/mcp");
app.Run();
[McpServerToolType]
public static class EchoTool
{
[McpServerTool, Description("Echoes the message back to the client.")]
public static string Echo(string message) => $"hello {message}";
}
- When consuming a server, use
McpClient.CreateAsync(...)and stay capability-aware.
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
var transport = new StdioClientTransport(new StdioClientTransportOptions
{
Name = "Everything",
Command = "npx",
Arguments = ["-y", "@modelcontextprotocol/server-everything"],
});
await using var client = await McpClient.CreateAsync(transport);
IList<McpClientTool> tools = await client.ListToolsAsync();
if (client.ServerCapabilities.Prompts is not null)
{
var prompts = await client.ListPromptsAsync();
}
Treat optional features as negotiated capabilities, not assumptions.
- Client capabilities: configure
McpClientOptions.Capabilitiesfor roots, sampling, and elicitation. - Server capabilities are inferred from registered features.
- Check
client.ServerCapabilitiesbefore using completions, logging, prompt list-change notifications, or resource subscriptions. - Use
client.NegotiatedProtocolVersionorserver.NegotiatedProtocolVersiononly when version-specific behavior matters.
- Client capabilities: configure
Keep HTTP guidance current.
- Streamable HTTP is the recommended transport for remote servers.
MapMcp()also serves SSE compatibility endpoints for older clients.- HTTP clients can use
AutoDetectby default, or forceStreamableHttp/Sse. - Session resumption is available for Streamable HTTP through
McpClient.ResumeSessionAsync(...).
Treat the
.NET AIMCP quickstarts as bootstrap examples.build-mcp-clientandbuild-mcp-serverare good starting points when the surrounding app is still MEAI-centric.publish-mcp-registryis the distribution step, not the design step. Stabilize the protocol surface before publishing.
Respect current error and serialization rules.
- Tool exceptions normally come back as
CallToolResult.IsError == true. - Throw
McpProtocolExceptiononly for protocol-level JSON-RPC failures. McpClientToolinherits fromAIFunction, so discovered tools can be passed directly intoIChatClient.- Experimental APIs use
MCPEXP...diagnostics; suppress them intentionally, not globally by accident. - If you use a custom
JsonSerializerContext, prependMcpJsonUtilities.DefaultOptions.TypeInfoResolverso MCP protocol types keep the SDK's contract.
- Tool exceptions normally come back as
Anti-Patterns To Avoid
| Anti-pattern | Why it causes trouble | Better approach |
|---|---|---|
| Picking HTTP transport for a purely local child-process scenario | Adds unnecessary hosting, auth, and deployment surface | Use stdio for local/editor-hosted integrations |
| Treating SSE as the default remote transport | Locks new work to legacy behavior | Prefer Streamable HTTP and keep SSE only for backward compatibility |
Writing tools without [Description] metadata |
Hosts and models lose schema clarity | Describe tool purpose and parameters explicitly |
| Returning huge binary/text payloads from every tool call | Bloats context and slows hosts | Return focused content and move large data to resources |
| Logging to stdout on stdio servers | Corrupts the protocol stream | Send logs to stderr |
| Assuming prompts/resources/logging/completions exist | Breaks against partial implementations | Check negotiated capabilities first |
| Using filters for normal business logic | Makes handlers opaque and hard to reason about | Keep filters for cross-cutting policy, audit, or protocol plumbing |
Deliver
- a correctly packaged MCP server or client that matches the deployment topology
- explicit tool/resource/prompt definitions with descriptions and bounded payloads
- capability-aware handling for optional MCP features
- validation notes for transport, auth boundary, and host/client interoperability
Validate
- chosen package matches the topology:
Core,ModelContextProtocol, orAspNetCore - stdio servers do not write logs or diagnostics to stdout
- HTTP servers use
MapMcp()and are tested at the final route, for example/mcp - tools, resources, and prompts use current
[McpServer*]attributes or documented handler/filter alternatives - client code checks
ServerCapabilitiesbefore using subscriptions, completions, logging, or prompt/resource list-change flows - Streamable HTTP is the default for new remote servers; SSE is used only for legacy compatibility
- experimental APIs and custom serialization settings are reviewed intentionally rather than copied blindly
Files (dotnet-skills)
-
references
-
patterns.md 10.8 KB
# MCP C# SDK Patterns Use this file when the task needs concrete current patterns from the official MCP C# SDK rather than high-level routing guidance. ## Package and Transport Matrix | Scenario | Package | Transport / API | |----------|---------|-----------------| | Minimal client or low-level host | `ModelContextProtocol.Core` | `McpClient`, low-level server APIs | | Typical client or stdio server | `ModelContextProtocol` | `StdioClientTransport`, `WithStdioServerTransport()` | | ASP.NET Core server | `ModelContextProtocol.AspNetCore` | `WithHttpTransport()`, `MapMcp()` | | Remote client over HTTP | `ModelContextProtocol` or `Core` | `HttpClientTransport` | ## Minimal stdio server ```csharp using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using ModelContextProtocol.Server; using System.ComponentModel; var builder = Host.CreateApplicationBuilder(args); builder.Logging.AddConsole(options => { options.LogToStandardErrorThreshold = LogLevel.Trace; }); builder.Services .AddMcpServer() .WithStdioServerTransport() .WithToolsFromAssembly(); await builder.Build().RunAsync(); [McpServerToolType] public static class EchoTools { [McpServerTool, Description("Echoes the message back to the caller.")] public static string Echo([Description("Message to echo")] string message) => $"hello {message}"; } ``` Use `WithTools<T>()`, `WithResources<T>()`, and `WithPrompts<T>()` when you want explicit registration instead of assembly scanning. ## Minimal ASP.NET Core server ```csharp using ModelContextProtocol.Server; using System.ComponentModel; var builder = WebApplication.CreateBuilder(args); builder.Services .AddMcpServer() .WithHttpTransport() .WithTools<WeatherTools>() .WithResources<WeatherResources>() .WithPrompts<WeatherPrompts>(); var app = builder.Build(); app.MapMcp("/mcp"); app.Run(); [McpServerToolType] public static class WeatherTools { [McpServerTool, Description("Returns the current weather for a city.")] public static string GetCurrentWeather( [Description("City name")] string city) => $"Current weather for {city}: sunny"; } ``` Notes: - `MapMcp()` serves Streamable HTTP and legacy SSE endpoints. - New remote clients should connect to the mapped route directly and prefer Streamable HTTP. - Only point SSE clients to `{route}/sse`. ## Stdio client pattern ```csharp using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; var transport = new StdioClientTransport(new StdioClientTransportOptions { Name = "Everything", Command = "npx", Arguments = ["-y", "@modelcontextprotocol/server-everything"], }); await using var client = await McpClient.CreateAsync(transport); IList<McpClientTool> tools = await client.ListToolsAsync(); CallToolResult result = await client.CallToolAsync( "echo", new Dictionary<string, object?> { ["message"] = "Hello MCP!" }); Console.WriteLine(result.Content.OfType<TextContentBlock>().First().Text); ``` ## HTTP client pattern ```csharp using ModelContextProtocol.Client; var transport = new HttpClientTransport(new HttpClientTransportOptions { Endpoint = new Uri("https://example.com/mcp"), TransportMode = HttpTransportMode.StreamableHttp, ConnectionTimeout = TimeSpan.FromSeconds(30), AdditionalHeaders = new Dictionary<string, string> { ["Authorization"] = "Bearer <token>" } }); await using var client = await McpClient.CreateAsync(transport); ``` For mixed environments, `HttpTransportMode.AutoDetect` is the default. It tries Streamable HTTP first and falls back to SSE when needed. ## Session resumption Use this only for Streamable HTTP sessions: ```csharp var transport = new HttpClientTransport(new HttpClientTransportOptions { Endpoint = new Uri("https://example.com/mcp"), KnownSessionId = previousSessionId }); await using var client = await McpClient.ResumeSessionAsync( transport, new ResumeClientSessionOptions { ServerCapabilities = previousServerCapabilities, ServerInfo = previousServerInfo }); ``` ## Tool pattern ```csharp [McpServerToolType] public sealed class BuildTools(IBuildService builds) { [McpServerTool, Description("Queues a build for the requested branch.")] public async Task<string> QueueBuildAsync( [Description("Git branch to build")] string branch, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(branch)) { throw new McpProtocolException("Branch is required.", McpErrorCode.InvalidParams); } var buildId = await builds.QueueAsync(branch, cancellationToken); return $"queued:{buildId}"; } } ``` Guidance: - `string` results are wrapped as `TextContentBlock`. - Use `ImageContentBlock`, `AudioContentBlock`, or `EmbeddedResourceBlock` when the tool returns richer content. - Use `[Description]` on the method and parameters so hosts can build better schemas. - Methods can accept `McpServer`, `ClaimsPrincipal`, `IProgress<ProgressNotificationValue>`, and DI-registered services in addition to normal arguments. ## Resource pattern ```csharp [McpServerResourceType] public static class RepoResources { [McpServerResource( UriTemplate = "repo://readme", Name = "Repository README", MimeType = "text/markdown")] [Description("Returns the repository overview document.")] public static string ReadReadme() => File.ReadAllText("README.md"); [McpServerResource(UriTemplate = "repo://files/{path}", Name = "Repository File")] [Description("Returns a file under the approved repository root.")] public static TextResourceContents ReadFile(string path) { var fullPath = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, path)); var root = Path.GetFullPath(Environment.CurrentDirectory); if (!fullPath.StartsWith(root, StringComparison.Ordinal)) { throw new McpException("Requested file is outside the repository root."); } return new TextResourceContents { Uri = $"repo://files/{path}", MimeType = "text/plain", Text = File.ReadAllText(fullPath) }; } } ``` Use resource templates when the URI contains parameters. Clients can enumerate them with `ListResourceTemplatesAsync()` and materialize them with `ReadResourceAsync(...)`. ## Prompt pattern ```csharp using Microsoft.Extensions.AI; using ModelContextProtocol.Protocol; [McpServerPromptType] public static class ReviewPrompts { [McpServerPrompt, Description("Builds a code-review prompt.")] public static IEnumerable<ChatMessage> CodeReview( [Description("Programming language")] string language, [Description("Code to review")] string code) => [ new(ChatRole.User, $"Review this {language} code:\n\n```{language}\n{code}\n```") ]; [McpServerPrompt, Description("Builds a document-review prompt with an embedded resource.")] public static IEnumerable<PromptMessage> ReviewDocument( [Description("Document identifier")] string id) => [ new() { Role = Role.User, Content = new TextContentBlock { Text = "Review the attached document." } }, new() { Role = Role.User, Content = new EmbeddedResourceBlock { Resource = new TextResourceContents { Uri = $"docs://documents/{id}", MimeType = "text/plain", Text = LoadDocument(id) } } } ]; } ``` Use `ChatMessage` for normal text/image flows and `PromptMessage` when you need protocol-specific content such as embedded resources. ## Capability-aware client pattern ```csharp var options = new McpClientOptions { Capabilities = new ClientCapabilities { Roots = new RootsCapability { ListChanged = true }, Sampling = new SamplingCapability(), Elicitation = new ElicitationCapability { Form = new FormElicitationCapability(), Url = new UrlElicitationCapability() } } }; await using var client = await McpClient.CreateAsync(transport, options); if (client.ServerCapabilities.Resources is { Subscribe: true }) { await client.SubscribeToResourceAsync("repo://readme"); } if (client.ServerCapabilities.Logging is not null) { await client.SetLoggingLevelAsync(LoggingLevel.Info); } ``` Check `client.ServerCapabilities` before using: - resource subscriptions - prompt/resource list-change notifications - completions - logging - any feature that is optional in the spec ## Passing MCP tools into a chat client `McpClientTool` inherits from `AIFunction`, so discovered tools can be passed directly into `IChatClient`: ```csharp IList<McpClientTool> tools = await client.ListToolsAsync(); IChatClient chatClient = ...; var response = await chatClient.GetResponseAsync( "Use the MCP tools to answer the question.", new() { Tools = [.. tools] }); ``` ## Filters for cross-cutting behavior Use filters for audit, custom JSON-RPC routing, or policy, not for normal domain logic: ```csharp builder.Services .AddMcpServer() .WithMessageFilters(messageFilters => { messageFilters.AddIncomingFilter(next => async (context, cancellationToken) => { if (context.JsonRpcMessage is JsonRpcRequest request) { Console.Error.WriteLine($"Incoming MCP method: {request.Method}"); } await next(context, cancellationToken); }); }) .WithRequestFilters(requestFilters => { requestFilters.AddCallToolFilter(next => async (context, cancellationToken) => { Console.Error.WriteLine($"Executing tool: {context.Params?.Name}"); return await next(context, cancellationToken); }); }) .WithTools<WeatherTools>(); ``` ## Experimental APIs and serialization When using experimental MCP APIs: - suppress only the relevant `MCPEXP...` diagnostic ids - avoid blanket `NoWarn` entries for unrelated code - if you supply a custom `JsonSerializerContext`, prepend `McpJsonUtilities.DefaultOptions.TypeInfoResolver` so MCP protocol types continue to serialize with the SDK's contract ## Validation Checklist - client and server transport choices match the deployment topology - stdio servers keep stdout protocol-clean - HTTP endpoints are tested at the real final route - tool/resource/prompt descriptions are explicit - optional features are guarded by capability checks - filter usage is cross-cutting rather than replacing normal handlers static string LoadDocument(string id) => $"Document {id}"; -
security.md 6.4 KB
# MCP C# SDK Security Notes Use this file when the task involves safe server/client design, auth boundaries, or data exposure rules for MCP. ## Security Priorities 1. Keep the MCP transport clean and predictable. 2. Limit what tools and resources can reach. 3. Return safe protocol errors instead of leaking internals. 4. Authenticate and authorize at the transport boundary. 5. Make optional capabilities explicit. ## stdio hygiene For stdio servers, anything written to stdout can corrupt the protocol stream. Route logs to stderr: ```csharp builder.Logging.AddConsole(options => { options.LogToStandardErrorThreshold = LogLevel.Trace; }); ``` Do not: - write banner text to stdout - print debug tracing with `Console.WriteLine` - mix app startup messaging into the MCP pipe ## Parameter validation Treat every tool/resource/prompt argument as untrusted input. ```csharp [McpServerToolType] public sealed class FileTools(IFileSystem files) { [McpServerTool, Description("Reads a text file below the approved workspace root.")] public async Task<string> ReadTextFileAsync( [Description("Relative path below the workspace root")] string relativePath, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(relativePath)) { throw new McpProtocolException("Path is required.", McpErrorCode.InvalidParams); } var root = Path.GetFullPath("/path/to/approved/workspace"); var fullPath = Path.GetFullPath(Path.Combine(root, relativePath)); if (!fullPath.StartsWith(root, StringComparison.Ordinal)) { throw new McpException("Requested path is outside the allowed workspace."); } return await files.File.ReadAllTextAsync(fullPath, cancellationToken); } } ``` Patterns: - normalize paths before checking the root - whitelist supported operations and file types - clamp numeric limits and pagination inputs - reject blank or ambiguous identifiers early ## Error boundaries Use the right exception type for the right kind of failure: - `McpProtocolException` for JSON-RPC or contract-level failures such as invalid parameters - `McpException` for domain errors whose message is safe to surface - ordinary exceptions only for unexpected faults; they will be converted into generic tool errors This distinction matters because tool failures are exposed differently from protocol failures. ## Authorization For HTTP servers, prefer normal ASP.NET Core auth middleware and endpoint policy around `MapMcp()`: - bearer tokens, cookies, or mutual TLS belong at the HTTP boundary - rate limiting belongs in ASP.NET Core middleware or infrastructure, not ad hoc inside every tool - map unauthenticated requests to standard HTTP auth behavior before MCP handlers run Inside MCP handlers, use injected principals for per-operation authorization: ```csharp [McpServerToolType] public sealed class DeploymentTools(IDeploymentService deployments) { [McpServerTool, Description("Cancels a deployment owned by the current user.")] public async Task<string> CancelDeploymentAsync( [Description("Deployment identifier")] string deploymentId, ClaimsPrincipal user, CancellationToken cancellationToken = default) { if (!user.Identity?.IsAuthenticated ?? true) { throw new McpException("Authentication is required."); } await deployments.CancelAsync(deploymentId, user, cancellationToken); return $"Cancelled deployment {deploymentId}."; } } ``` ## Capability minimization Only enable features you are prepared to support safely: - roots: only if the client should disclose filesystem roots - sampling: only if the server should request LLM completions from the client - elicitation: only if the server is allowed to prompt the user for more input - resource subscriptions: only if you can track and notify subscribers correctly Do not assume a host/client supports these features. Capability negotiation is part of the security boundary. ## Tool and resource output discipline Keep payloads small and deliberate. Prefer: - summaries plus identifiers - paginated lists - direct resources for large text or binary data - explicit MIME types Avoid: - dumping whole databases or repositories into one tool result - returning secrets or internal stack traces - embedding large binary payloads when a resource URI is enough ## Remote transport guidance For remote servers: - prefer Streamable HTTP - use HTTPS - pass auth via standard HTTP headers or ASP.NET Core auth - use SSE only for legacy compatibility For local-only integrations: - prefer stdio - keep environment variables explicit - avoid inheriting more process privileges than the child server needs ## Filters as policy points Filters are appropriate for audit, tracing, and global policy checks: ```csharp builder.Services .AddMcpServer() .WithRequestFilters(filters => { filters.AddCallToolFilter(next => async (context, cancellationToken) => { var toolName = context.Params?.Name; if (toolName is "delete_all_data") { throw new McpException("This tool is disabled in the current environment."); } return await next(context, cancellationToken); }); }); ``` Do not hide primary business rules in filters if the tool handler itself can express them clearly. ## Experimental APIs Experimental MCP APIs can change outside normal patch-level expectations. Before adopting them: - suppress only the specific `MCPEXP...` diagnostic you intend to accept - document why the suppression exists - isolate experimental usage behind an internal abstraction if the project needs a stable surface If you use source-generated JSON serialization, prepend `McpJsonUtilities.DefaultOptions.TypeInfoResolver` so MCP protocol types keep the SDK's serialization contract, including experimental fields when required by the wire protocol. ## Review Checklist - stdout remains protocol-clean for stdio servers - every externally supplied argument is validated and normalized - auth happens at the HTTP boundary and is rechecked inside sensitive handlers - sensitive operations are scoped to the caller's identity or allowed root - optional capabilities are enabled intentionally rather than by accident - tool/resource outputs exclude secrets, internal stack traces, and oversized payloads
-
-
SKILL.md 10.8 KB
--- name: dotnet-mcp version: "1.1.1" category: "AI" description: "Build or consume Model Context Protocol (MCP) servers and clients in .NET using the official MCP C# SDK, including stdio, Streamable HTTP, tools, prompts, resources, and capability negotiation." compatibility: "Requires the official MCP C# SDK packages (`ModelContextProtocol.Core`, `ModelContextProtocol`, or `ModelContextProtocol.AspNetCore`) on .NET 8+; current guidance targets the v1.1.x SDK." --- # MCP C# SDK for .NET ## Trigger On - building or consuming MCP servers from a .NET application or library - choosing between stdio and HTTP transport for MCP - exposing tools, resources, prompts, completions, or logging to an MCP host - connecting a .NET app to an existing MCP server and passing discovered tools into `IChatClient` - bootstrapping a minimal MCP client/server from the `.NET AI` quickstarts or publishing a server to the MCP Registry - implementing capability-aware flows such as roots, sampling, elicitation, subscriptions, or session resumption ## Use This Skill Instead Of - Use `dotnet-mcp` when **protocol interoperability** is the requirement. - Use `dotnet-microsoft-extensions-ai` when you only need model/provider abstraction or local tool orchestration without the MCP wire protocol. - Use `dotnet-microsoft-agent-framework` when the main problem is agent orchestration; combine it with `dotnet-mcp` only when those agents must consume or expose MCP endpoints. - Use the `.NET AI` quickstarts for the very first vertical slice, then come back here to harden transport, capability negotiation, publishing, and host interoperability. ## Documentation - [MCP C# SDK overview](https://csharp.sdk.modelcontextprotocol.io/) - [Getting Started](https://csharp.sdk.modelcontextprotocol.io/concepts/getting-started.html) - [API reference](https://csharp.sdk.modelcontextprotocol.io/api/ModelContextProtocol.html) - [Conceptual docs](https://csharp.sdk.modelcontextprotocol.io/concepts/index.html) - [Versioning policy](https://csharp.sdk.modelcontextprotocol.io/versioning.html) - [Experimental APIs](https://csharp.sdk.modelcontextprotocol.io/experimental.html) - [MCP C# SDK repository](https://github.com/modelcontextprotocol/csharp-sdk) - [Model Context Protocol specification](https://modelcontextprotocol.io/specification/) ## References Load only what the task needs: - [`references/patterns.md`](references/patterns.md) - current server/client patterns, transports, capabilities, filters, and chat-client integration - [`references/security.md`](references/security.md) - safe error handling, auth boundaries, stdio logging hygiene, and defensive tool/resource patterns ## Package Selection | Package | Choose when | |---------|-------------| | `ModelContextProtocol.Core` | You only need a client or low-level server APIs and want the smallest dependency set. | | `ModelContextProtocol` | You want the main SDK package with hosting, DI, attribute discovery, and stdio server support. Start here for most projects. | | `ModelContextProtocol.AspNetCore` | You are hosting a remote MCP server in ASP.NET Core over HTTP. This includes the main package. | ## Transport Selection | Transport | Use when | Notes | |-----------|----------|-------| | `StdioClientTransport` / `WithStdioServerTransport()` | The MCP server should run as a local child process. | Best for local tooling and editor/agent integrations. | | `HttpClientTransport` + `HttpTransportMode.StreamableHttp` | The server is remote or should be reachable over HTTP. | Recommended HTTP transport; supports streaming and session resumption. | | `HttpTransportMode.Sse` | You must connect to an older SSE-only server. | Legacy compatibility only; do not choose this for new servers. | ```mermaid flowchart LR A["Need MCP interoperability in .NET"] --> B{"Role?"} B -->|"Expose MCP surface"| C{"Where will it run?"} B -->|"Consume an MCP server"| D{"Transport?"} C -->|"Local child process"| E["ModelContextProtocol\nAddMcpServer()\nWithStdioServerTransport()"] C -->|"Remote HTTP endpoint"| F["ModelContextProtocol.AspNetCore\nAddMcpServer()\nWithHttpTransport()\nMapMcp()"] D -->|"stdio"| G["StdioClientTransport\nMcpClient.CreateAsync()"] D -->|"HTTP"| H["HttpClientTransport\nAutoDetect or StreamableHttp"] E --> I["Register tools/resources/prompts"] F --> I G --> J["Check ServerCapabilities\nbefore optional features"] H --> J ``` ## Workflow 1. Pick the package and transport first. - Local child-process server: `ModelContextProtocol` + `WithStdioServerTransport()`. - Remote server: `ModelContextProtocol.AspNetCore` + `WithHttpTransport()` + `MapMcp()`. - Client-only app: start with `ModelContextProtocol` or `ModelContextProtocol.Core`. - Registry distribution: pair a minimal server with the MCP Registry publishing flow only after the server contract is stable. 2. Model the MCP surface explicitly. - Tools: `[McpServerToolType]` + `[McpServerTool]` - Resources: `[McpServerResourceType]` + `[McpServerResource]` - Prompts: `[McpServerPromptType]` + `[McpServerPrompt]` - Use custom handlers or filters only for cross-cutting behavior, protocol extensions, or advanced routing. 3. Prefer attribute discovery for straightforward servers. ```csharp using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using ModelContextProtocol.Server; using System.ComponentModel; var builder = Host.CreateApplicationBuilder(args); builder.Logging.AddConsole(options => { options.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}"; } ``` 4. For HTTP servers, use the ASP.NET Core transport and map the endpoint directly. ```csharp using ModelContextProtocol.Server; using System.ComponentModel; var builder = WebApplication.CreateBuilder(args); builder.Services .AddMcpServer() .WithHttpTransport() .WithToolsFromAssembly(); var app = builder.Build(); app.MapMcp("/mcp"); app.Run(); [McpServerToolType] public static class EchoTool { [McpServerTool, Description("Echoes the message back to the client.")] public static string Echo(string message) => $"hello {message}"; } ``` 5. When consuming a server, use `McpClient.CreateAsync(...)` and stay capability-aware. ```csharp using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; var transport = new StdioClientTransport(new StdioClientTransportOptions { Name = "Everything", Command = "npx", Arguments = ["-y", "@modelcontextprotocol/server-everything"], }); await using var client = await McpClient.CreateAsync(transport); IList<McpClientTool> tools = await client.ListToolsAsync(); if (client.ServerCapabilities.Prompts is not null) { var prompts = await client.ListPromptsAsync(); } ``` 6. Treat optional features as negotiated capabilities, not assumptions. - Client capabilities: configure `McpClientOptions.Capabilities` for roots, sampling, and elicitation. - Server capabilities are inferred from registered features. - Check `client.ServerCapabilities` before using completions, logging, prompt list-change notifications, or resource subscriptions. - Use `client.NegotiatedProtocolVersion` or `server.NegotiatedProtocolVersion` only when version-specific behavior matters. 7. Keep HTTP guidance current. - Streamable HTTP is the recommended transport for remote servers. - `MapMcp()` also serves SSE compatibility endpoints for older clients. - HTTP clients can use `AutoDetect` by default, or force `StreamableHttp` / `Sse`. - Session resumption is available for Streamable HTTP through `McpClient.ResumeSessionAsync(...)`. 8. Treat the `.NET AI` MCP quickstarts as bootstrap examples. - `build-mcp-client` and `build-mcp-server` are good starting points when the surrounding app is still MEAI-centric. - `publish-mcp-registry` is the distribution step, not the design step. Stabilize the protocol surface before publishing. 9. Respect current error and serialization rules. - Tool exceptions normally come back as `CallToolResult.IsError == true`. - Throw `McpProtocolException` only for protocol-level JSON-RPC failures. - `McpClientTool` inherits from `AIFunction`, so discovered tools can be passed directly into `IChatClient`. - Experimental APIs use `MCPEXP...` diagnostics; suppress them intentionally, not globally by accident. - If you use a custom `JsonSerializerContext`, prepend `McpJsonUtilities.DefaultOptions.TypeInfoResolver` so MCP protocol types keep the SDK's contract. ## Anti-Patterns To Avoid | Anti-pattern | Why it causes trouble | Better approach | |--------------|-----------------------|-----------------| | Picking HTTP transport for a purely local child-process scenario | Adds unnecessary hosting, auth, and deployment surface | Use stdio for local/editor-hosted integrations | | Treating SSE as the default remote transport | Locks new work to legacy behavior | Prefer Streamable HTTP and keep SSE only for backward compatibility | | Writing tools without `[Description]` metadata | Hosts and models lose schema clarity | Describe tool purpose and parameters explicitly | | Returning huge binary/text payloads from every tool call | Bloats context and slows hosts | Return focused content and move large data to resources | | Logging to stdout on stdio servers | Corrupts the protocol stream | Send logs to stderr | | Assuming prompts/resources/logging/completions exist | Breaks against partial implementations | Check negotiated capabilities first | | Using filters for normal business logic | Makes handlers opaque and hard to reason about | Keep filters for cross-cutting policy, audit, or protocol plumbing | ## Deliver - a correctly packaged MCP server or client that matches the deployment topology - explicit tool/resource/prompt definitions with descriptions and bounded payloads - capability-aware handling for optional MCP features - validation notes for transport, auth boundary, and host/client interoperability ## Validate - chosen package matches the topology: `Core`, `ModelContextProtocol`, or `AspNetCore` - stdio servers do not write logs or diagnostics to stdout - HTTP servers use `MapMcp()` and are tested at the final route, for example `/mcp` - tools, resources, and prompts use current `[McpServer*]` attributes or documented handler/filter alternatives - client code checks `ServerCapabilities` before using subscriptions, completions, logging, or prompt/resource list-change flows - Streamable HTTP is the default for new remote servers; SSE is used only for legacy compatibility - experimental APIs and custom serialization settings are reviewed intentionally rather than copied blindly
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.