Skip to main content
Glama
README.md
# todo-mcp-server

The same todo app served four ways, to isolate what changes when you stop
hand-writing MCP tools and serve a tRPC router instead.

|             | MCP tools by hand | tRPC router          |
| ----------- | ----------------- | -------------------- |
| **no auth** | `1_simple`        | `3_trpc2mcp`         |
| **auth**    | `2_authenticated` | `4_trpc2mcp_authed`  |

Shared at the root, so each stage is only its routing layer: `services.ts` (the
todo domain), `auth.ts` (bearer middleware + tRPC context), `trpc-mcp.ts` (the
tRPC → MCP adapter, adapted from [Jacse/trpc-mcp](https://github.com/Jacse/trpc-mcp)).

TypeScript on Bun, Express, no build step.

## Run

```
bun install
bun run start:simple          # stdio
bun run start:authenticated   # :3000/mcp
bun run start:trpc            # :3001/trpc
bun run start:trpc-mcp        # stdio, same router as start:trpc
bun run start:trpc-authed     # :3002 — /trpc and /mcp, one router, one gate
```

The bearer token *is* the user id; `alice` is the only one with the `premium` scope.

## The idea

Opt a procedure in with one `.meta()` key and `trpc-mcp.ts` walks the router and
registers it as a tool. No `meta.mcp` (see `stats`) means it stays tRPC-only.

```ts
addTodo: t.procedure
  .meta({ mcp: { enabled: true, name: "add_todo", description: "Add a new todo item" } })
  .input(z.object({ text: z.string().describe("Todo text") }))
  .mutation(({ input }) => Todos.add(user, input.text)),
```

Auth then only gets written once. `2_authenticated/` reads `extra.authInfo` in
every handler and gates `export_todos` inline; `4_trpc2mcp_authed/` does both in
tRPC middleware, and the `/trpc` and `/mcp` mounts inherit it from the same
`req.auth` — see `examples/4_trpc2mcp_authed/server.ts`.