# C8 — Build a C# MCP Server for BookTracker

> **Summary — what this page covers**
> In **Day 1 Lab 3** you *used* GitHub's MCP server from Claude Code. Now you flip it around and
> *build* one: a new `BookTracker.Mcp` host that exposes BookTracker's services as MCP tools, then
> connect **Claude Code** to it and watch it query your real database.
>
> **Time:** ~45 minutes · **Format:** hands-on, solo · **You start from:** `checkpoint/c7-rag` · **You end at:** `checkpoint/c8-mcp-server`

MCP (Model Context Protocol) is how an AI host like Claude Code talks to your tools. A server you
build is a **thin governed gateway**: Claude never touches the database — it calls *your* tools, which
run *your* validated Core services. The payoff in this lab is the through-line from C4: the same
`IReadingProgressService` you wrote on Day 1 becomes reachable a third way — first as an HTTP endpoint
(C4), then as an in-process agent tool (C6), and now as an MCP tool any host can call (C8). One
service, three surfaces.

> **Prerequisites:** a working local checkout you can run, and the **Claude Code** CLI installed and
> signed in (paid Claude plan). This lab starts from the **C7** checkpoint.

---

## 1. Start from the C7 checkpoint

Each lab begins where the previous one ended. Start a working branch from `checkpoint/c7-rag`, then
move into the solution folder:

```bash
git switch -c my-c8 checkpoint/c7-rag
cd src/BookTracker
```

> The matching tag, **`checkpoint/c8-mcp-server`**, is the answer key. If you get stuck, diff your
> work against it — but try the build yourself first.

---

## 2. Create the `BookTracker.Mcp` project

The MCP server is a **separate host** from `BookTracker.Api` — its own ASP.NET Core web project that
references **Core + Data** and reuses the same services. Create it as a web SDK project and add it to
the solution:

```bash
dotnet new web -n BookTracker.Mcp
dotnet sln BookTracker.sln add BookTracker.Mcp/BookTracker.Mcp.csproj

dotnet add BookTracker.Mcp reference BookTracker.Core/BookTracker.Core.csproj
dotnet add BookTracker.Mcp reference BookTracker.Data/BookTracker.Data.csproj
```

The `.csproj` keeps the web SDK (`Sdk="Microsoft.NET.Sdk.Web"`) and targets `net10.0` — it references
**Core + Data only**, never the Api. The dependency direction stays intact: `Mcp → Core + Data`, just
like the Api.

---

## 3. Add the MCP SDK packages

You need **two** packages — the core SDK and the ASP.NET Core HTTP transport. Both are at the GA
**1.x** line:

```bash
dotnet add BookTracker.Mcp package ModelContextProtocol --version 1.4.0
dotnet add BookTracker.Mcp package ModelContextProtocol.AspNetCore --version 1.4.0
```

> Don't skip `ModelContextProtocol.AspNetCore`. The core `ModelContextProtocol` package gives you the
> server primitives and attributes, but `WithHttpTransport()` and `MapMcp()` — the methods that let
> Claude Code reach the server over HTTP — live in the **`.AspNetCore`** package. You need **both**.

---

## 4. Wire up `Program.cs` — the MCP host

Register the MCP server, reuse the BookTracker stack against the **same SQLite database**, migrate on
startup, and map the MCP endpoint:

```csharp
using BookTracker.Core.Services;
using BookTracker.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// MCP server over Streamable HTTP; tools are discovered from [McpServerTool] methods in this assembly.
builder.Services.AddMcpServer()
    .WithHttpTransport()
    .WithToolsFromAssembly();

// Reuse the BookTracker stack against the same DB. AddBookTrackerData registers the DbContext +
// repositories; the Core services are registered here just as the API host does.
var connectionString = builder.Configuration.GetConnectionString("BookTracker")
    ?? "Data Source=booktracker.db";
builder.Services.AddBookTrackerData(connectionString);
builder.Services.AddScoped<IBookService, BookService>();
builder.Services.AddScoped<IAuthorService, AuthorService>();
builder.Services.AddScoped<IReadingProgressService, ReadingProgressService>();

var app = builder.Build();

// Migrate at startup so the server runs straight from a clone.
using (var scope = app.Services.CreateScope())
    scope.ServiceProvider.GetRequiredService<BookTrackerDbContext>().Database.Migrate();

app.MapMcp();
app.Run();
```

Copy `ConnectionStrings:BookTracker` (`Data Source=booktracker.db`) into `BookTracker.Mcp/appsettings.json`
so the server points at the same database as the Api.

---

## 5. Give it its own port

The Api already uses `~5000`/`5255`. Run the MCP server on its **own** port — **`5100`** — so the two
hosts don't collide. Set it in `BookTracker.Mcp/Properties/launchSettings.json`:

```json
{
  "profiles": {
    "http": {
      "commandName": "Project",
      "applicationUrl": "http://localhost:5100",
      "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }
    }
  }
}
```

Or override it ad hoc: `dotnet run --project BookTracker.Mcp --urls http://localhost:5100`.

---

## 6. Define the tools — `Tools/BookMcpTools.cs`

Tools are attributed methods on a `[McpServerToolType]` class. Services are **DI-injected straight
into the tool method** — the SDK resolves them per call. The `[Description]` is the contract Claude
reads to decide when and how to call a tool, so write it like a skill description. Define three tools,
validating inputs before they reach the data layer:

```csharp
using System.ComponentModel;
using System.Text.Json;
using BookTracker.Core.Dtos;
using BookTracker.Core.Services;
using ModelContextProtocol.Server;

namespace BookTracker.Mcp.Tools;

[McpServerToolType]
public class BookMcpTools
{
    [McpServerTool]
    [Description("Search BookTracker for books whose title matches a query. Returns matching books as JSON.")]
    public async Task<string> SearchBooks(
        IBookService books,
        [Description("Text to match against book titles")] string query,
        CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(query))
            return "Error: query is required.";
        return Serialize(await books.SearchAsync(query, ct));
    }

    [McpServerTool]
    [Description("Get a book's reading progress (page, status, percent complete) by book id.")]
    public async Task<string> GetReadingProgress(
        IReadingProgressService progress,
        [Description("The book id")] int bookId,
        CancellationToken ct)
        // Reuses the C4 service — its business rules hold over MCP just as over HTTP.
        => Serialize(await progress.GetForBookAsync(bookId, ct));

    [McpServerTool]
    [Description("Add a new book to BookTracker. The author must already exist. Returns the created book as JSON.")]
    public async Task<string> AddBook(
        IBookService books,
        [Description("Book title")] string title,
        [Description("Id of an existing author")] int authorId,
        [Description("Total page count")] int totalPages,
        [Description("ISBN (optional)")] string? isbn,
        [Description("Genre (optional)")] string? genre,
        CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(title))
            return "Error: title is required.";

        // CreateAsync returns a BookMutationResult; check its status before returning the book.
        var result = await books.CreateAsync(new CreateBookRequest(title, authorId, isbn, totalPages, genre), ct);
        return result.Status == BookMutationStatus.Success
            ? Serialize(result.Book)
            : $"Error: author {authorId} does not exist.";
    }

    private static string Serialize(object? value) => JsonSerializer.Serialize(value);
}
```

| Tool                   | Calls (reused service)                             |
| ---------------------- | -------------------------------------------------- |
| `search_books`         | `IBookService.SearchAsync`                         |
| `get_reading_progress` | **`IReadingProgressService.GetForBookAsync` (C4)** |
| `add_book`             | `IBookService.CreateAsync`                         |

> Reuse, don't reimplement. Each tool delegates to the same validated Core service the HTTP API uses —
> no new data access lives in the MCP project.

---

## 7. Add the security stubs

A gateway is only as safe as its rules. Add two small teaching stubs under `Security/`.

`Security/McpAuth.cs` — a bearer-token middleware that is a **no-op unless `Mcp:ApiKey` is
configured**, so the server runs locally without setup but teaches the pattern:

```csharp
namespace BookTracker.Mcp.Security;

public class McpAuth
{
    private readonly RequestDelegate _next;
    private readonly string? _apiKey;

    public McpAuth(RequestDelegate next, IConfiguration configuration)
    {
        _next = next;
        _apiKey = configuration["Mcp:ApiKey"];
    }

    public async Task InvokeAsync(HttpContext context)
    {
        if (!string.IsNullOrEmpty(_apiKey)
            && context.Request.Headers.Authorization.ToString() != $"Bearer {_apiKey}")
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("Unauthorized: missing or invalid bearer token.");
            return;
        }
        await _next(context);
    }
}
```

`Security/AuditLogger.cs` — logs every tool invocation so you can answer "what did the agent do?":

```csharp
namespace BookTracker.Mcp.Security;

public class AuditLogger
{
    private readonly ILogger<AuditLogger> _logger;
    public AuditLogger(ILogger<AuditLogger> logger) => _logger = logger;

    public void ToolInvoked(string toolName, string argumentsSummary)
        => _logger.LogInformation("MCP tool invoked: {Tool} args=({Args})", toolName, argumentsSummary);
}
```

Register `AuditLogger` (`builder.Services.AddScoped<AuditLogger>();`) and add the middleware
(`app.UseMiddleware<McpAuth>();` before `app.MapMcp();`). You can then inject `AuditLogger` into each
tool method and call `audit.ToolInvoked(...)` at the top — the answer-key tag does exactly this.

Build it:

```bash
dotnet build BookTracker.sln
```

---

## 8. Connect Claude Code and call a tool

Run the server, then register it with Claude Code as an HTTP MCP server:

```bash
dotnet run --project BookTracker.Mcp --urls http://localhost:5100   # leave this running

# in another terminal, from src/BookTracker:
claude mcp add --transport http booktracker http://localhost:5100
claude mcp list                                                     # booktracker - connected
```

Now ask Claude Code something that needs your tools:

```
Search BookTracker for books about space
```

Watch `search_books` run against the **real database** and return seeded books. Then prove the
through-line:

```
What is the reading progress for book 1?
```

`get_reading_progress` calls the **same C4 service** — the one already exposed as a C4 HTTP endpoint
and a C6 agent tool — now reachable over MCP. One service, three surfaces.

---

## ✅ Checkpoint — you're done when:

- [ ] `BookTracker.Mcp` (web SDK, refs Core + Data) builds and **runs on its own port** (`5100`).
- [ ] `ModelContextProtocol` + `ModelContextProtocol.AspNetCore` (both 1.4.0) referenced; `WithHttpTransport()` + `MapMcp()` wired.
- [ ] **3+ tools** with typed params and clear `[Description]`s, each calling a **real Core service**; inputs validated.
- [ ] `claude mcp list` shows **`booktracker` connected**, and Claude Code invokes a tool against the DB.
- [ ] `get_reading_progress` returns the **C4** service's data (honoring its rules).
- [ ] Auth (`McpAuth`) and audit (`AuditLogger`) stubs are present.

Tag or compare your work against **`checkpoint/c8-mcp-server`**.

---

## What's next

**Lab 5 (C8 → C9):** you'll have Claude generate tests for the MCP server and the rest of the
solution, then wire up **CI/CD** so the whole thing — Api and MCP server alike — builds and tests on
every push.
