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.
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-containedwidget/dist/index.html.- The widget is registered as its own MCP resource (
registerAppResource, URIui://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.tsusescreateUIResource'sadapters.mcpAppsoption to inject a small runtime script translating the widget's plainpostMessagecalls 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'sonChangedoeswindow.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 fakerole: 'user'message in the host - removed oncetoggle_todoitself could produce a visible reply.)
- can surface as a real chat reply on its own. (An earlier version of this
demo also sent a
- 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 aui-lifecycle-iframe-render-datamessage (payload.renderData.toolOutput). Bothlist_todosandtoggle_todoinclude the current state as a JSON content block for exactly this reason, andTodoList.tsxlistens for that message and updates its own state from it (seeextractTodos/ theuseEffectinwidget/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/registerAppResourcerewrite was necessary: the older mcp-ui convention (a tool embeddingcontent: [uiResource]directly in its own result) is not what Claude's MCP Apps support expects - it only renders UI for tools whose_meta.ui.resourceUripoints at a separately-registered resource, which is whatsrc/server.tsdoes now. If you still see text but no checkbox UI after this change, double checkpnpm installactually pulled in@modelcontextprotocol/ext-appsand that you restarted the server/tunnel after the change.
Suggested live-coding order
store.ts- just the in-memory array, explain it's a stand-in for a DB.index.tsskeleton -McpServer+StdioServerTransport, no tools yet.- Add
list_todosas a plainserver.registerToolreturning only a text block - show it works as a normal MCP tool first, no UI involved. - Open
widget/, show it's a normal Vite + React app (pnpm --dir widget run devrendersTodoList.tsxin a plain browser tab, no MCP involved yet). pnpm run build:widget. IntroduceregisterAppResource(from@modelcontextprotocol/ext-apps/server) to register the builtwidget/dist/index.html(viasrc/ui-render.ts+createUIResource) as its own resource atui://todo-list/view.html- this is the MCP Apps tool/UI split: the UI is a resource, not something a tool returns inline.- Switch
list_todostoregisterAppToolwith_meta: { ui: { resourceUri: 'ui://todo-list/view.html' } }- now the checkbox list renders in chat, but clicking does nothing yet. - Add the
postMessage({ type: 'tool', payload: { toolName, params } })call insideTodoList.tsx's checkboxonChange- this is the "UI -> LLM" half. - Add
toggle_todoas a secondregisterAppTool- 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()insrc/server.ts) rather than a flat "done" message, since this is what actually shows up as the chat reply. - 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-datainstead of remounting the iframe. Add theuseEffect/extractTodoslistener inTodoList.tsxso the already-open panel picks up the change. - Bonus:
src/http.ts- samecreateServer(), different transport (StreamableHTTPServerTransportbehind an Express/mcproute instead of stdio) - shows the tool/UI code is transport-agnostic.
Recommended Servers
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.
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.
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.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
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.
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.
Neon Database
MCP server for interacting with Neon Management API and databases
E2B
Using MCP to run code via e2b.
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.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.