37signals MCP toolkit: the elements our MCP servers share, extracted from the
product instances that proved them. Sibling of the sdk and cli meta-repos.
basecamp-mcp-server and
hey-mcp-server are thin dispatch
layers over their product SDKs, exposing domain gateway tools — one MCP
tool per domain, {"action", "params"} dispatch, an in-band describe action,
read-only filtering, fail-closed domain narrowing. Building the second instance
duplicated the machinery under that surface. This repo is where the duplicated
parts live once, so the third instance (Fizzy) starts from a seed instead of a
copy.
Code moves here only after two product instances have proven it by duplication — the third validates it. Nothing lands speculatively: if only one server runs it (today: hosted HTTP transport and the OAuth resource-server plumbing, which exist only in basecamp-mcp-server), it stays in that server until a second instance forces it to converge or diverge. Extraction is a move of reviewed code, not a rewrite.
Products depend on github.com/basecamp/mcp. This module never imports a
product SDK or server — no basecamp-sdk, no hey-sdk. Anything that needs a
product client belongs in the product server, behind an interface defined here.
Arriving as the extraction sequence lands (see the program board):
| Package | Extracted from | Contents |
|---|---|---|
mcptest |
both servers' test suites | in-memory MCP client/server wire-test harness, snapshot testing with -update |
gateway |
hey-mcp-server internal/server, basecamp-mcp-server internal/tools registry |
gateway dispatch conventions, read-only and domain filtering over a small catalog interface |
catalog |
hey-mcp-server internal/catalog |
Domain/Operation types and the behavior-model + OpenAPI join, product-parameterized (tool prefix, domain specs, embedded model snapshot) |
result |
basecamp-mcp-server internal/tools (result_shape.go, result_cap.go), hey-mcp-server internal/server/wire_shape.go; first consumed by hey-mcp-server and fizzy-mcp-server |
structuredContent always a JSON object, the serialized-size result cap with page/offset continuations, title mirroring; one receiving middleware applies all three |
auth/exchange |
basecamp-mcp-server internal/auth/exchange |
RFC 8693 delegation-exchange client for a hosted server: client_secret_post and private_key_jwt client auth, the terminal (re-authorize) vs infra (retry, never re-consent) error fork, allowlisted OAuth error codes with credential-redacted descriptions, optional DPoP proofs and the nonce retry |
auth/upstream |
basecamp-mcp-server internal/upstream |
the error type for credentialed endpoints whose text never renders its cause (fixed reason, status, allowlisted code, scrubbed description), bounded Retry-After and lifetimes, and upstreamtest's hostile-endpoint catalog |
Per-product forever: domain curation and blurbs, vendored SDK model snapshots, SDK clients and adapters, env var surfaces, product-specific tools and resources.
The Go toolchain is pinned in .mise.toml and go.mod. Activate mise before
building:
eval "$(mise hook-env)" && make readymake ready runs vet, the race-enabled tests, and the build — run it before
committing.