
Mcp
- 18 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
mcp is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- mcp
- AI & Agent Building
- AI-coding skill
Mcp by the numbers
- 18 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #10,710 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill mcpAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 18 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
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, session resumption, or enterprise managed authorization
Use This Skill Instead Of
- Use
mcpwhen protocol interoperability is the requirement. - Use
microsoft-extensions-aiwhen you only need model/provider abstraction or local tool orchestration without the MCP wire protocol. - Use
microsoft-agent-frameworkwhen the main problem is agent orchestration; combine it withmcponly 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 integration
- 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. |
Current v1.4 Notes
- Enterprise managed authorization now has an SDK surface through
IdentityAssertionGrantProviderfor the Identity Assertion Authorization Grant flow. Use it only when the enterprise SSO and MCP authorization-server contract is part of the actual scenario. StdioClientTransportOptions.InheritEnvironmentVariablescontrols whether child-process MCP servers inherit the parent environment. Set it intentionally when launching untrusted or third-party servers.- Streamable HTTP session
DELETEis hardened to require the same authenticated user that opened the session. Do not build custom session cleanup paths that bypass that authorization check. - Stdio transport no longer logs child-process environment variables at trace level, but server authors should still treat environment variables as secrets.
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 --> JWorkflow
1. 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.
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.
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.
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.
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.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.
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
AutoDetectby default, or forceStreamableHttp/Sse. - Session resumption is available for Streamable HTTP through
McpClient.ResumeSessionAsync(...). - For authenticated Streamable HTTP sessions, cleanup and resume operations must preserve the same user boundary.
8. Treat the .NET AI MCP 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.
9. 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.
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
{
"version": "1.2.0",
"category": "AI",
"packages": [
"ModelContextProtocol",
"ModelContextProtocol.AspNetCore"
]
}
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 |
| Enterprise managed authorization | ModelContextProtocol plus authentication support | IdentityAssertionGrantProvider for ID-JAG flows |
Minimal stdio server
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
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. - In SDK v1.4.0 and later, Streamable HTTP session cleanup must preserve the same authenticated user that opened the session.
Stdio client pattern
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
var transport = new StdioClientTransport(new StdioClientTransportOptions
{
Name = "Everything",
Command = "npx",
Arguments = ["-y", "@modelcontextprotocol/server-everything"],
InheritEnvironmentVariables = false,
});
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
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.
Enterprise managed authorization
Use IdentityAssertionGrantProvider only when the deployment needs the Identity Assertion Authorization Grant flow: exchange an enterprise IdP ID token for a JWT authorization grant, then exchange that grant with the MCP authorization server for an MCP access token. Keep this out of ordinary local stdio or simple bearer-token scenarios.
Session resumption
Use this only for Streamable HTTP sessions:
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
[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:
stringresults are wrapped asTextContentBlock.- Use
ImageContentBlock,AudioContentBlock, orEmbeddedResourceBlockwhen 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
[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
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
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:
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:
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
NoWarnentries for unrelated code - if you supply a custom
JsonSerializerContext, prependMcpJsonUtilities.DefaultOptions.TypeInfoResolverso 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}";
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:
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
- inherit parent environment variables into child servers unless the server needs them and the values are safe for that trust boundary
Parameter validation
Treat every tool/resource/prompt argument as untrusted input.
[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:
McpProtocolExceptionfor JSON-RPC or contract-level failures such as invalid parametersMcpExceptionfor 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:
[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}.";
}
}For Streamable HTTP, SDK v1.4.0 requires a DELETE session cleanup request to come from the same authenticated user that created the session. Preserve that boundary when adding reverse proxies, custom auth middleware, or session-management helpers.
For enterprise SSO, use IdentityAssertionGrantProvider only when the Identity Assertion Authorization Grant flow is explicitly part of the deployment. Treat exchanged ID tokens, JWT authorization grants, and MCP access tokens as separate secrets with distinct lifetimes.
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:
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