Lists
Expose lists to AI agents over MCP with personal API keys
Summary
Planned and shipped MCP access for the Lists app on branch feat/mcp, opened as PR #2 (https://github.com/dkarthick-works/lists/pull/2). Work ran in five phases, phases 1-4 delegated to subagents and verified afterwards.
- API keys (de828f5): migration 007 adds lists.api_keys (hash-only storage, last_used_at, revoked_at); sqlc queries; JWT-protected GET/POST/DELETE /api/keys; auth.KeyMiddleware(lookup) that resolves an lst_ key to its user under the same context key as the JWT middleware.
- Shared logic refactor (72634eb): topLists, searchLists, openList, newList and addEntry extracted from the REST handlers in internal/api/lists.go, with errStatus/errListNotFound carrying status and message. A side-by-side run of old vs new binaries over 21 requests gave identical output; only change is malformed JSON to a missing list now returns 400 instead of 404.
- MCP server (a9c01be): official Go SDK v1.8.0, stateless Streamable HTTP with JSON responses at /mcp behind KeyMiddleware. Tools search_lists, list_lists, get_list, create_list, add_entry call the Phase 2 functions; validation errors surface as isError tool results. Verified with curl JSON-RPC and the MCP Inspector CLI; go mod tidy was needed afterwards.
- Settings UI (0ab2234): ApiKeys.tsx under Profile creates a key shown once with a copyable claude mcp add command, lists keys with last use, and revokes inline. Vite now proxies /mcp in dev. Clicked through in Chrome; phone width not verified.
- README MCP section committed and branch pushed.
Outstanding: local DB still holds subagent test rows for three random user ids plus revoked keys; the cleanup delete was blocked by the auto-mode classifier and was handed to the user as SQL.
Decisions
- Personal API keys (lst_ prefix, pasted as a Bearer header) instead of OAuth 2.1, since target clients are Claude Code/Cursor; OAuth only if claude.ai or ChatGPT connectors are needed
- MCP served from the same Go binary and chi router as the REST API, not a separate service
- Keys accepted only on /mcp, never on /api
- Keys are 32 crypto/rand bytes, base64url, stored as SHA-256 hex: high entropy makes bcrypt unnecessary and a deterministic hash allows indexed lookup
- Revoke sets revoked_at rather than deleting the row
- MCP tools call shared service functions instead of the DB so page splitting, retitling and ancestor touch stay in one place
- v1 tools limited to read and add; delete, reorder and pin left out
- Kept the SDK’s localhost DNS-rebinding protection enabled
Learnings
- go-sdk overwrites Cache-Control on MCP responses, so no-store needs a response-writer wrapper rather than middleware
- go-sdk would infer uuid.UUID as an array of 16 integers in an output schema, so no output schema is declared
- go-sdk rejects requests reaching 127.0.0.1 with a non-localhost Host header; matters only for a same-host proxy forwarding to loopback
- Subagents default to the parent session’s model when neither the call, agent definition nor CLAUDE_CODE_SUBAGENT_MODEL sets one