ScreenshotNeo

BlogAI agents

How to Build an MCP Server in C#

Build a working MCP server in C# with the official SDK, expose tools, choose stdio or HTTP, and prepare it for reliable deployment.

By the ScreenshotNeo team1 October 20269 min read

How to Build an MCP Server in C#

Direct answer: For a local MCP server in C#, create a .NET console app, install ModelContextProtocol and Microsoft.Extensions.Hosting, configure AddMcpServer() with WithStdioServerTransport(), and register attributed tools with WithToolsFromAssembly(). For a remotely hosted server, use ModelContextProtocol.AspNetCore, Streamable HTTP, and app.MapMcp().

The official C# SDK provides client and server building blocks for MCP, an open protocol for connecting AI applications to tools and data. The SDK documentation is the source of truth for package versions and API changes: C# SDK repository.

1. Choose stdio or Streamable HTTP

Requirement Recommended transport What it means
A desktop client starts your server process stdio The client launches the executable and communicates over standard input and output.
Several clients need one shared service Streamable HTTP ASP.NET Core hosts the server and clients connect over HTTP.
You need subscriptions, unsolicited server-to-client requests, or client isolation Stateful HTTP Keep transport sessions deliberately rather than using the stateless default.
You are maintaining an older integration SSE only when required Current SDK guidance treats SSE as legacy for new remote servers.

For local development, stdio is usually the shortest path. For a deployed service, current guidance favors Streamable HTTP, which runs statelessly by default in SDK v2.0. Stateless operation avoids in-memory session tracking and is easier to scale horizontally. Choose stateful sessions only when your application needs session-specific behavior.

A local MCP client starts a C# server and exchanges tool messages over stdio.
A local MCP client starts a C# server and exchanges tool messages over stdio.

2. Build a minimal local MCP server

Install the packages

dotnet new console -n CSharpMcpServer
cd CSharpMcpServer
dotnet add package ModelContextProtocol
dotnet add package Microsoft.Extensions.Hosting

If you are unsure which package to begin with, start with ModelContextProtocol. It includes hosting, dependency injection, stdio server support, and attribute-based tool discovery. Use ModelContextProtocol.Core when you specifically need lower-level APIs or fewer dependencies.

Replace 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);

// stdout belongs to the MCP protocol. Send logs to stderr instead.
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 supplied message back to the caller.")]
    public static string Echo(
        [Description("The message to return unchanged.")] string message)
        => $"hello {message}";
}

Run it with:

dotnet run

The process waits for an MCP client. Do not write diagnostic output with Console.WriteLine; any unexpected stdout bytes can corrupt the protocol stream. Use the configured logger, which writes to stderr.

How tool discovery works

  1. [McpServerToolType] marks a class containing tools.
  2. [McpServerTool] marks each callable method.
  3. WithToolsFromAssembly() scans the assembly and registers those methods.
  4. The SDK derives an input schema from method parameters and their types.
  5. [Description] text is exposed to clients and models, so describe the tool and every argument precisely.

Keep the first tool narrow and predictable. An attribute does not provide authorization, access control, or validation for an external system. Add those checks in the method or in a service it calls.

3. Add dependency injection and asynchronous work

Register application services with the host and inject them into tool classes. Use asynchronous methods for network, database, and file operations.

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(o => o.LogToStandardErrorThreshold = LogLevel.Trace);

builder.Services.AddSingleton<StatusService>();
builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();

await builder.Build().RunAsync();

public sealed class StatusService
{
    public Task<string> GetStatusAsync(CancellationToken cancellationToken)
        => Task.FromResult("ready");
}

[McpServerToolType]
public sealed class StatusTools(StatusService status)
{
    [McpServerTool, Description("Returns the current service status.")]
    public Task<string> GetStatus(
        [Description("Cancellation token supplied by the host.")] CancellationToken cancellationToken)
        => status.GetStatusAsync(cancellationToken);
}

Use explicit return types and stable output shapes. If a tool returns structured data, define a small DTO rather than concatenating ambiguous text.

4. Build an HTTP MCP server with ASP.NET Core

Install the HTTP package

dotnet new web -n CSharpMcpHttpServer
cd CSharpMcpHttpServer
dotnet add package ModelContextProtocol.AspNetCore

Configure Program.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Server;
using System.ComponentModel;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddMcpServer()
    .WithHttpTransport()
    .WithToolsFromAssembly();

var app = builder.Build();

app.MapMcp();

app.Run();

[McpServerToolType]
public static class EchoTool
{
    [McpServerTool, Description("Echoes the supplied message back to the caller.")]
    public static string Echo(
        [Description("The message to return unchanged.")] string message)
        => $"hello {message}";
}

For a local HTTP process, restrict accepted host names to loopback values such as localhost and 127.0.0.1, following the SDK guidance. For a public deployment, add authentication, authorization, input validation, secret management, rate limiting, TLS termination, and host filtering appropriate to your infrastructure. An endpoint URL alone is not a security boundary.

Stateless versus stateful HTTP

Stateless HTTP is the default in the current SDK. It avoids storing transport sessions in process memory, which simplifies replicas and load balancing. Enable stateful behavior only when you require session-specific capabilities such as subscriptions, unsolicited server-to-client requests, or isolation between clients. If you choose stateful sessions, plan how session affinity and shared state work across multiple instances.

5. Call an HTTP server from scripts

The exact JSON-RPC methods and protocol headers should follow the SDK and MCP specification version used by your server. The following examples show the shape of an HTTP request; replace the endpoint and request body with the operation your client needs.

cURL

curl -i https://localhost:5001/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  --data '{
    "jsonrpc":"2.0",
    "id":1,
    "method":"tools/list",
    "params":{}
  }' \
  --insecure

Python

import requests

response = requests.post(
    "https://localhost:5001/mcp",
    headers={
        "Content-Type": "application/json",
        "Accept": "application/json, text/event-stream",
    },
    json={
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/list",
        "params": {},
    },
    timeout=30,
    verify=False,  # Only for a local development certificate.
)
response.raise_for_status()
print(response.text)

Node.js

const response = await fetch('https://localhost:5001/mcp', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'accept': 'application/json, text/event-stream'
  },
  body: JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'tools/list',
    params: {}
  })
});

if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
console.log(await response.text());

6. Design tools that models can use correctly

  • Use a specific name: prefer get_invoice_status to process.
  • Describe side effects: say whether a tool reads, creates, updates, or deletes data.
  • Validate every argument: enforce ranges, formats, allowed identifiers, and authorization.
  • Make retries safe: use idempotency keys for operations that create or charge resources.
  • Return actionable errors: distinguish invalid input, missing resources, permission failures, and transient outages.
  • Limit scope: expose only the operations the client actually needs.

7. Run and package the server

Local stdio checklist

  1. Publish the executable with dotnet publish -c Release.
  2. Give the MCP client the published executable and required arguments.
  3. Pass secrets through the environment or a secret manager, not source code.
  4. Keep stdout exclusively for protocol messages.
  5. Write logs to stderr and include request identifiers where available.

HTTP deployment checklist

  • Terminate TLS at a trusted proxy or the application.
  • Configure host filtering and reject unexpected Host headers.
  • Authenticate clients before exposing sensitive tools.
  • Authorize each operation and resource, not only the connection.
  • Set request size, execution timeout, and concurrency limits.
  • Use a shared store or a deliberate routing strategy if stateful sessions are enabled.
  • Monitor error rates, latency, cancellations, and downstream failures.

8. Or skip the browser setup

If your MCP tools need website screenshots, you can call ScreenshotNeo instead of maintaining browser automation. Its API returns PNG, JPEG, WebP, or PDF from one GET request, and its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

Screenshot cleanup happens before the final image is returned.
Screenshot cleanup happens before the final image is returned.

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result.

Read the ScreenshotNeo API documentation for all options. The simplest call is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Features include full-page capture with lazy images loaded, CSS selector capture, device presets, custom viewports, retina scale, PDF page controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture, and a usage API.

There are 1,000 free screenshots each month with no card. Paid plans start at $5 for 3,000 screenshots. Create your free ScreenshotNeo account.

9. Troubleshooting

Symptom Likely cause Fix
Client reports invalid JSON or disconnects Logs or banners were written to stdout. Use the host logger or write diagnostics to stderr.
No tools appear The class or method lacks the MCP attributes, or assembly scanning is missing. Add [McpServerToolType], [McpServerTool], and WithToolsFromAssembly().
Tool arguments fail to deserialize Parameter types or required values do not match the generated schema. Use explicit types, descriptions, and validation; inspect the result of tools/list.
HTTP request is rejected before reaching the tool Wrong content type, endpoint, host, or transport expectations. Confirm /mcp, request headers, accepted hosts, and the SDK version’s HTTP documentation.
Local HTTP access is unexpectedly blocked Host filtering rejects the supplied host name. Use a loopback host during local development and configure allowed hosts deliberately.
Remote calls hang Downstream work has no cancellation or timeout. Pass CancellationToken, set bounded timeouts, and return a clear transient error.
Duplicate records are created after a retry The operation is not idempotent. Require an idempotency key and persist the result before acknowledging success.

10. Performance, reliability, and cost

Performance

  • Keep tool handlers asynchronous so one slow dependency does not block unrelated calls.
  • Set bounded timeouts for every network and database operation.
  • Reuse HTTP clients through dependency injection.
  • Return only the fields the model needs; large tool results increase latency and context usage.
  • Cache safe read-only results and invalidate them when source data changes.
  • For stateless HTTP, scale replicas behind a load balancer without relying on in-memory sessions.

Reliability

  • Classify errors as caller, authorization, not-found, dependency, or retryable failures.
  • Make side-effecting operations idempotent.
  • Propagate cancellation when the client disconnects.
  • Log structured events to stderr for stdio and to your normal server sink for HTTP.
  • Pin and review SDK package versions; MCP APIs and protocol revisions change over time.

Cost

The C# SDK itself is distributed as NuGet software. Your infrastructure cost depends on CPU, memory, network traffic, downstream services, and deployment size. For screenshot tools, ScreenshotNeo bills only clean shots; bot checks, blank pages, timeouts, failed loads, and cache hits are free. Its plans range from the free 1,000-shot tier to paid tiers beginning at $5 for 3,000 shots.

11. Version notes

The .NET team’s July 28, 2026 announcement says MCP C# SDK v2.0 implements the 2026-07-28 MCP specification revision and that HTTP now runs statelessly by default. Check the current SDK documentation before publishing or upgrading because package names, transport APIs, and protocol details can change.

FAQ

Which NuGet package should I install first?

Install ModelContextProtocol for most local servers and general SDK work. Install ModelContextProtocol.AspNetCore when the server is hosted by ASP.NET Core over HTTP.

Can one server support both stdio and HTTP?

Yes, but treat them as separate hosting configurations and secure the HTTP surface independently. Start with the transport your clients actually require.

Is SSE the right transport for a new remote server?

Usually no. Current guidance recommends Streamable HTTP for new remote deployments; use SSE when compatibility with an existing integration requires it.

Does adding [McpServerTool] secure a method?

No. It exposes a method to MCP discovery. Authentication, authorization, validation, rate limits, and secret handling remain application responsibilities.

When should HTTP be stateful?

Use stateful sessions when you need subscriptions, unsolicited server-to-client requests, or client isolation. Otherwise, stateless mode is simpler to operate and scale.