Basecamp MCP

Overview
Basecamp MCP is a Model Context Protocol server that turns a Basecamp 3 account into a set of on-demand tools any MCP-compatible AI client, Claude Desktop, Cursor, or anything else that speaks MCP, can call directly. Point an agent at it, sign in with your own Basecamp account through OAuth, and the agent can read project message boards, to-dos, documents, Campfire chat, and file attachment metadata as native tool calls, with no separate export step and no copy-pasting a doc into a prompt. It is deliberately read-only for this version: a pure context provider that hands an agent real project history to reason over, rather than an integration that goes off and takes actions in your name.
Technologies
- Node.js 22, TypeScript
- The official MCP SDK for the server itself, with both Streamable HTTP and stdio transports
- Express for the OAuth routes and the per-user MCP endpoint
- simple-oauth2 for Basecamp’s OAuth 2.0 authorization-code flow
- better-sqlite3 for per-user token storage, with WAL mode for concurrent reads
- got as the underlying HTTP client for Basecamp’s REST API
- node-html-parser for the HTML-to-Markdown conversion pipeline
- zod for input validation on every tool’s parameters
- Docker for containerized deployment
Features
- Eleven read-only tools covering projects, message boards, to-dos, documents, Campfire chat, and attachments, from listing all projects down to fetching a single to-do or document with full detail
- Per-user OAuth: each team member authenticates with their own Basecamp account and gets back a private MCP URL, a random UUID path rather than a shared bearer token, that binds every request to their identity. No server-wide credential, no permission bleed between users
- Proactive token refresh with a five-minute buffer before expiry, plus a per-user mutex so two near-simultaneous requests for the same user cannot both trigger a refresh and race each other over Basecamp’s rotating refresh tokens
- Rate-limit-aware HTTP client: on a 429 it reads the Retry-After header, falling back to exponential backoff with jitter, and retries automatically instead of surfacing a raw HTTP error to the agent
- One paginated envelope for every list tool, tracking the items themselves plus whether more pages exist and what the next page number is, parsed from Basecamp’s Link headers, with a hard cap of 100 items and 50KB per response so a single tool call cannot blow out an agent’s context window
- HTML-to-Markdown conversion for every doc, message, and comment body, including Basecamp-specific tags for attachments and mentions, so an agent reads clean text instead of raw rich-text markup
- Structured, typed errors, expired token, rate limited, not found, permission denied, and so on, with a retryable flag, so an agent can tell try again apart from ask the user to re-authenticate
- Dual transport: Streamable HTTP with session management for production, or a single stdio process for local single-user development
Development and Challenges
One MCP URL per user, not one shared token
The natural first draft of an internal tool like this is a single server-side Basecamp credential that everyone’s agent points at, simplest to wire up, and the fastest way to get every tool call attributed to whoever set up the integration instead of the person actually running it. Basecamp’s OAuth is per-account anyway, so the real fix was structural: each user completes their own OAuth flow, gets a private, random MCP URL, and every session that connects through that URL is bound to their Basecamp identity for its lifetime. There is no shared secret to leak, and no way for one person’s agent to quietly act as someone else.
Keeping a single tool call from drowning the agent
A Basecamp project can have thousands of to-dos and comment threads, and the obvious fetch everything so the agent has full context tool is exactly the one that burns through an agent’s context window on the first call. Every list tool enforces the same pagination envelope, a hard cap of 100 items and 50KB of serialized JSON per response, so the agent asks for more deliberately instead of getting it by accident. Rich text gets the same treatment: docs and messages are converted from Basecamp’s HTML straight to Markdown, including its custom attachment and mention tags, so the agent reads clean text instead of paying token cost for markup it cannot use anyway.
Refresh tokens that rotate, read under load
Basecamp expires access tokens in a couple of hours and rotates the refresh token on every use, which means two things have to be true at once: tokens need refreshing proactively, before they expire, a five-minute buffer rather than waiting for a 401, and two requests arriving close together for the same user cannot both try to refresh, the second one would hand Basecamp an already-rotated refresh token and fail outright. A per-user mutex makes every concurrent caller await the same in-flight refresh instead of racing it.
Conclusion
The interesting part of this project was never the Basecamp API itself, it is a well-documented, fairly ordinary REST API. The real work was in the boundary between that API and an AI agent: making sure the agent only ever sees what a real per-user OAuth grant allows, and only ever gets back as much data as it can actually use in one turn. Read-only was a deliberate v1 constraint, not a limitation to design around, the same per-user auth and pagination discipline is exactly what write operations will need to build on next.