Serve Fizzy over MCP with fizzy mcp
What
fizzy mcp runs an MCP (Model Context Protocol) server on stdin/stdout, serving Fizzy as tools backed by the signed-in account. Eight domain gateway tools — fizzy_identity, fizzy_boards, fizzy_columns, fizzy_cards, fizzy_comments, fizzy_steps, fizzy_tags, fizzy_users (45 actions) — served through the shared toolkit at github.com/basecamp/mcp, dispatching real API calls through the CLI's authenticated, account-scoped fizzy-sdk client. One install, no separate binary:
claude mcp add fizzy -- fizzy mcp
fizzy mcp --writes # serve write actions too (read-only is the default)
fizzy mcp --domains boards,cards # narrow the served surface (fails closed on unknown keys)
Same pattern as basecamp/hey-cli#357, adapted to Fizzy.
Design
Why not import fizzy-mcp-server? This repo is public; basecamp/fizzy-mcp-server is private, so a module dependency is a broken build for everyone outside the org, and vendoring it wholesale would publish it. Everything the CLI actually needs is public — the basecamp/mcp toolkit (gateway, mcptest) and the fizzy-sdk client already linked in — except the catalog, which is small, hand-written, and deliberately duplicated per the toolkit's two-instance-by-duplication convention: internal/mcpserver/catalog/ carries a verbatim copy of the server's internal/catalog (catalog.go, domains.go, and the rendered-surface snapshot), synced by scripts/sync-mcp-catalog.sh with provenance recorded in PROVENANCE.json. Machinery proven in both instances moves to the toolkit; until then drift shows up as a reviewed diff here, and the snapshot test renders the full served surface so any sync shows its effect.
Dispatch through the SDK, not a second HTTP client. Where fizzy-mcp-server carries its own minimal API client, the CLI already holds an authenticated *fizzy.Client/*fizzy.AccountClient — token resolution, profiles, retry, account scoping. The dispatcher (internal/mcpserver/dispatch.go) maps catalog operations onto the SDK's generic verbs: path params substituted and escaped, query params encoded Rails-style (board_ids[]=a&board_ids[]=b), remaining params gathered into the body and wrapped under the operation's body key, with unknown params rejected in-band naming what the action accepts. Response conventions match the server's: paginated listings surface the Link rel="next" page number as {"data": ..., "next_page": N} to pass back as page; a bodiless 201's Location is followed so create actions answer with the created resource; API errors come back in-band with their HTTP status.
Account scoping diverges deliberately. fizzy-mcp-server discovers the account slug via /my/identity when FIZZY_ACCOUNT is unset; the CLI already has profile/account resolution (fizzy setup, --profile, FIZZY_PROFILE), so fizzy mcp requires a configured account like every other account-scoped command and never invents a second resolution path. get_identity stays unscoped for slug discovery from the tool surface itself.
Read-only by default, matching the server's posture. --writes opts in, paired with a Read+Write access token — the token's permission is the server-side enforcement, this filter is the client-side surface. --domains narrows the served tools and fails closed on unknown keys at startup.
Tests
- Catalog pins (
catalog/catalog_test.go): 8 domains/45 actions, curation validation table, provenance, and the full rendered-surface snapshot (-updateto regenerate). - Request-builder tables (
dispatch_test.go): path/query/body routing, Rails-style encoding, error cases, Link-header parsing. - Wire tests (
server_test.go): a real MCP client over the toolkit's mcptest harness, dispatching through a real fizzy-sdk client onto a fake Fizzy (httptest) that asserts bearer auth, account scoping, query encoding, and body wrapping, and plays back 204s, 201 Locations, and Link pagination. - Command-level end-to-end (
mcp_test.go):fizzy mcpthrough the cobra root with an in-memory transport seam — initialize handshake, tools/list, and a tool call asserting the CLI's own bearer token on the upstream request; read-only default,--writesopt-in,--domainspassthrough failing closed.
make check (fmt, vet, golangci-lint v2.10, tidy, race tests) green; SURFACE.txt regenerated.
Summary by cubic
Adds fizzy mcp to serve Fizzy over the Model Context Protocol on stdio from the existing CLI, no separate binary: 8 domain tools covering 45 actions (boards, cards, comments, steps, tags, users, columns, identity), dispatched through the CLI's signed-in, account-scoped SDK client instead of a second HTTP client. Read-only by default; --writes opts in to write actions and --domains narrows the served tools, failing closed on unknown keys.
Design notes
- Reuses the
basecamp/mcptoolkit (gateway, mcptest); new deps aregithub.com/basecamp/mcpandgithub.com/modelcontextprotocol/go-sdk. - The tool catalog is deliberately duplicated from
fizzy-mcp-serverinstead of imported or vendored: that repo is private, so a module dependency would break every outside build. A sync script copies it verbatim with provenance recorded, and a snapshot test renders the full surface so any drift shows up as a reviewable diff. - Dispatches real API calls through
fizzy.Client/fizzy.AccountClient, so auth, retry, profile, and account scoping already work; account resolution follows the CLI's existing path (configured account,get_identityunscoped for slug discovery) rather than the server's/my/identityfallback. The identity tool's describe doc stays neutral about resolution, and the README and--helpenumerate all eight domains. - Response conventions match
fizzy-mcp-server: boards, tags, and users listings paginate via the Linkrel="next"header and accept thepageparam (columns and steps render full collections and stay unpaginated), a bodiless 201Locationis followed so create actions answer with the created resource, and API errors come back in-band with their HTTP status.