Files
awesome-copilot/skills/dotnet-mcp-builder/references/transport-stdio.md
T
Adrien Clerbois aa01464ccb Update dotnet-mcp-builder skill to ModelContextProtocol 2.x (#2487)
* Update dotnet-mcp-builder skill to ModelContextProtocol 2.x

Align the skill with the C# SDK 2.0.0 release and the MCP 2026-07-28
spec: stable line is now 2.x, HttpServerTransportOptions.Stateless
defaults to true, roots/sampling/MCP-channel logging are [Obsolete]
(MCP9005) with the multi-round-trip input_required pattern as the
replacement, discovery-first negotiation (server/discover) supersedes
the initialize handshake, Mcp-Method/Mcp-Name routable headers, raw
structuredContent for non-object results, required Tool.inputSchema,
and the new ModelContextProtocol.Extensions.Tasks and
ModelContextProtocol.Extensions.Apps packages (typed MCP Apps support
replacing the hand-rolled _meta/ui:// pattern on 1.x).

* Address Copilot review: Apps extension accuracy, header scope, capability ownership

- packages.md: the Apps package replaces the manual _meta wiring, not the
  ui:// resource; note the experimental MCPEXP003 diagnostic; label the
  1.x -> 2.0 list as highlights and add the OAuth/SSE runtime changes
  with a pointer to the full release notes.
- transport-http.md: Mcp-Method is on every POST, Mcp-Name only on named
  invocations (tools/call, prompts/get, resources/read) - do not require
  it globally at gateways.
- mcp-apps.md: current MIME type is text/html;profile=mcp-app (skybridge
  is a legacy draft value); document [McpAppUi] + WithMcpApps().
- server-features.md: roots/sampling are client capabilities, only
  logging sits on ServerCapabilities.

* Correct stateful HTTP guidance: 2026-07-28 has no HTTP sessions

Per the official SDK v2 elicitation docs, a server with Stateless=false
refuses the 2026-07-28 revision so dual-path clients fall back to an
initialize-capable revision; ElicitAsync cannot be used on 2026-07-28
Streamable HTTP at all. Reframe stateful HTTP as down-level
compatibility mode and document the multi-round-trip pattern
(InputRequiredException / InputRequest.ForElicitation, retry with
InputResponses -> ElicitResult) as the current-protocol way to ask
mid-tool, across SKILL.md, transport-http.md, and elicitation.md.
2026-07-31 10:31:14 +10:00

5.1 KiB

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 (elicitation, notifications, the deprecated sampling/roots) — STDIO always supports them, no Stateless flag to worry about.

If the user wants a remote/multi-tenant server, use HTTP Streamable instead.

Minimal server

dotnet new console -n MyStdioServer -f net10.0
cd MyStdioServer
dotnet add package ModelContextProtocol
dotnet add package Microsoft.Extensions.Hosting
// 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) during negotiation (the 2026-07-28 server/discover exchange, or the legacy initialize response for down-level clients — the SDK handles both automatically). By default it derives them from your assembly. To override:

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:

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:

{
  "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:

"command": "dnx",
"args": ["MyMcpServer", "--version", "1.2.3"]

Wiring to VS Code (GitHub Copilot Chat)

In .vscode/mcp.json:

{
  "servers": {
    "my-server": {
      "type": "stdio",
      "command": "dotnet",
      "args": ["run", "--project", "${workspaceFolder}/src/MyMcpServer"]
    }
  }
}

Local debugging

The cleanest workflow is MCP Inspector:

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 for more.

Graceful shutdown

builder.Build().RunAsync() already handles SIGINT/SIGTERM. If you have background work to flush, use IHostApplicationLifetime:

var host = builder.Build();
var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStopping.Register(() =>
{
    // flush, close handles, etc. — keep it fast (<5s) so the client doesn't hang.
});
await host.RunAsync();