Table of Contents

  1. Introduction
  2. Prerequisites
  3. Project Setup
  4. Understanding the MCP Architecture
  5. Implementing the Core Server
  6. Creating MCP Tools
  7. Tool Naming and Descriptions
  8. SSE (Server-Sent Events) Specifics
  9. Testing Your MCP Server
  10. Complete Example

Introduction

The Model Context Protocol (MCP) is an open protocol that standardizes how applications provide context to Large Language Models (LLMs). This guide demonstrates how to build an MCP server using ASP.NET Core and C#.

An MCP server exposes tools that LLMs can discover and invoke. These tools are simply API endpoints decorated with special attributes that make them discoverable through the MCP protocol.

What You’ll Build

In this tutorial, you’ll create a minimal MCP server with:

  • An SSE (Server-Sent Events) endpoint for real-time communication
  • Two MCP tools for retrieving time information
  • Complete testing setup using .http files

Prerequisites

  • .NET 9.0 SDK or later
  • Basic understanding of ASP.NET Core Web APIs
  • A code editor (Visual Studio, VS Code, or Rider)
  • cURL or an HTTP client for testing

Project Setup

1. Create a New ASP.NET Core Web API Project

dotnet new webapi -n McpRemoteServer
cd McpRemoteServer

2. Install Required NuGet Packages

Add the MCP SDK packages to your project:

dotnet add package ModelContextProtocol
dotnet add package ModelContextProtocol.AspNetCore

Your .csproj file should look like this:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
    <PackageReference Include="ModelContextProtocol" Version="0.4.0-preview.1" />
    <PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.4.0-preview.1" />
  </ItemGroup>

</Project>

Understanding the MCP Architecture

An MCP server has two main components:

  1. SSE Endpoint (/sse): Establishes a persistent connection and creates a session ID
  2. Message Endpoint (/message): Receives JSON-RPC requests to list or call tools

The flow works as follows:

C C l l i i e e n n t t G P E O T S T / s / s m e e s s a S g e e r ? v s e e r s s ( i c o r n e I a d t = e x s x x s e s s S i e o r n v , e r r e ( t c u a r l n l s s s t e o s o s l i , o n r e I t D u ) r n s r e s u l t )

Implementing the Core Server

Configure Program.cs

Replace the contents of Program.cs with this minimal setup:

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

app.MapMcp();

app.Run();

What This Does:

  • AddMcpServer(): Registers MCP services in the dependency injection container
  • WithHttpTransport(): Enables HTTP/SSE transport for the MCP server
  • WithToolsFromAssembly(): Automatically discovers all controllers decorated with [McpServerToolType]
  • MapMcp(): Maps the MCP endpoints (/sse and /message)

That’s it! With just 6 lines of code, you have a functioning MCP server. Now you need to add tools.

Creating MCP Tools

MCP tools are standard ASP.NET Core controller actions decorated with special attributes. Let’s create a TimeController with two tools.

Create the TimeController

Create a new file Controllers/TimeController.cs:

using Microsoft.AspNetCore.Mvc;
using ModelContextProtocol.Server;

namespace McpRemoteServer.Controllers;

public class TimeResponse
{
    public required string TimeZone { get; set; }
    public DateTime LocalTime { get; set; }
}

[ApiController]
[Route("[controller]")]
[McpServerToolType]
public class TimeController : ControllerBase
{
    [HttpGet]
    [McpServerTool]
    public TimeResponse GetCurrentUtcTime()
    {
        return new TimeResponse
        {
            TimeZone = "UTC",
            LocalTime = DateTime.UtcNow
        };
    }

    [HttpGet]
    [McpServerTool]
    public TimeResponse GetCurrentTimeForTimezone([FromQuery] string timeZone)
    {
        var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
        var localTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, timeZoneInfo);

        return new TimeResponse
        {
            TimeZone = timeZone,
            LocalTime = localTime
        };
    }
}

Key Attributes Explained

  • [McpServerToolType]: Marks the controller as containing MCP tools. This attribute must be on the controller class.
  • [McpServerTool]: Marks a specific action method as an MCP tool that can be invoked by LLMs.

Tool Methods

Your tool methods should:

  • Be public action methods in a controller
  • Return serializable objects (POCOs, primitives, etc.)
  • Use standard ASP.NET Core parameter binding ([FromQuery], [FromBody], etc.)

Tool Naming and Descriptions

Default Tool Naming Convention

By default, MCP converts your C# method names to snake_case for tool names:

C# Method Name MCP Tool Name
GetCurrentUtcTime get_current_utc_time
GetCurrentTimeForTimezone get_current_time_for_timezone

Customizing Tool Names

You can override the default name by providing a name parameter to the [McpServerTool] attribute:

[HttpGet]
[McpServerTool(Name = "fetch_utc_time")]
public TimeResponse GetCurrentUtcTime()
{
    // ...
}

Adding Tool Descriptions

Descriptions help LLMs understand when to use your tools. Add them using the Description parameter:

[HttpGet]
[McpServerTool(
    Name = "get_current_utc_time",
    Description = "Returns the current UTC time. Use this when you need to know the current time in UTC timezone.")]
public TimeResponse GetCurrentUtcTime()
{
    // ...
}

[HttpGet]
[McpServerTool(
    Name = "get_current_time_for_timezone",
    Description = "Returns the current time for a specific timezone. Provide a valid IANA timezone identifier (e.g., 'America/New_York', 'Europe/London').")]
public TimeResponse GetCurrentTimeForTimezone([FromQuery] string timeZone)
{
    // ...
}

Best Practices for Descriptions:

  • Be concise but specific
  • Mention required parameters and their format
  • Provide examples when helpful
  • Describe when the LLM should use this tool

SSE (Server-Sent Events)

What is SSE?

Server-Sent Events (SSE) is a server push technology that enables a server to send automatic updates to a client via HTTP connection. In the context of MCP:

  • The client initiates a connection to /sse
  • The server creates a session and returns a unique sessionId
  • This session maintains state for subsequent tool calls

SSE Endpoint Behavior

When you call GET /sse, the server:

  1. Creates a new session with a unique ID
  2. Keeps the connection open
  3. Can stream events back to the client
  4. Returns the session ID in the response

Session Management

The session ID must be included in subsequent requests:

G P E O T S T / s / s m e e s s a R g e e t ? u s r e n s s s i s o e n s I s d i = o a n b I c d 1 = 2 a 3 b c 1 2 U 3 s e s t h e s e s s i o n

Why SSE?

SSE provides several benefits for MCP servers:

  • Persistent Connection: Maintains state across multiple tool calls
  • Real-time Updates: Server can push updates to the client
  • HTTP-based: Works through firewalls and proxies
  • Efficient: Reuses the same connection for multiple operations

SSE vs WebSockets

MCP uses SSE rather than WebSockets because:

  • Simpler protocol (one-way communication is sufficient)
  • Better HTTP compatibility
  • Automatic reconnection handling in browsers
  • Lower overhead

Testing Your MCP Server

Running the Server

dotnet run

The server will start on http://localhost:5237 (or https://localhost:7107 for HTTPS).

Test Files Setup

Create a .http folder in your project root to organize test files:

M c p R e . m h o t t t e p s t t S / s o i e e o m r . l e v h s / G G e t _ e e r t l t t / p i C C s u u t r r . r r h e e t n n t t t p U T t i c m T e i F m o e r . T h i t m t e p z o n e . h t t p

1. Testing the SSE Endpoint

Create .http/sse.http:

GET http://localhost:5237/sse
Accept: application/json

Expected Response: The connection will open and you’ll receive a session ID in the SSE stream. Copy this session ID for the next tests.

Using cURL:

curl --no-buffer http://localhost:5237/sse

2. Testing Tools List

Create .http/tools_list.http:

POST http://localhost:5237/message?sessionId=YOUR_SESSION_ID
Content-Type: application/json

{
    "method": "tools/list",
    "params": {
        "protocolVersion": "2024-11-05",
        "capabilities": {},
        "clientInfo": {
            "name": "MyClient",
            "version": "1.0.0.0"
        }
    },
    "id": 1,
    "jsonrpc": "2.0"
}

Expected Response:

{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "tools": [
            {
                "name": "get_current_utc_time",
                "description": "",
                "inputSchema": {
                    "type": "object",
                    "properties": {}
                }
            },
            {
                "name": "get_current_time_for_timezone",
                "description": "",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "timeZone": {
                            "type": "string"
                        }
                    },
                    "required": ["timeZone"]
                }
            }
        ]
    }
}

3. Testing GetCurrentUtcTime Tool

Create .http/time/GetCurrentUtcTime.http:

POST http://localhost:5237/message?sessionId=YOUR_SESSION_ID
Content-Type: application/json

{
    "method": "tools/call",
    "params": {
        "protocolVersion": "2024-11-05",
        "name": "get_current_utc_time",
        "capabilities": {},
        "clientInfo": {
            "name": "MyClient",
            "version": "1.0.0.0"
        }
    },
    "id": 1,
    "jsonrpc": "2.0"
}

Expected Response:

{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "content": [
            {
                "type": "text",
                "text": "{\"timeZone\":\"UTC\",\"localTime\":\"2024-10-07T12:34:56.789Z\"}"
            }
        ]
    }
}

4. Testing GetCurrentTimeForTimezone Tool

Create .http/time/GetCurrentTimeForTimezone.http:

POST http://localhost:5237/message?sessionId=YOUR_SESSION_ID
Content-Type: application/json

{
    "method": "tools/call",
    "params": {
        "protocolVersion": "2024-11-05",
        "name": "get_current_time_for_timezone",
        "arguments": {
            "timeZone": "America/New_York"
        },
        "capabilities": {},
        "clientInfo": {
            "name": "MyClient",
            "version": "1.0.0.0"
        }
    },
    "id": 1,
    "jsonrpc": "2.0"
}

Expected Response:

{
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
        "content": [
            {
                "type": "text",
                "text": "{\"timeZone\":\"America/New_York\",\"localTime\":\"2024-10-07T08:34:56.789\"}"
            }
        ]
    }
}

Testing Workflow

  1. Start the server: dotnet run
  2. Get a session ID: Call the SSE endpoint
  3. Replace sessionId: Update YOUR_SESSION_ID in all test files with the actual session ID
  4. List tools: Verify both tools are discovered
  5. Call tools: Test each tool with and without parameters

Common Issues

Issue: “Session not found”

  • Solution: Make sure you’ve called /sse first and are using the correct session ID

Issue: “Tool not found”

  • Solution: Verify the controller has [McpServerToolType] and methods have [McpServerTool]

Issue: “Invalid timezone”

  • Solution: Use valid IANA timezone identifiers (e.g., “America/New_York”, not “EST”)

Complete Example

Here’s the complete, minimal working MCP server:

Project Structure

M c p R e C P M . m o r c h o n o p t t t g R t e r T r e p s t t S o i a m s o i e l m m e o m r l e . t . l e v e C c e h s / G G e r o s S t _ e e r s n e t l t t / / t r p i C C r v s u u o e t r r l r . r r l . h e e e c t n n r s t t t . p p U T c r t i s o c m j T e i F m o e r . T h i t m t e p z o n e . h t t p

Program.cs (6 lines)

var builder = WebApplication.CreateBuilder(args);

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

var app = builder.Build();

app.MapMcp();

app.Run();

TimeController.cs (42 lines)

using Microsoft.AspNetCore.Mvc;
using ModelContextProtocol.Server;

namespace McpRemoteServer.Controllers;

public class TimeResponse
{
    public required string TimeZone { get; set; }
    public DateTime LocalTime { get; set; }
}

[ApiController]
[Route("[controller]")]
[McpServerToolType]
public class TimeController : ControllerBase
{
    [HttpGet]
    [McpServerTool(Description = "Returns the current UTC time")]
    public TimeResponse GetCurrentUtcTime()
    {
        return new TimeResponse
        {
            TimeZone = "UTC",
            LocalTime = DateTime.UtcNow
        };
    }

    [HttpGet]
    [McpServerTool(Description = "Returns the current time for a specific IANA timezone")]
    public TimeResponse GetCurrentTimeForTimezone([FromQuery] string timeZone)
    {
        var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
        var localTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, timeZoneInfo);

        return new TimeResponse
        {
            TimeZone = timeZone,
            LocalTime = localTime
        };
    }
}

That’s it! You now have a fully functional MCP server in less than 50 lines of code.

Next Steps

  • Add More Tools: Create additional controllers with more MCP tools
  • Add Authentication: Secure your MCP server with authentication
  • Deploy: Host your MCP server on Azure, AWS, or your preferred platform
  • Connect to LLMs: Configure Claude or other LLMs to use your MCP server

Resources


This guide demonstrates building an MCP server with ASP.NET Core using the official Microsoft C# SDK for Model Context Protocol.