mcp-todo-demo

mcp-todo-demo

A minimal MCP server that provides an interactive to-do list with checkboxes in chat, demonstrating MCP Apps UI resource integration and tool-based state updates.

Category
Visit Server

README

mcp-todo-demo

A minimal MCP server that shows an interactive to-do list (checkboxes) as in-chat UI, for teaching the basics of mcp-ui / MCP Apps:

  • widget/ - the actual UI: a real standalone React app (a normal Vite + React project), built to a single self-contained widget/dist/index.html.
  • The widget is registered as its own MCP resource (registerAppResource, URI ui://todo-list/view.html) - per the MCP Apps spec (@modelcontextprotocol/ext-apps), the UI is a separate resource that tools link to, not something a tool embeds in its own result. src/server.ts uses createUIResource's adapters.mcpApps option to inject a small runtime script translating the widget's plain postMessage calls into whatever wire format the host's MCP Apps implementation expects.
  • list_todos (registerAppTool) - links to that resource via _meta.ui.resourceUri, so the host renders it as an interactive checkbox UI.
  • toggle_todo - called automatically when the user checks/unchecks an item. The checkbox's onChange does window.parent.postMessage({ type: 'tool', ... }), the host turns that into a real tool call, and the result goes back into the conversation - that's how the LLM "knows" what's done. It's a normal, model-visible tool (not app-only), so its result - an encouraging confirmation, e.g. "Buy milk" is done! 2 to go - next up: "Walk the dog"
    • can surface as a real chat reply on its own. (An earlier version of this demo also sent a {type:'prompt', payload:{prompt}} action to force a chat reply, but that always renders as a fake role: 'user' message in the host - removed once toggle_todo itself could produce a visible reply.)
  • The host reuses the same mounted widget panel rather than recreating it from scratch on every linked tool call - it doesn't re-run our bundle's initial window.__TODOS__ injection a second time. Instead, it pushes each tool's result into the already-open iframe as a ui-lifecycle-iframe-render-data message (payload.renderData.toolOutput). Both list_todos and toggle_todo include the current state as a JSON content block for exactly this reason, and TodoList.tsx listens for that message and updates its own state from it (see extractTodos / the useEffect in widget/src/TodoList.tsx) - without this, the visible checkbox panel would silently go stale even though the server-side state (and the model's text replies) are correct.

Setup

widget/ is a pnpm workspace member (see pnpm-workspace.yaml), so one install at the root covers both packages:

pnpm install            # installs deps for both root and widget/
pnpm run build:widget    # builds widget/dist/index.html
pnpm run dev             # starts the server over stdio

Re-run pnpm run build:widget whenever you edit widget/src/TodoList.tsx. You can also run pnpm --dir widget run dev to iterate on the widget in a normal browser tab before wiring it back into the MCP server.

This repo uses pnpm (there's a pnpm-lock.yaml) - don't mix in npm install, it'll create a second, conflicting lockfile.

Two ways to run it

The tools themselves (src/server.ts) don't know or care which transport serves them - that's a deliberate teaching point. index.ts and http.ts are both thin wrappers around the same createServer().

stdio (what Claude Desktop / Claude Code expect for local servers):

pnpm run dev

HTTP (exposes POST /mcp, for curl, the MCP Inspector, or any host that takes a server URL instead of a local command):

pnpm run dev:http
# MCP server listening at http://localhost:3000/mcp

It runs in stateless mode (no session IDs to manage) - every request gets its own server+transport pair, but the to-do data still persists between requests because it lives in store.ts at module scope, not on the transport. Point the MCP Inspector at it to poke around without a real host:

npx @modelcontextprotocol/inspector
# then connect to http://localhost:3000/mcp (Streamable HTTP)

Wiring it into Claude Desktop

Add to Claude Desktop's MCP config (Settings -> Developer -> Edit Config), using the absolute path to this folder:

{
  "mcpServers": {
    "todo-demo": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/mcp-todo-demo/src/index.ts"]
    }
  }
}

Restart Claude Desktop, then in a chat: "show my to-do list". Claude calls list_todos, renders the checkbox UI in-chat, and clicking a checkbox round-trips through toggle_todo.

Wiring it into claude.ai (remote connector)

claude.ai's web UI connects to remote MCP servers over HTTP, not stdio, so this needs pnpm run dev:http plus a public URL pointing at your localhost (e.g. a pinggy/ngrok tunnel to http://localhost:3000/mcp). Add it in claude.ai under Settings -> Connectors -> Add custom connector, using the tunnel's /mcp URL.

Why the registerAppTool/registerAppResource rewrite was necessary: the older mcp-ui convention (a tool embedding content: [uiResource] directly in its own result) is not what Claude's MCP Apps support expects - it only renders UI for tools whose _meta.ui.resourceUri points at a separately-registered resource, which is what src/server.ts does now. If you still see text but no checkbox UI after this change, double check pnpm install actually pulled in @modelcontextprotocol/ext-apps and that you restarted the server/tunnel after the change.

Suggested live-coding order

  1. store.ts - just the in-memory array, explain it's a stand-in for a DB.
  2. index.ts skeleton - McpServer + StdioServerTransport, no tools yet.
  3. Add list_todos as a plain server.registerTool returning only a text block - show it works as a normal MCP tool first, no UI involved.
  4. Open widget/, show it's a normal Vite + React app (pnpm --dir widget run dev renders TodoList.tsx in a plain browser tab, no MCP involved yet).
  5. pnpm run build:widget. Introduce registerAppResource (from @modelcontextprotocol/ext-apps/server) to register the built widget/dist/index.html (via src/ui-render.ts + createUIResource) as its own resource at ui://todo-list/view.html - this is the MCP Apps tool/UI split: the UI is a resource, not something a tool returns inline.
  6. Switch list_todos to registerAppTool with _meta: { ui: { resourceUri: 'ui://todo-list/view.html' } } - now the checkbox list renders in chat, but clicking does nothing yet.
  7. Add the postMessage({ type: 'tool', payload: { toolName, params } }) call inside TodoList.tsx's checkbox onChange - this is the "UI -> LLM" half.
  8. Add toggle_todo as a second registerAppTool - this is the "LLM -> UI" half: a normal tool the host calls on your behalf, whose result lands back in the conversation and the linked resource re-renders. Make its return text an encouraging confirmation (encouragement() in src/server.ts) rather than a flat "done" message, since this is what actually shows up as the chat reply.
  9. Point out that the widget panel doesn't refresh on its own after a linked tool call - the host pushes the result in as ui-lifecycle-iframe-render-data instead of remounting the iframe. Add the useEffect/extractTodos listener in TodoList.tsx so the already-open panel picks up the change.
  10. Bonus: src/http.ts - same createServer(), different transport (StreamableHTTPServerTransport behind an Express /mcp route instead of stdio) - shows the tool/UI code is transport-agnostic.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured