Skip to content

Building an MCP Server

Key Points

  • Build a .NET app that exposes tools/resources/prompts via MCP. Consumed by Claude, ChatGPT, your agents, etc.
  • Annotation-based: decorate methods; SDK auto-registers.
  • Transport: stdio for local; HTTP for remote.
  • Senior practices: validate inputs; auth; rate limit; logging; sandbox.

Hello server

using ModelContextProtocol.Server;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();

await builder.Build().RunAsync();

[McpServerToolType]
public class TimeTools
{
    [McpServerTool, Description("Get current UTC time as ISO 8601")]
    public string GetUtcTime() => DateTime.UtcNow.ToString("O");

    [McpServerTool, Description("Convert UTC time to a target timezone")]
    public string ConvertTime(
        [Description("ISO 8601 UTC time")] string utc,
        [Description("Timezone like 'America/Los_Angeles'")] string timeZone)
    {
        var dt = DateTime.Parse(utc, null, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
        var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
        return TimeZoneInfo.ConvertTimeFromUtc(dt, tz).ToString("O");
    }
}

Run: dotnet run. Configure Claude Desktop / Cursor / your agent to connect.

Resources

[McpServerResourceType]
public class FileResources(string rootPath)
{
    [McpServerResource(UriTemplate = "file://{path}")]
    public async Task<ResourceContents> ReadFile(string path, CancellationToken ct)
    {
        var fullPath = Path.GetFullPath(Path.Combine(rootPath, path));
        if (!fullPath.StartsWith(rootPath)) throw new UnauthorizedAccessException();
        var content = await File.ReadAllTextAsync(fullPath, ct);
        return new ResourceContents { Text = content, MimeType = "text/plain" };
    }
}

Path traversal guard critical.

Prompts

[McpServerPromptType]
public class CodeReviewPrompts
{
    [McpServerPrompt, Description("Code review prompt template")]
    public IList<ChatMessage> Review(
        [Description("Code language")] string language,
        [Description("Code to review")] string code)
    {
        return [
            new(ChatRole.System, $"You are a senior {language} reviewer."),
            new(ChatRole.User, $"Review:\n```{language}\n{code}\n```")
        ];
    }
}

DI in tools

Tools can depend on services:

public class GitHubTools(IGitHubService gh)
{
    [McpServerTool, Description("List PRs")]
    public async Task<List<PullRequest>> ListPrs(string repo, CancellationToken ct)
        => await gh.ListPrsAsync(repo, ct);
}

builder.Services.AddSingleton<IGitHubService, GitHubService>();
builder.Services.AddMcpServer().WithToolsFromAssembly();

DI scope per request.

HTTP/SSE transport

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

var app = builder.Build();
app.MapMcpEndpoints();   // /mcp/sse, /mcp/message
app.Run();

For remote/multi-user servers.

Authentication

app.UseAuthentication();
app.UseAuthorization();
app.MapMcpEndpoints().RequireAuthorization();

Standard ASP.NET Core auth applies.

Error handling

[McpServerTool]
public async Task<User> GetUser(int id)
{
    var u = await _service.GetAsync(id);
    if (u is null) throw new McpException($"User {id} not found", McpErrorCode.InvalidRequest);
    return u;
}

McpException returns proper JSON-RPC error codes.

Rate limiting

builder.Services.AddRateLimiter(o =>
{
    o.AddFixedWindowLimiter("mcp", c => { c.PermitLimit = 100; c.Window = TimeSpan.FromMinutes(1); });
});
app.UseRateLimiter();
app.MapMcpEndpoints().RequireRateLimiting("mcp");

Logging

builder.Logging.AddOpenTelemetry(o => o.AddOtlpExporter());

OTel emits spans per tool call. Track usage.

Testing

[Fact]
public async Task GetTime_returns_iso8601()
{
    var server = new TestMcpServer().AddToolsFromType<TimeTools>();
    var client = server.CreateClient();
    var result = await client.CallToolAsync("GetUtcTime", new());
    Assert.Single(result.Content);
}

Common server use cases

  • Internal data API as MCP: customers' AI agents can query.
  • Coding tools: read code, run linter, run tests.
  • Workflow controllers: pause / resume / approve.
  • Domain APIs: any business function exposed to agents.

Security checklist

  • ✅ Input validation per tool.
  • ✅ Path / URL allowlists.
  • ✅ Auth on HTTP transport.
  • ✅ Rate limiting.
  • ✅ No secrets in tool descriptions.
  • ✅ Sandbox file ops.
  • ✅ Audit log every tool call.

Senior considerations

  • Tool descriptions are LLM-facing prompts — clear, specific, no ambiguity.
  • Idempotent ops when possible.
  • Return structured data (JSON) — easier for LLM to use.
  • Test with real LLMs — descriptions that read well to humans may confuse LLMs.

Cross-references