Serve Fizzy over MCP with fizzy mcp - #208
Conversation
fizzy mcp runs an MCP server on stdin/stdout, serving Fizzy boards, cards, comments, steps, tags, and users as domain gateway tools backed by the signed-in account. The CLI assembles the same shape as fizzy-mcp-server from the public basecamp/mcp toolkit (gateway, mcptest) plus a duplicated copy of the server's hand-written catalog — synced by scripts/sync-mcp-catalog.sh with provenance recorded — and dispatches through the CLI's authenticated, account-scoped fizzy-sdk client. Read-only by default, matching the server's posture; --writes opts in (pair with a Read+Write token) and --domains narrows the surface, failing closed on unknown keys. Paginated listings surface the Link rel=next page number as next_page; 201 Locations are followed so create actions answer with the created resource.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 57b755f2c9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Action: "list_boards", Method: "GET", Path: "/boards", ReadOnly: true, | ||
| Summary: "List the boards you have access to", |
There was a problem hiding this comment.
Add page parameters to paginated collection actions
When an account has more than one page of boards, render returns a next_page, but this catalog entry does not accept page, so passing that value back is rejected as an unknown parameter and the remaining boards are inaccessible through MCP. The existing CLI confirms this endpoint is paginated in internal/commands/board.go:58-75; the same omission affects list_tags and list_users at lines 380 and 396, whose CLI implementations also support pages. Mark these operations paginated and include pageParam().
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Right on all three — verified against the fizzy app's controllers: boards#index, tags#index, and users#index all use geared pagination (set_page_and_extract_portion_from), while list_columns and list_steps render their full collections and stay unpaginated. Since the catalog is a synced duplicate, fixed in its home repo first (fizzy-mcp-server@df27419) and synced here in c047092 via scripts/sync-mcp-catalog.sh — which also gave the sync path its first real exercise.
There was a problem hiding this comment.
Pull request overview
Adds a stdio MCP server backed by the CLI’s authenticated Fizzy SDK client.
Changes:
- Adds eight MCP domain tools with read-only defaults and filtering.
- Implements catalog-driven API dispatch, pagination, and create-response handling.
- Adds documentation, dependency updates, snapshots, and comprehensive tests.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
AGENTS.md |
Documents the MCP package. |
README.md |
Documents MCP setup and usage. |
SURFACE.txt |
Records the new command and flags. |
go.mod |
Adds MCP dependencies. |
go.sum |
Updates dependency checksums. |
internal/commands/help.go |
Groups the MCP command in help. |
internal/commands/mcp.go |
Implements the MCP command. |
internal/commands/mcp_test.go |
Tests command behavior end-to-end. |
internal/mcpserver/server.go |
Assembles the MCP gateway server. |
internal/mcpserver/server_test.go |
Tests the MCP wire behavior. |
internal/mcpserver/dispatch.go |
Dispatches tools through the SDK. |
internal/mcpserver/dispatch_test.go |
Tests request construction and pagination. |
internal/mcpserver/catalog/catalog.go |
Defines catalog and validation behavior. |
internal/mcpserver/catalog/domains.go |
Defines domain tools and actions. |
internal/mcpserver/catalog/catalog_test.go |
Tests catalog invariants and snapshots. |
internal/mcpserver/catalog/PROVENANCE.json |
Records catalog provenance. |
internal/mcpserver/catalog/testdata/catalog_snapshot.txt |
Pins the rendered MCP surface. |
scripts/sync-mcp-catalog.sh |
Synchronizes the duplicated catalog. |
Suppressed comments (2)
internal/commands/mcp.go:43
- Startup failures returned here go through the root
Executeerror renderer. Because an MCP launch has piped stdout, that renderer emits a normal CLI JSON envelope to stdout, which is not a JSON-RPC message and masks the useful authentication/configuration error from the MCP client. MCP command errors need to be routed to stderr (or otherwise excluded from the root stdout renderer) so stdout remains protocol-only.
func runMCP(cmd *cobra.Command, args []string) error {
if err := requireAuthAndAccount(); err != nil {
return err
internal/commands/mcp.go:66
- A SIGINT/SIGTERM cancels
ctx, and the MCP SDK'sWaitreports that cancellation as an error. Propagating it makes a normal signal-driven shutdown exit as a failure (and currently invokes the CLI error renderer); treat cancellation of this command context as a clean shutdown while preserving unrelated transport errors.
return session.Wait()
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| { | ||
| Action: "get_identity", Method: "GET", Path: "/my/identity", ReadOnly: true, Unscoped: true, | ||
| Summary: "List the accounts the token can access, with your user record in each", | ||
| Doc: "Each account carries a slug; account-scoped actions use it automatically when FIZZY_ACCOUNT is set, or discover it here when exactly one account exists.", |
There was a problem hiding this comment.
True — that wording described fizzy-mcp-server's FIZZY_ACCOUNT-or-discovery startup, not the wire surface. Since the catalog is shared by duplication, reworded it consumer-neutrally in its home repo (fizzy-mcp-server@4bef952) — scoped actions run against the configured account, and get_identity lists what the token reaches — and synced here in 9d4c889.
| Long: "Run an MCP (Model Context Protocol) server on stdin/stdout, serving Fizzy\n" + | ||
| "boards, cards, comments, steps, tags, and users as tools backed by your\n" + | ||
| "signed-in account.\n\n" + |
There was a problem hiding this comment.
Fixed in 9d4c889 — the help text and README now name all eight served domains.
Codex review caught that list_boards, list_tags, and list_users answered next_page without accepting a page param, stranding everything past page one. Verified against the fizzy app's controllers (geared pagination on boards#index, tags#index, users#index; list_columns and list_steps render their full collections and stay unpaginated), fixed in the catalog's home repo (fizzy-mcp-server@df27419), and synced here by scripts/sync-mcp-catalog.sh.
- get_identity's describe payload promised FIZZY_ACCOUNT-or-discovery, fizzy-mcp-server's startup behavior — here the account comes from CLI profile config, so agents were told a fallback exists that this server does not have. Reworded consumer-neutrally in the catalog's home repo (fizzy-mcp-server@4bef952) and synced. - fizzy mcp --help and the README named six of the eight served domains; now all eight.
What
fizzy mcpruns 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:Same pattern as basecamp/hey-cli#357, adapted to Fizzy.
Design
Why not import fizzy-mcp-server? This repo is public;
basecamp/fizzy-mcp-serveris 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 — thebasecamp/mcptoolkit (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'sinternal/catalog(catalog.go,domains.go, and the rendered-surface snapshot), synced byscripts/sync-mcp-catalog.shwith provenance recorded inPROVENANCE.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 Linkrel="next"page number as{"data": ..., "next_page": N}to pass back aspage; a bodiless 201'sLocationis 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/identitywhenFIZZY_ACCOUNTis unset; the CLI already has profile/account resolution (fizzy setup,--profile,FIZZY_PROFILE), sofizzy mcprequires a configured account like every other account-scoped command and never invents a second resolution path.get_identitystays unscoped for slug discovery from the tool surface itself.Read-only by default, matching the server's posture.
--writesopts in, paired with a Read+Write access token — the token's permission is the server-side enforcement, this filter is the client-side surface.--domainsnarrows the served tools and fails closed on unknown keys at startup.Tests
catalog/catalog_test.go): 8 domains/45 actions, curation validation table, provenance, and the full rendered-surface snapshot (-updateto regenerate).dispatch_test.go): path/query/body routing, Rails-style encoding, error cases, Link-header parsing.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.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 mcpto 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;--writesopts in to write actions and--domainsnarrows the served tools, failing closed on unknown keys.Design notes
basecamp/mcptoolkit (gateway, mcptest); new deps aregithub.com/basecamp/mcpandgithub.com/modelcontextprotocol/go-sdk.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.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.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.Written for commit 9d4c889. Summary will update on new commits.