Skip to content

Serve Fizzy over MCP with fizzy mcp - #208

Merged
jeremy merged 3 commits into
masterfrom
mcp-subcommand
Aug 28, 2026
Merged

Serve Fizzy over MCP with fizzy mcp#208
jeremy merged 3 commits into
masterfrom
mcp-subcommand

Conversation

@jeremy

@jeremy jeremy commented Aug 28, 2026

Copy link
Copy Markdown
Member

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 (-update to 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 mcp through 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, --writes opt-in, --domains passthrough 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/mcp toolkit (gateway, mcptest); new deps are github.com/basecamp/mcp and github.com/modelcontextprotocol/go-sdk.
  • The tool catalog is deliberately duplicated from fizzy-mcp-server instead 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_identity unscoped for slug discovery) rather than the server's /my/identity fallback. The identity tool's describe doc stays neutral about resolution, and the README and --help enumerate all eight domains.
  • Response conventions match fizzy-mcp-server: boards, tags, and users listings paginate via the Link rel="next" header and accept the page param (columns and steps render full collections and stay unpaginated), a bodiless 201 Location is 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.

Review in cubic

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.
Copilot AI balanced review requested due to automatic review settings August 28, 2026 11:35

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread internal/mcpserver/catalog/domains.go Outdated
Comment on lines +93 to +94
Action: "list_boards", Method: "GET", Path: "/boards", ReadOnly: true,
Summary: "List the boards you have access to",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Execute error 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's Wait reports 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.

Comment thread internal/mcpserver/catalog/domains.go Outdated
{
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.",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread internal/commands/mcp.go Outdated
Comment on lines +27 to +29
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" +

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9d4c889 — the help text and README now name all eight served domains.

jeremy added 2 commits August 28, 2026 04:43
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.
@jeremy
jeremy merged commit ceaa4b5 into master Aug 28, 2026
16 checks passed
@jeremy
jeremy deleted the mcp-subcommand branch August 28, 2026 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants