Content
# MCP Learning Project
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 on
`ModelContextProtocol.AspNetCore` that exposes tools, resources, and prompts.
- `SimpleMcpClient` — a console client built on `ModelContextProtocol` that
connects over Streamable HTTP, lists every capability the server advertises,
and exercises each one.
## Repository layout
```
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
```
## Requirements
- .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` |
## Running the server
```powershell
cd SimpleMcpServer
dotnet run --launch-profile http
```
The 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:
```json
{
"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."
}
```
## Running the client
In a second terminal, with the server already running:
```powershell
cd SimpleMcpClient
dotnet run # uses default http://localhost:5116/
dotnet run -- http://localhost:5116/ # or pass a custom endpoint
```
Expected 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.
```
## Server features
### Tools (14)
| 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. |
### Resources
- **Static**: `info://server` returns JSON metadata (server name, version, host, runtime).
- **Templated**: `greeting://{name}` and `echo://{message}` return text computed from the URI segment.
### Prompts
- `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.
## How it is wired
### Server registration (`SimpleMcpServer/Program.cs`)
```csharp
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.
### Tool authoring pattern
```csharp
[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.
### Client connection (`SimpleMcpClient/Program.cs`)
```csharp
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 });
```
## Using the server from an MCP-aware tool
### Cursor (`~/.cursor/mcp.json`)
```json
{
"mcpServers": {
"SimpleMcp": {
"type": "http",
"url": "http://localhost:5116/"
}
}
}
```
### Claude Code
```powershell
claude mcp add --transport http simple http://localhost:5116/
```
> **Note on HTTPS**: the project ships with an `http` launch profile on
> `localhost:5116`. If you switch to `https://localhost:7297`, most MCP clients
> will refuse the ASP.NET dev cert. Either keep using `http` for local
> development, or run `dotnet dev-certs https --trust` and ensure the cert is in
> the trust store the client uses.
## Error handling
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.
## License
For learning / demo purposes. No warranty.