Skip to content

Commit 8b32809

Browse files
committed
Rewrite generated project README: setup, endpoints, auth, clients, MCP App UI dev and build
1 parent 9ef0d37 commit 8b32809

1 file changed

Lines changed: 252 additions & 15 deletions

File tree

Lines changed: 252 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,273 @@
11
# DotnetMcpTemplate
22

3-
An MCP server on ASP.NET Core (.NET 10) with the official MCP C# SDK 2.0.
3+
A [Model Context Protocol](https://modelcontextprotocol.io) server on ASP.NET Core (.NET 10) with the official MCP C#
4+
SDK 2.0. It exposes tools, resources and prompts over Streamable HTTP, signs users in through any OpenID Connect
5+
identity provider, and renders interactive views in the chat with MCP Apps.
6+
7+
- [Quick start](#quick-start)
8+
- [Endpoints](#endpoints)
9+
- [Configuration](#configuration)
10+
- [Authentication](#authentication)
11+
- [Connecting clients](#connecting-clients)
12+
- [MCP App UI](#mcp-app-ui)
13+
- [Project structure](#project-structure)
14+
- [Development](#development)
15+
- [Testing](#testing)
16+
- [Docker](#docker)
17+
- [Documentation](#documentation)
18+
- [Troubleshooting](#troubleshooting)
19+
20+
## Quick start
21+
22+
**Requirements:** [.NET 10 SDK](https://dotnet.microsoft.com/download) (version pinned in `global.json`).
23+
Optional: Docker; Node 22+ with pnpm (`corepack enable`) for the MCP App UI.
424

525
```bash
626
cp .env.example .env
27+
```
28+
29+
Set `MCP_AUTH_MODE=none` in `.env` for a first run without an identity provider (only allowed with `APP_ENV=dev`),
30+
then start the server:
31+
32+
```bash
733
dotnet run --project src/DotnetMcpTemplate
34+
# info: Server running at http://localhost:5080
35+
```
36+
37+
Try it with the MCP Inspector:
38+
39+
```bash
40+
npx @modelcontextprotocol/inspector
41+
# Transport: Streamable HTTP · URL: http://localhost:5080/mcp · Connect → Tools → List Tools
42+
```
43+
44+
## Endpoints
45+
46+
| Path | Purpose |
47+
| --- | --- |
48+
| `/mcp` | MCP endpoint (Streamable HTTP, stateless). Change with `MCP_PATH` |
49+
| `/` | Server info: `{ name, version, status, mcp }` |
50+
| `/health` | Health check for load balancers and orchestrators |
51+
| `/.well-known/oauth-protected-resource/mcp` | Protected resource metadata (tells clients where to sign in) |
52+
| `/.well-known/oauth-authorization-server` | OAuth metadata of the built-in OIDC proxy |
53+
| `/oauth/*` | OIDC proxy: register, authorize, consent, callback, token |
54+
55+
## Configuration
56+
57+
All settings are environment variables, usually from `.env` (never committed; `.env.example` lists every variable).
58+
Real environment variables (Docker, CI, shell) take precedence. Settings are validated at startup and every problem
59+
is reported by variable name.
60+
61+
| Variable | Default | Purpose |
62+
| --- | --- | --- |
63+
| `APP_ENV` | `dev` | `dev`, `stage` or `prod` |
64+
| `APP_PORT` | `5080` | Listening port (IPv4 and IPv6: `localhost`, `127.0.0.1`, `[::1]`) |
65+
| `APP_URL` | `http://localhost:{APP_PORT}` | Public URL; set it behind a reverse proxy |
66+
| `MCP_SERVER_NAME`, `MCP_PATH`, `MCP_INSTRUCTIONS` | | Server identity and endpoint |
67+
| `MCP_AUTH_MODE` | `required` | `required`, `mixed` or `none` (dev only) |
68+
| `AUTH_PROVIDER` | `oidc` | `oidc`, `jwt` or a custom provider |
69+
| `OIDC_*` | | Identity provider: discovery URL, client id and secret, scopes, claims |
70+
| `AUTH_*` | | Token signing key and lifetimes, allowed redirect URIs, store (`memory` or `redis`) |
71+
| `SAMPLE_API_*` | | Example external API client; one `{PREFIX}_*` set per API |
72+
73+
Details: [configuration guide](docs/development/configuration-and-environments.md).
74+
75+
## Authentication
76+
77+
With `AUTH_PROVIDER=oidc` the server is the OAuth authorization server for MCP clients and signs users in at your
78+
identity provider (Entra ID, Google, Auth0, Okta, Keycloak, Zitadel, ...):
79+
80+
1. Register one web application at the identity provider with the redirect URI
81+
`http://localhost:5080/oauth/callback` (in general `${APP_URL}/oauth/callback`).
82+
2. Set `OIDC_DISCOVERY_URL`, `OIDC_CLIENT_ID` and `OIDC_CLIENT_SECRET` in `.env`, and `MCP_AUTH_MODE=required`.
83+
3. Connect a client: it registers, shows the consent page, and sends you to the identity provider to sign in.
84+
85+
Tools, resources and prompts declare `[Authorize]` or `[AllowAnonymous]`. Scopes are defined in
86+
`Core/Auth/AppScopes.cs` and policies in `Core/Auth/Policies.cs`. For multiple instances set `AUTH_STORE=redis`,
87+
`REDIS_URL` and the same `AUTH_TOKEN_SIGNING_KEY` everywhere.
88+
89+
Details: [authentication and authorization](docs/development/authentication-and-authorization.md).
90+
91+
## Connecting clients
92+
93+
The server URL is `APP_URL` + `MCP_PATH`, for example `https://mcp.example.com/mcp`.
94+
95+
| Client | How |
96+
| --- | --- |
97+
| Claude (claude.ai, Desktop) | Settings → Connectors → Add custom connector → the URL |
98+
| VS Code | `.vscode/mcp.json`: `{ "servers": { "my-server": { "type": "http", "url": "…/mcp" } } }` |
99+
| Cursor | `~/.cursor/mcp.json`: `{ "mcpServers": { "my-server": { "url": "…/mcp" } } }` |
100+
| MCP Inspector | `npx @modelcontextprotocol/inspector` → Streamable HTTP → the URL |
101+
102+
A client's redirect URI must match `AUTH_ALLOWED_REDIRECT_URIS`. Details: [connecting clients](docs/development/connecting-clients.md).
103+
104+
## MCP App UI
105+
106+
MCP Apps let a tool render an interactive view inside the conversation. The views live in `ui/` (React 19, Vite,
107+
Tailwind 4, `@modelcontextprotocol/ext-apps` 2). Each view builds to one self-contained HTML file in
108+
`src/DotnetMcpTemplate/ui_dist/`, which the server serves as a `ui://` resource. The built bundles are committed, so
109+
the server runs without Node; you only need the UI toolchain to change a view.
110+
111+
The sample view `projects-dashboard` belongs to the `show_projects_dashboard` tool. It renders the tool's result and
112+
loads an owner's projects by calling `list_projects` from the view.
113+
114+
### Setup
115+
116+
```bash
117+
corepack enable # once per machine: provides pnpm
118+
cd ui
119+
pnpm install
120+
```
121+
122+
### Develop
123+
124+
**Sandbox** (no server, no host, no login). Each view renders with sample data from `ui/src/apps/<entry>/sandbox.ts`,
125+
including the tools it calls, so drill-down, paging and loading states all work:
126+
127+
```bash
128+
cd ui
129+
pnpm dev # http://localhost:5173: sandbox index; open a view from there
8130
```
9131

10-
Connect MCP clients to `http://localhost:5080/mcp`. `GET /` returns server info.
132+
Edits hot-reload in the browser. This is the fastest loop for layout and interaction work.
133+
134+
**With the server** (real data, real tool calls). Run the server and the UI watcher side by side:
135+
136+
```bash
137+
# terminal 1: server with hot reload
138+
dotnet watch --project src/DotnetMcpTemplate
139+
140+
# terminal 2: rebuild bundles on save (all views, or name one)
141+
cd ui && pnpm run watch # or: pnpm run watch projects-dashboard
142+
```
143+
144+
In development the server reads bundles straight from `src/DotnetMcpTemplate/ui_dist/`, so each rebuild is served on
145+
the next `resources/read` without a restart. Open the view from an MCP client that supports MCP Apps by asking it to
146+
run the tool (e.g. "show the projects dashboard").
147+
148+
### Build
149+
150+
```bash
151+
cd ui
152+
pnpm run build # typecheck + build every view
153+
pnpm run build projects-dashboard # typecheck + build only the named views
154+
pnpm run entries # list the views
155+
pnpm run typecheck # typecheck only
156+
```
157+
158+
Output: `src/DotnetMcpTemplate/ui_dist/<entry>.html`. **Commit it with your UI changes.** Builds are reproducible,
159+
and CI fails when the committed bundles don't match the source. The Docker image rebuilds the UI from
160+
`ui/pnpm-lock.yaml` as well. Production bundles contain no sample data: opening one directly shows
161+
"Open this view in an MCP host".
162+
163+
### Add a view
164+
165+
1. Create `ui/src/entries/<entry>.html` and `<entry>.tsx` (copy the `projects-dashboard` pair). Names are kebab-case.
166+
2. Create `ui/src/apps/<entry>/`: `schema.ts` (zod schemas mirroring the C# response records), `sandbox.ts`,
167+
`lib/use<Entry>.ts`, `components/`, `App.tsx`.
168+
3. Add a resource in `Capabilities/Resources/UiResources.cs` that returns `bundles.Read("<entry>")` as
169+
`ui://<entry>`, and put `[McpAppUi(ResourceUri = "ui://<entry>")]` and `UseStructuredContent = true` on the tool.
170+
4. `pnpm run build <entry>` and commit the bundle.
171+
172+
Patterns (host connection, schemas, errors, theming): [ui/README.md](ui/README.md) ·
173+
[MCP Apps guide](docs/development/mcp-apps.md).
174+
175+
## Project structure
11176

12177
```text
13178
.
14179
├── src/DotnetMcpTemplate/
15180
│ ├── Program.cs
16181
│ ├── Capabilities/ What the server exposes (thin): Tools/, Resources/, Prompts/
17182
│ ├── Services/ Business logic
18-
│ ├── Models/ Request/response DTOs
183+
│ ├── Models/ Request / response DTOs
19184
│ ├── Integrations/ One folder per external API (typed client + its models), IntegrationsSetup.cs
20-
│ └── Core/ Plumbing you rarely touch
21-
│ ├── ApiClients/ ApiClient base, registration, resilience, auth handlers
22-
│ ├── Auth/ Providers (OIDC proxy for any IdP, JWT, Dev), scopes, policies, AppUser
23-
│ ├── Mcp/ Server setup, handler registry, Filters/ (logging, errors), Apps/ (MCP App bundles)
24-
│ ├── Validation/ Argument validation for every call
25-
│ ├── Common/ Settings/ (.env, validated typed settings), Errors/, Middleware/
26-
│ └── ServerInfo/ GET / server info
27-
├── tests/ Unit, Integration (real MCP client, full OAuth flow), Architecture; mirror src/
28-
├── ui/ React + Vite workspace for MCP App views
185+
│ ├── Core/ Plumbing you rarely touch
186+
│ │ ├── ApiClients/ ApiClient base, registration, resilience, auth handlers
187+
│ │ ├── Auth/ Providers (OIDC proxy, JWT, dev), scopes, policies, AppUser
188+
│ │ ├── Mcp/ Server setup, handler registry, Filters/ (logging, errors), Apps/ (bundles)
189+
│ │ ├── Validation/ Argument validation for every call
190+
│ │ ├── Common/ Settings/ (.env, typed settings), Errors/, Middleware/
191+
│ │ └── ServerInfo/ GET /
192+
│ └── ui_dist/ Built MCP App bundles (committed)
193+
├── ui/ MCP App views: entries/, apps/<entry>/, shared/
194+
├── tests/ Unit, Integration, Architecture (mirror src/)
195+
├── docs/ Guides (development/) and architecture decisions (adr/)
29196
├── docker/ Dockerfile, Compose
30-
├── docs/ ADRs (adr/) and guides (development/)
31197
├── global.json Pinned .NET SDK
32198
└── Directory.*.props Shared build settings, central package versions
33199
```
34200

35-
Guides, organized by task: [docs/development/README.md](docs/development/README.md). Architecture decisions:
36-
[docs/README.md](docs/README.md). MCP App views: [ui/README.md](ui/README.md).
201+
Calls flow one way: **Capabilities → Services → Integrations → external APIs**. Architecture tests enforce it.
202+
203+
## Development
204+
205+
```bash
206+
dotnet watch --project src/DotnetMcpTemplate # hot reload; press Ctrl+R to restart after editing .env
207+
```
208+
209+
Adding a tool is one class; tools are discovered automatically:
210+
211+
```csharp
212+
[McpServerToolType]
213+
[Authorize]
214+
public sealed class InvoiceTools(IInvoiceService invoices)
215+
{
216+
[McpServerTool(Name = "get_invoice", ReadOnly = true, UseStructuredContent = true)]
217+
[Description("Gets an invoice by number.")]
218+
public Task<InvoiceDto> GetInvoice(
219+
[Required, RegularExpression("^INV-[0-9]{6}$"), Description("Invoice number, e.g. INV-000123.")] string number,
220+
AppUser user,
221+
CancellationToken cancellationToken) =>
222+
invoices.GetAsync(number, user, cancellationToken);
223+
}
224+
```
225+
226+
Keep tools thin: validate input with attributes, call one service, return a DTO. Business rules go in `Services/`,
227+
external calls in `Integrations/`. Throw `AppException.NotFound/Conflict/Rule(...)` for expected errors; the model
228+
receives the message. See [tools, resources and prompts](docs/development/tools-resources-prompts.md).
229+
230+
## Testing
231+
232+
```bash
233+
dotnet test # unit, integration and architecture tests
234+
dotnet test --filter "FullyQualifiedName~ToolTests"
235+
cd ui && pnpm run typecheck # MCP App views
236+
```
237+
238+
Integration tests start the server in memory and call it through a real MCP client, including the complete OAuth
239+
flow against a fake identity provider. Architecture tests fail the build when a tool skips `[Authorize]` /
240+
`[AllowAnonymous]`, calls an API client directly, or a service leaks external models.
241+
See [testing](docs/development/testing.md).
242+
243+
## Docker
244+
245+
```bash
246+
docker compose -f docker/docker-compose.yml up --build # local, uses .env
247+
docker build -f docker/Dockerfile -t dotnetmcptemplate-slug . # image only
248+
```
249+
250+
The image builds the MCP App bundles when `ui/pnpm-lock.yaml` exists, then runs the server on `APP_PORT` over plain
251+
HTTP. In production, terminate TLS at a reverse proxy and set `APP_URL` to the public HTTPS URL.
252+
253+
## Documentation
254+
255+
| | |
256+
| --- | --- |
257+
| [Development guides](docs/development/README.md) | Every guide, organized by task: run, build, connect, secure, test |
258+
| [MCP App UI workspace](ui/README.md) | Layout and patterns of the `ui/` workspace |
259+
| [Architecture decisions](docs/README.md) | Why the code is shaped this way (ADRs) |
260+
261+
## Troubleshooting
262+
263+
| Symptom | Fix |
264+
| --- | --- |
265+
| Settings error at startup | The message names each variable; compare `.env` with `.env.example` |
266+
| `MCP_AUTH_MODE=none` is refused | Only allowed with `APP_ENV=dev` |
267+
| Login fails with `IDX20803: Unable to obtain configuration` | Open `OIDC_DISCOVERY_URL` in a browser: it must return JSON (note the dot in `/.well-known/`) |
268+
| Identity provider says the redirect URI is invalid | Register `${APP_URL}/oauth/callback` exactly, not `APP_URL` alone |
269+
| A client can't register (`invalid_redirect_uri`) | Add its redirect URI pattern to `AUTH_ALLOWED_REDIRECT_URIS` |
270+
| Clients are signed out after every restart | `AUTH_STORE=memory` keeps state in memory; use `redis` to persist it |
271+
| `.env` changes have no effect under `dotnet watch` | Settings are read at startup: press Ctrl+R in the watch terminal |
272+
| `UI bundle not found at …/ui_dist/<entry>.html` | `cd ui && pnpm install && pnpm run build` |
273+
| CI fails on "Committed bundles match the source" | `cd ui && pnpm run build`, then commit `src/DotnetMcpTemplate/ui_dist/` |

0 commit comments

Comments
 (0)