Serve HEY over MCP with hey mcp

Merged
JE
2mo ago
+17248-4

What

hey mcp runs an MCP (Model Context Protocol) server on stdin/stdout, serving HEY as tools backed by the signed-in account. Seven domain gateway tools — hey_boxes, hey_search, hey_threads, hey_contacts, hey_todos, hey_calendar, hey_identity — derived from hey-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 hey -- hey mcp
hey mcp --read-only                # serve only read-only actions
hey mcp --domains boxes,search     # narrow the served surface (fails closed on unknown keys)

Design

Why not import hey-mcp-server? This repo is public; basecamp/hey-mcp-server is private, so a module dependency is a broken build for everyone outside the org, and vendoring it would publish it wholesale. Everything the server actually needs is already public — the basecamp/mcp toolkit (catalog, gateway, mcptest) and hey-sdk's model exports — so the CLI assembles the same shape from those parts, per the toolkit's own two-instance-by-duplication convention. hey-mcp-server remains the home of the hosted/HTTP story; when its catalog curation finds a shared public home (hey-sdk being the natural candidate), both consumers converge on it.

Real dispatch, not the stub. hey-mcp-server's handler is still the spike boundary (dispatch names the operation it would call). Here the CLI's *hey.Client — auth, token refresh, retry, account scoping, body limits — is already in hand, so the dispatcher maps catalog operations onto the SDK's generic verbs: path params substituted and escaped, query params encoded, remaining params gathered into the request body with property names checked against the body schema (a typo guard — types and required properties stay the server's to enforce, with its errors surfaced in-band). Calling convention (path/query/body all flat in params, describe serves the schema, paginated listings surface their geared_pagination cursor as next_page to pass back as page) lives in internal/mcpserver/dispatch.go; it should migrate to the toolkit once a second instance proves it.

Catalog snapshot. internal/mcpserver/model/ vendors hey-sdk's behavior-model.json + openapi.json at go/v0.28.0 — the same version go.mod pins — synced by scripts/sync-mcp-model.sh with provenance recorded. Unmapped SDK tags are pinned by test, so growing the surface is a deliberate one-line change plus a snapshot refresh.

Auth composition with #355/#356. hey mcp never touches the login flow: it reads stored credentials through the same auth.Manager → SDK bridge as every other command (requireAuth() + the package sdk client), and the Manager's proactive refresh keeps a long-running server alive across token expiry. The ephemeral-port listener (#356) and per-install id (#355) change how tokens are acquired, not how they're read, so this composes with both unchanged — based on main; a rebase after they land is a no-op for this diff.

Tests

  • internal/mcpserver: catalog derivation pinned (seven tools, unmapped tags accounted, snapshot ref asserted against go.mod's hey-sdk version); buildRequest table (path substitution/escaping, scalar formatting, query encoding, body gathering, schema-checked strays, missing/malformed params); wire-level tests via the toolkit's mcptest harness (tools/list, describe, dispatch-through-API, read-only refusal, domain narrowing).
  • internal/cmd: command registration, account-scope policy, auth requirement, and an end-to-end test running the actual hey mcp command against an httptest HEY server through the transport seam — MCP initialize + tools/list + a tool call, asserting the CLI's bearer token rides on the dispatched request. --read-only/--domains passthrough proven over the wire.
  • make check green except internal/tui's pre-existing unix-socket-path failures on this macOS sandbox (fail on main too, as #356 also notes). go test -race clean on the new packages. Size budget: 34.3 MiB of 37 (the embedded 425 KB model is noise).