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
访问服务器

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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选