Todo MCP Server
A minimal todo app MCP server for ChatGPT that exposes tools for managing tasks and provides an interactive HTML widget interface. It demonstrates how to build MCP servers with React-based UIs and includes development OAuth for ChatGPT connector setup.
README
ChatGPT Todo MCP demo (Apps SDK + React)
A minimal todo app for ChatGPT: an MCP server exposes tools and an interactive HTML UI, built with React + Vite and embedded as a single-file bundle. Includes a small dev OAuth layer so ChatGPT’s connector wizard can complete discovery.
Official reference: Apps SDK Quickstart.
Quick start
npm install
npm start # builds widget (prestart) then runs server on port 8787 by default
-
MCP endpoint:
http://localhost:8787/mcp -
For ChatGPT: expose with HTTPS (e.g. ngrok) and create a connector pointing at
https://<your-host>/mcp. -
If discovery URLs show the wrong scheme/host behind a tunnel, set:
export PUBLIC_BASE_URL=https://your-ngrok-host.example
Project layout
| Path | Role |
|---|---|
server.js |
HTTP router: OAuth discovery + CORS + MCP StreamableHTTPServerTransport on /mcp |
oauth-dev.js |
Dev-only OAuth 2.1 discovery + DCR/PKCE (replace with a real IdP for production) |
widget/ |
Vite + React source for the in-chat UI |
dist/todo-widget.html |
Built single-file HTML (gitignored); loaded by server.js at startup |
Architecture and concepts
One-sentence model
ChatGPT acts as the MCP client. It speaks MCP over HTTPS to your Node server at /mcp. The server registers tools (what the model can call) and a resource (HTML for the widget). The widget runs in an iframe and talks to ChatGPT through a JSON-RPC bridge over postMessage. OAuth metadata on the same origin lets ChatGPT attach the connector; it is separate from MCP tool execution but required for onboarding.
Model Context Protocol (MCP)
MCP is a standard way for a host (ChatGPT) to discover and invoke tools and read resources on a server. This repo uses @modelcontextprotocol/sdk: an McpServer instance registers capabilities and is connected to a transport that maps MCP messages to HTTP (StreamableHTTPServerTransport).
Base MCP vs Apps SDK helpers
@modelcontextprotocol/sdk: coreMcpServer, schemas, transport.@modelcontextprotocol/ext-apps:registerAppToolandregisterAppResourcenormalize UI metadata (which HTML resource to show for a tool) and set the Apps HTML MIME type (RESOURCE_MIME_TYPE).
The widget is registered as a resource at a logical URI (e.g. ui://widget/todo.html). That URI does not need to be a public web URL; the host resolves it via MCP resources/read. Each tool’s _meta.ui.resourceUri points at the same URI so ChatGPT knows which UI surface belongs to which tool.
HTTP front door (server.js)
One Node http.Server handles several surfaces:
- OAuth / discovery (
oauth-dev.js) — well-known URLs and token endpoints ChatGPT expects. - CORS
OPTIONSfor/mcp. - Health
GET /. - MCP
POST/GET/DELETEon/mcpvia the streamable HTTP transport. - 404 for unknown paths.
So you have one process, multiple logical HTTP APIs (OAuth HTTP + MCP HTTP).
Streamable HTTP and server lifetime
The transport is created per incoming MCP request, with sessionIdGenerator: undefined (stateless mode for this demo). A new McpServer is constructed per request and torn down when the response closes.
Important: in-memory todo state (todos in server.js) lives at module scope, not inside the McpServer instance. So state persists for the lifetime of the Node process even though each request gets a new MCP server object.
Tools and the UI contract
Tools (add_todo, complete_todo) declare input schemas (Zod) so the host validates arguments.
Tool results include:
content: usual MCP content (e.g. text) for the model/conversation.structuredContent: JSON consumed by the widget—here{ tasks: [...] }.
Using the same structuredContent shape for every mutation keeps the React UI in sync whether the call was triggered by the user in the widget or by the model in chat.
OAuth (oauth-dev.js)
ChatGPT’s connector flow fetches OAuth protected resource metadata and authorization server metadata (see Apps SDK auth). Without those routes, setup can fail with “Error fetching OAuth configuration.”
This repo ships a development-only authorization server (discovery, dynamic client registration, authorize redirect, PKCE token exchange) scoped to ChatGPT redirect URLs. Do not use it as-is for production—swap in Auth0, Stytch, Cognito, or similar, and verify tokens on MCP requests.
PUBLIC_BASE_URL forces the public https:// origin in metadata when proxies/ngrok do not set Host / X-Forwarded-Proto the way you need.
Widget bridge (widget/src/bridge.ts)
The built HTML runs inside ChatGPT’s iframe. It does not call your /mcp URL like a normal SPA; it uses the MCP Apps UI bridge:
ui/initializethenui/notifications/initialized— handshake with the host.tools/call— ask the host to run a named MCP tool with arguments (same tools the model uses).ui/notifications/tool-result— when the model runs a tool, the host can push the result so the UI updates without a direct return path fromtools/call.
So there are two update paths: RPC responses for UI-initiated calls, and notifications for model-initiated calls.
Why single-file HTML (Vite + vite-plugin-singlefile)
ChatGPT receives the widget as embedded HTML from the MCP resource read, not as “your site + separate JS chunks.” Relative chunk URLs would break in that embedding model. The build produces one dist/todo-widget.html with inlined JS/CSS; server.js reads it at startup into todoHtml.
React is a developer ergonomics layer; the deployable artifact is static HTML.
End-to-end flows
User in ChatGPT: message → model selects a tool → ChatGPT POSTs to your /mcp → tool runs → returns structuredContent.tasks → host shows/updates the widget.
User in the widget: React → tools/call via postMessage → host forwards to MCP → same handlers → RPC result updates state.
Connector setup: ChatGPT hits /.well-known/... on your origin → OAuth linking if required → subsequent MCP calls to /mcp may include Authorization: Bearer ... (enforcing that on every tool is a production step).
Natural next steps
| Area | Direction |
|---|---|
| State | Persist todos in a database; scope by authenticated user id from the access token. |
| Auth | Replace oauth-dev.js with a real IdP; validate issuer, audience, scopes on each MCP request. |
| MCP session | Stateful sessions if you need different streaming or lifecycle semantics. |
| Tools | Richer descriptions/schemas, optional outputSchema, clearer names for model routing. |
| Widget | Same bridge; improve UX, errors, and loading states. |
Scripts
| Script | Description |
|---|---|
npm run build |
Build dist/todo-widget.html from widget/ |
npm start |
npm run build then node server.js |
npm run build:widget |
Vite build only |
Default port: 8787 (PORT env overrides).
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。