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 update
Описание
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
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.
Установка Todo Demo
У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.
▸ github.com/vishaldodiya/mcp-uiFAQ
Todo Demo MCP бесплатный?
Да, Todo Demo MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Todo Demo?
Нет, Todo Demo работает без API-ключей и переменных окружения.
Todo Demo — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить Todo Demo в Claude Desktop, Claude Code или Cursor?
Открой Todo Demo на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Похожие MCP
Gmail
Read, send and search emails from Claude
автор: GoogleSlack
Send, search and summarize Slack messages
автор: SlackRunbear
No-code MCP client for team chat platforms, such as Slack, Microsoft Teams, and Discord.
Discord Server
A community discord server dedicated to MCP by [Frank Fiegel](https://github.com/punkpeye)
Compare Todo Demo with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории communication
