mcp-todo-demo
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-todo-demoshow my to-do list"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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.)
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 stdioRe-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.
Related MCP server: Basic MCP Server
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 devHTTP (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/mcpIt 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.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/vishaldodiya/mcp-ui'
If you have feedback or need assistance with the MCP directory API, please join our Discord server