Build a todo manager

Every code sample on this page is real — it's the exact server we used to verify this tutorial, tested with the actual MCP Inspector CLI before publishing. By the end, you'll have an MCP server where every user only ever sees their own todos, enforced by real per-user identity, not a shared API key.

Prefer Python? Same tutorial, built on FastMCP →

What you'll have at the end

  • An MCP server with three tools: add_todo, list_todos, complete_todo.
  • Every tool call authenticated by a real mcpauth-issued token.
  • Per-user data isolation — Alice's todos are invisible to Bob, and vice versa.
  • A working server you can test live with MCP Inspector.

1. Create a project

Sign in at getmcpauth.dev/dashboard and create a project. Copy the registration secret it shows you — it's shown once.

2. Install dependencies

npm install express getmcpauth @modelcontextprotocol/sdk zod

3. Build the tools

Each tool reads extra.authInfo.extra.sub— the authenticated user's subject, populated automatically once mcpAuth() verifies the request — and scopes the in-memory store to that user:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

// Keyed by the authenticated user's subject — this is the whole point:
// each user only ever sees their own todos.
const todosBySubject = new Map();

function getTodos(subject) {
  if (!todosBySubject.has(subject)) todosBySubject.set(subject, []);
  return todosBySubject.get(subject);
}

function buildServer() {
  const server = new McpServer({ name: "todo-manager", version: "1.0.0" });

  server.registerTool(
    "add_todo",
    { title: "Add todo", description: "Add a new todo item.", inputSchema: { text: z.string() } },
    async ({ text }, extra) => {
      const subject = extra.authInfo?.extra?.sub;
      const todos = getTodos(subject);
      const todo = { id: todos.length + 1, text, done: false };
      todos.push(todo);
      return { content: [{ type: "text", text: `Added #${todo.id}: ${text}` }] };
    },
  );

  server.registerTool(
    "list_todos",
    { title: "List todos", description: "List this user's todos.", inputSchema: {} },
    async (_args, extra) => {
      const subject = extra.authInfo?.extra?.sub;
      const todos = getTodos(subject);
      const text = todos.length === 0
        ? "No todos yet."
        : todos.map((t) => `#${t.id} [${t.done ? "x" : " "}] ${t.text}`).join("\n");
      return { content: [{ type: "text", text }] };
    },
  );

  return server;
}

4. Protect the server

Wrap the MCP route with mcpAuth(). Requests without a valid token never reach your tool handlers:

import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { mcpAuth } from "getmcpauth";

const app = express();
app.use(express.json());

// Everything under /mcp requires a valid mcpauth token.
app.use("/mcp", mcpAuth({ registrationSecret: process.env.MCPAUTH_SECRET }));

app.post("/mcp", async (req, res) => {
  const server = buildServer();
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(4321, () => console.log("todo-manager listening on :4321"));

5. Test it live

Run the server, then use MCP Inspector to add a todo:

npx @modelcontextprotocol/inspector --cli http://localhost:4321/mcp \
  --transport http --header "Authorization: Bearer <your token>" \
  --method tools/call --tool-name add_todo --tool-arg text="Buy milk"

List it back — you'll only ever see todos added under the same token's identity:

npx @modelcontextprotocol/inspector --cli http://localhost:4321/mcp \
  --transport http --header "Authorization: Bearer <your token>" \
  --method tools/call --tool-name list_todos

Try it with a second token minted for a different subject — the list comes back empty. That's real per-user isolation, enforced by the token, not by anything your server has to implement itself.

Closing notes

If your product already has its own login system and you'd rather not send users through mcpauth's own GitHub-based consent screen, see server-to-server token minting — your backend can mint a token for an already-authenticated user directly.