Skip to content
mdr
All work
live2025·open-source

MCP Todo Flow

Reference implementation for how an AI agent, an MCP server, a REST API, and a UI share state in production.

Screenshot of MCP Todo Flow

TL;DR

  • Shared business logic prevents drift across MCP + REST.
  • Tool output serves model + program simultaneously.
  • Production deployment with health checks, CORS, persistence.
  • Forkable as a starting point.

Architecture

Flow
CLIENTPROTOCOLCORESTORAGEDemo agentMCP clientWeb appVite + ReactMCP serverstdio transportREST APIExpresspackages/sharedbusiness logicTSSQLitesingle source of truth

Shared core. MCP exposes capabilities, REST exposes endpoints, the UI never knows MCP exists.

sync · HTTP / RPCdata · read / write

Problem

Most MCP examples are toy demos — a single tool wired to a chat. They don't show the real-world shape: an agent, an MCP server, a REST API, and a UI all needing to share state without drift, with auth, with persistence, with health checks.

Approach

A four-package monorepo: MCP server, REST API, demo agent, web UI — all sharing one SQLite source of truth via packages/shared. Tool outputs serve two consumers at once (LLM-readable text + program-readable structuredContent). Full Docker Compose deployment with Nginx reverse proxy, named volume for persistence.

Deep dive

Shared business logic, two protocols

Both the MCP server and the REST API import from packages/shared. Adding a new operation is one function, two two-line wrappers — and the agent and the UI both see it. No drift, no copy-paste, no 'which one is canonical?'.

Dual-target tool output

MCP tools return both content (text for the model) and structuredContent (a typed object for programs). The web UI consumes the structured form. The agent consumes the text. The server emits both from the same call — no second source of truth.

tspackages/mcp-server/tools/listTodos.ts — one call, two consumers
server.tool("list_todos", "List open todos.", async () => {
  const todos = await shared.todos.listOpen();

  return {
    content: [
      {
        type: "text",
        text: todos.length
          ? todos.map((t, i) => `${i + 1}. ${t.title}`).join("\n")
          : "No open todos.",
      },
    ],
    structuredContent: {
      todos: todos.map(({ id, title, dueAt }) => ({ id, title, dueAt })),
    },
  };
});

Production deployment, not just a demo

Docker Compose with Nginx gateway, compiled Node backend, static Vite frontend, named volume for SQLite persistence, health checks on /healthz. The whole thing comes up with one command.

Outcome

Public reference implementation. Production-grade Docker setup. Used as a teaching artifact for the MCP integration pattern. Forkable as a starting point for real integrations.

Related

More in ai.