Serve Basecamp over MCP with basecamp mcp
What
basecamp mcp runs an MCP (Model Context Protocol) server on stdin/stdout, serving Basecamp as tools backed by the signed-in account. Fifteen domain gateway tools — basecamp_projects, basecamp_todos, basecamp_cards, basecamp_messages, basecamp_campfires, basecamp_boosts, basecamp_schedules, basecamp_files, basecamp_people, basecamp_automation, basecamp_reports, basecamp_everything, basecamp_clientside, basecamp_forwards, basecamp_account (247 actions) — derived from basecamp-sdk's model exports via the shared toolkit at github.com/basecamp/mcp, dispatching real API calls through the CLI's authenticated, account-scoped SDK client. One install, no separate binary:
claude mcp add basecamp -- basecamp mcp
basecamp mcp --read-only # serve only read-only actions
basecamp mcp --domains projects,todos # narrow the served surface (fails closed on unknown keys)
Completes the CLI-subcommand trio with basecamp/hey-cli#357 and basecamp/fizzy-cli#208, same uniform shape.
Design
Why not import basecamp-mcp-server? This repo is public; basecamp/basecamp-mcp-server is private, so a module dependency is a broken build for everyone outside the org, and vendoring its ~21k-line hand-written tool package would publish it wholesale. Everything the CLI actually needs is public — the basecamp/mcp toolkit (catalog, gateway, mcptest) and basecamp-sdk's model exports (behavior-model.json + openapi.json, build products of its Smithy model) — so the CLI derives the catalog from those, the way hey-cli does. The domain curation carries over basecamp-mcp-server's grouping (projects, todos, cards, messages, campfires, schedules, files, people, account) where the SDK's tags allow it.
Born on the toolkit convention. basecamp-mcp-server's production wire still speaks the ancestor dialect ({resource, action} calls, centralized describe); converging it is a wire change under decision on the MCP program board ("Basecamp MCP: converge onto toolkit"). This subcommand is new surface with zero existing consumers, so it speaks the uniform toolkit convention ({"action": ..., "params": ...} + in-band describe) from birth, matching hey mcp and fizzy mcp regardless of how the hosted decision lands.
Curation follows the tags where they diverge from the server. The toolkit catalog claims whole tags, so the server's checkins domain — whose questionnaire operations live inside the SDK's grab-bag Automation tag alongside templates, webhooks, lineup, dock tools, and search — is served as basecamp_automation, and the server's admin grab-bag lands across basecamp_reports, basecamp_everything, and basecamp_automation. Every tag is claimed: Catalog.Unmapped is pinned empty, so an SDK tag nobody has decided about fails the build. The server's digests domain (MCP App previews) has no SDK surface and is deliberately absent.
Model snapshot with a bounded patch. internal/mcpserver/model/ vendors basecamp-sdk's exports at go/v0.14.0 — the version go.mod pins, enforced by test — synced by scripts/sync-mcp-model.sh with provenance recorded. The sync applies two patch tables, both pinned by tests and refused by the script once upstream catches up: tags assigned to the 16 operations the export leaves untagged (questionnaire ops → Automation, report ops → a Reports tag, RepositionTodo → Todos, card-column subscriptions → Card Tables), and 3 raw-binary upload operations dropped (CreateAttachment, CreateCampfireUpload, UpdateAccountLogo — multipart/octet-stream can't ride the JSON tool-call convention; uploads stay a CLI affair via basecamp attach/upload).
Account scoping through the CLI's own plumbing. Every SDK operation is account-scoped (/{accountId}/...); after load, the catalog is rescoped — the accountId parameter stripped from paths and schemas, pinned by test — and dispatch goes through app.Account() (*basecamp.AccountClient), which supplies the account the way every other command does. Auth, token refresh, retry, resilience gating, and observability hooks all ride along. basecamp mcp requires a configured account up front (stdio belongs to the MCP wire, so no interactive resolution) and never touches the login flow.
Full surface by default, matching the server's posture. basecamp-mcp-server serves read-write by default with BASECAMP_MCP_READ_ONLY opt-in, so the subcommand mirrors it: --read-only narrows to read-only actions (derived from the behavior model's per-operation readonly trait, not name heuristics). --domains narrows the served tools and fails closed on unknown keys at startup. (fizzy-cli defaulted the other way — also mirroring its server.)
Dispatch conventions. Path params substituted and escaped, query params encoded (Rails-style assignee_ids[]=1&assignee_ids[]=2 arrays supported), remaining params gathered into the body with property names checked against the body schema (a typo guard — types and required properties stay the API's to enforce, with its errors surfaced in-band). Paginated listings surface the Link rel="next" page as {"next_page": N, "results": ...} to pass back as page; bodiless 204s answer {"status": 204} (+location when present); API errors come back in-band with the SDK's rendering.
Tests
- Catalog pins (
internal/mcpserver/catalog_test.go): 15 domains in order, every tag claimed (Unmapped pinned empty), 247 operations, binary-upload exclusions, account rescope (noaccountIdanywhere), provenance ref == go.mod's basecamp-sdk version, and a full rendered-surface snapshot (testdata/catalog_snapshot.txt,-updateto regenerate). - Request-builder tables (
dispatch_test.go): path substitution/escaping, query encoding incl. Rails-style arrays, body gathering and schema-checked strays, missing/malformed params, Link-headernext_pageparsing. - Wire tests (
server_test.go): a real MCP client over the toolkit'smcptestharness, dispatching through a real basecamp-sdkAccountClientonto a fake Basecamp (httptest) that asserts bearer auth, account prefix, query encoding, and body shape, and plays back 204s, 201 creates, Link pagination, and API errors; read-only filtering (tools/list and dispatch refusal), domain narrowing, unknown-domain fail-closed. - Command-level end-to-end (
internal/commands/mcp_test.go):basecamp mcpthrough the cobra command with an in-memory transport seam — MCP initialize + tools/list + a tool call asserting the CLI's own bearer token on the upstream request; auth required, account required,--read-only/--domainspassthrough proven over the wire, unknown domains failing closed at startup. bin/ci(make check): fmt, vet, golangci-lint v2.11.1, unit tests, BATS e2e, naming, surface snapshot, skill drift, bare groups, lint lockstep, smoke coverage (mcp marked OOS — long-running stdio server, covered by the Go wire tests), SDK provenance, tidy — green, except nine pre-existing TTY-environment test failures (TestIsInteractive*, wizard tests) that fail identically on origin/main in this sandbox and pass in CI.go test -raceclean on the new packages.
The nix vendorHash may need the CI round-trip (make update-nix-hash value from the nix-build job) since go.mod grew github.com/basecamp/mcp and github.com/modelcontextprotocol/go-sdk.
Summary by cubic
Adds basecamp mcp, a new subcommand that runs an MCP server on stdio serving Basecamp as 15 domain gateway tools (247 operations) backed by the CLI's signed-in account. The catalog derives from basecamp-sdk's model exports via the github.com/basecamp/mcp toolkit, and dispatch goes through the CLI's account-scoped SDK client so existing auth, retry, and observability plumbing ride along.
New Features
- Serves read-write by default;
--read-onlyand--domainsnarrow the surface, failing closed on unknown domains. - Requires sign-in and a configured account up front because stdio belongs to the MCP wire and can't resolve an account interactively.
- Vendors the SDK model snapshot in
internal/mcpserver/model/, synced byscripts/sync-mcp-model.shand pinned to the go.mod basecamp-sdk version; the sync patches tags on 16 untagged operations and drops three raw-binary uploads that can't ride the JSON tool-call convention. - Paginated listings wrap the next page as
{"next_page": N, "results": ...}; bodiless 204 responses answer{"status": 204}.
Migration
- nix
vendorHashis refreshed for the newgithub.com/basecamp/mcpandgithub.com/modelcontextprotocol/go-sdkdependencies.