A minimal end-to-end Model Context Protocol sample written in .NET 9, showing both sides of the protocol:
SimpleMcpServer— an ASP.NET Core MCP server built onModelContextProtocol.AspNetCorethat exposes tools, resources, and prompts.SimpleMcpClient— a console client built onModelContextProtocolthat connects over Streamable HTTP, lists every capability the server advertises, and exercises each one.
MCP/
├── SimpleMcpServer/ # ASP.NET Core MCP server (HTTP transport)
│ ├── Program.cs # Server bootstrap + ServerInfo / instructions
│ ├── appsettings.json # Logging configuration
│ ├── Tools/
│ │ ├── CalculatorTools.cs # calc_add, calc_subtract, calc_multiply,
│ │ │ # calc_divide, calc_power, calc_sqrt
│ │ ├── TextTools.cs # text_echo, text_reverse, text_word_count,
│ │ │ # text_to_upper, text_to_lower
│ │ └── TimeTools.cs # time_now_utc, time_now_in_zone,
│ │ # time_diff_seconds
│ ├── Resources/
│ │ └── AppResources.cs # info://server (static)
│ │ # greeting://{name}, echo://{message} (templated)
│ └── Prompts/
│ └── AppPrompts.cs # code_review, summarize, explain_like_im_five
│
└── SimpleMcpClient/ # Console MCP client
└── Program.cs # Connects, lists, and calls every capability
- .NET 9 SDK
- Windows, macOS, or Linux
NuGet packages (already referenced):
| Project | Package | Version |
|---|---|---|
SimpleMcpServer |
ModelContextProtocol.AspNetCore |
1.3.0 |
SimpleMcpClient |
ModelContextProtocol |
1.3.0 |
cd SimpleMcpServer
dotnet run --launch-profile httpThe server listens on http://localhost:5116/. The MCP Streamable HTTP endpoint is
mounted at the root path (/) by app.MapMcp().
Server identity advertised to clients:
{
"name": "SimpleMcpServer",
"version": "1.0.0",
"instructions": "A demo MCP server exposing calculator, text and time tools, static and templated resources, and reusable prompt templates."
}In a second terminal, with the server already running:
cd SimpleMcpClient
dotnet run # uses default http://localhost:5116/
dotnet run -- http://localhost:5116/ # or pass a custom endpointExpected output (abridged):
Connected. Server: SimpleMcpServer v1.0.0
== Tools ==
calc_add Adds two numbers and returns the sum.
...
-> calc_add(2, 3)
-> 5
-> text_reverse("Hello, MCP!")
-> !PCM ,olleH
-> calc_divide(10, 0) // expected to fail
(server reported tool error)
-> An error occurred invoking 'calc_divide': Cannot divide by zero.
== Resources ==
info://server Static metadata about this MCP server (name, version, build host).
== Resource templates ==
greeting://{name} Returns a friendly greeting for the given name.
echo://{message} Echoes the message segment of the URI back to the caller.
| Name | Description |
|---|---|
calc_add, calc_subtract, calc_multiply, calc_divide, calc_power, calc_sqrt |
Basic arithmetic; calc_divide and calc_sqrt validate inputs and throw McpException on bad arguments. |
text_echo, text_reverse, text_word_count, text_to_upper, text_to_lower |
String utilities. |
time_now_utc, time_now_in_zone, time_diff_seconds |
Time helpers; time_now_in_zone accepts IANA or Windows time-zone ids. |
- Static:
info://serverreturns JSON metadata (server name, version, host, runtime). - Templated:
greeting://{name}andecho://{message}return text computed from the URI segment.
code_review(code, language="csharp")— emits a structured code-review prompt.summarize(text, maxSentences=3)— emits a summarization prompt.explain_like_im_five(topic)— emits an ELI5 prompt.
builder.Services
.AddMcpServer(options =>
{
options.ServerInfo = new Implementation { Name = "SimpleMcpServer", Version = "1.0.0" };
options.ServerInstructions = "...";
})
.WithHttpTransport()
.WithToolsFromAssembly()
.WithPromptsFromAssembly()
.WithResourcesFromAssembly();
app.MapMcp();WithToolsFromAssembly() (and the prompt/resource equivalents) auto-register every
class tagged with [McpServerToolType] / [McpServerPromptType] /
[McpServerResourceType]. Adding a new file is the only step needed for new
capabilities; no manual registration is required.
[McpServerToolType]
public sealed class CalculatorTools
{
[McpServerTool(Name = "calc_add"), Description("Adds two numbers.")]
public double Add(
[Description("First addend.")] double a,
[Description("Second addend.")] double b) => a + b;
}The [Description] attributes flow into the JSON schema clients receive — that is
what lets an LLM pick the right tool and fill its arguments correctly.
var transport = new HttpClientTransport(new HttpClientTransportOptions
{
Endpoint = new Uri("http://localhost:5116/"),
Name = "SimpleMcpClient"
// TransportMode defaults to AutoDetect:
// tries Streamable HTTP, falls back to SSE
});
await using var client = await McpClient.CreateAsync(transport, new McpClientOptions
{
ClientInfo = new Implementation { Name = "SimpleMcpClient", Version = "1.0.0" }
});
var tools = await client.ListToolsAsync();
var result = await client.CallToolAsync("calc_add",
new Dictionary<string, object?> { ["a"] = 2, ["b"] = 3 });{
"mcpServers": {
"SimpleMcp": {
"type": "http",
"url": "http://localhost:5116/"
}
}
}claude mcp add --transport http simple http://localhost:5116/Note on HTTPS: the project ships with an
httplaunch profile onlocalhost:5116. If you switch tohttps://localhost:7297, most MCP clients will refuse the ASP.NET dev cert. Either keep usinghttpfor local development, or rundotnet dev-certs https --trustand ensure the cert is in the trust store the client uses.
Tools raise McpException for invalid arguments (e.g. divide by zero, bad
time-zone, malformed timestamp). ModelContextProtocol.Core 1.3.0 only exposes
the standard Exception constructors on McpException, so error codes are not
specified explicitly — the message is what reaches the client as an IsError=true
tool result.
For learning / demo purposes. No warranty.