Todos playground
read as.md mcp.ggui.ai/playground/todos is a first-party hosted MCP service that exposes a per-user persistent todo list. Sign in to console.ggui.ai, point your agent at the URL, and watch the agent call a tool while ggui renders the list inline as generative UI. State survives refresh, re-login, and host swaps — todos are pinned to your Cognito identity, not the session.
This is the smallest “real” MCP-driven surface we ship. No code to write, no schema to learn — the four tools are described by the server’s tools/list response, and the host (Claude Desktop, Goose, your own agent) drives them.
What it demonstrates
Section titled “What it demonstrates”- Agent → tool → generative UI: the agent calls a tool, the runtime feeds the result into a blueprint, the host renders the resulting list inline.
- Per-user persistence: log out, come back tomorrow from a different host — same todos.
- Cognito identity at the MCP edge: tool handlers read
ctx.userIddirectly. No second auth layer; theAuthorization: Bearer ggui_user_*header that authenticated the MCP session also scopes the data.
If you want the deep “why” of the service model that makes this work, see MCP services — playground-todos is one of three reference services in cloud/mcp-services/.
The four tools
Section titled “The four tools”All four are scoped to the caller’s ctx.userId. Anonymous calls (no bearer token) return an explicit authentication required error.
| Tool | Input | Output | Notes |
|---|---|---|---|
todos_list | {} | { todos: Todo[] } | Oldest-first by createdAt. Empty array when the user has none — never null. |
todos_add | { text } | { todo } | text trimmed; empty or 500+ char bodies rejected at the schema boundary. |
todos_toggle | { id } | { found: boolean, todo: Todo|null } | Flips done. Missing id and cross-user id collapse to one shape (see Auth model). |
todos_delete | { id } | { ok: boolean } | ok: true only when the caller owned a todo with that id. |
A Todo row is { id, text, done, createdAt }. createdAt is an ISO-8601 string; id is opaque (treat it as a black-box handle returned by todos_add or todos_list).
Auth model
Section titled “Auth model”Cognito-gated. Every handler resolves ctx.userId from the bearer key minted by console.ggui.ai and rejects calls without one. The console is the only place those keys come from — either through an OAuth ceremony from an MCP-Apps-aware host or by minting one manually at /keys/connector.
Cross-user probing is structurally prevented. todos_toggle and todos_delete collapse two failure modes into one wire shape:
- The id doesn’t exist.
- The id exists but belongs to a different user.
Both return the same negative response (found: false / ok: false). A caller can’t enumerate other users’ id space.
Try it from Claude Desktop
Section titled “Try it from Claude Desktop”If you’ve already connected mcp.ggui.ai from the Claude Desktop walkthrough, Claude Desktop will pick up playground-todos once you add the second connector. In Settings → Connectors → Add custom connector, paste:
https://mcp.ggui.ai/playground/todosThe OAuth ceremony runs again against console.ggui.ai; approve it the same way you did for the main mcp.ggui.ai connector. A new ggui_user_* row appears in /keys/connector labelled with the connector name.
Once connected, try prompts like:
What’s on my todo list?
Add “ship the launch post” to my todos, then show me everything pending.
Mark the first todo as done.
Claude calls todos_list / todos_add / todos_toggle, and the table updates in the chat. Refresh Claude Desktop, ask again — same list comes back.
Try it from your own agent
Section titled “Try it from your own agent”The Todos playground is a vanilla remote MCP server. Anything that speaks streamable-http MCP + bearer auth works. For Claude Agent SDK:
import { query } from "@anthropic-ai/claude-agent-sdk";
const apiKey = process.env.GGUI_USER_KEY!; // ggui_user_*
const mcpServers = { todos: { type: "http" as const, url: "https://mcp.ggui.ai/playground/todos", headers: { Authorization: `Bearer ${apiKey}` }, },};
for await (const msg of query({ prompt: "Add 'try the playground' to my todos, then list everything.", options: { model: "claude-sonnet-4-6", mcpServers, allowedTools: [ "mcp__todos__todos_list", "mcp__todos__todos_add", "mcp__todos__todos_toggle", "mcp__todos__todos_delete", ], },})) { // consume the SDK message stream}The Claude Agent SDK namespaces every MCP tool as mcp__<serverName>__<toolName> — with serverName: "todos" here, the tool ids land as mcp__todos__todos_list, mcp__todos__todos_add, and so on. The pattern is the same for any other host that takes an MCP-server URL.
For the full agent-side wiring (system prompt, error handling, streaming), see the Claude Agent example. The same pattern applies — swap mcp.ggui.ai for mcp.ggui.ai/playground/todos, narrow the tool whitelist to the four todos tools.
How it’s built
Section titled “How it’s built”The service is closed-source (@ggui-private/mcp-playground-todos — not published or mirrored), but its shape is simple enough to describe:
- a package entry exporting a
createPlaygroundTodosHandlers({ store })convenience factory, - one file per tool (list / add / toggle / delete), each ~50 lines,
- a
TodoStoreinterface with an in-memory implementation for tests; production swaps in a DynamoDB-backed store against the same interface.
To build your own service in this shape, use the OSS McpService mount primitive in @ggui-ai/mcp-server — the same seam this service mounts through. The MCP services architecture page covers mount points, the AuthAdapter contract, and how ctx.userId is resolved end-to-end.