@kaiseraraujo/basecamp-mcp-server
v1.2.0
Published
MCP server for the Basecamp API — projects, to-dos, messages, documents, Campfire chat and people
Maintainers
Readme
basecamp-mcp-server
An MCP server for the Basecamp API. It gives an MCP client — Claude Code, Claude Desktop, or anything else that speaks the protocol — read and write access to Basecamp projects, to-dos, message boards, documents, Campfire chat and people.
Published on npm as @kaiseraraujo/basecamp-mcp-server.
Why this exists
Basecamp's own Agents preview ships a CLI and SDKs, but no MCP server. That's a real gap if your agent isn't running on your laptop: a CLI has to be installed on the machine doing the work, so a remote or cloud session can't reach it. This server is a plain stdio MCP process talking to the public Basecamp 4 API over HTTPS, so it works anywhere the client can start a Node process — locally, in a container, or in a hosted session.
Design notes worth knowing before you wire it up:
- OAuth refresh is handled for you. You supply a refresh token once; the server exchanges it for access tokens and caches them in memory until a minute before they expire.
- Responses are size-capped. List results render to at most ~25,000 characters, halving the page until they fit, so a project with a thousand to-dos can't flood the model's context. Every truncated or paged response says so and tells you the next page to ask for.
- Rich text is flattened. Basecamp stores message and document bodies as HTML; this server converts them to plain text before handing them to the model.
- Every tool returns both a readable text rendering and
structuredContentfor programmatic use. Passresponse_format: "json"on any list or get tool to see the raw records instead.
Tools
Projects
| Tool | What it does |
| --- | --- |
| basecamp_list_projects | List active projects with their IDs and enabled tools |
| basecamp_get_project | One project in full, including the dock with per-tool IDs |
| basecamp_create_project | Create a project. Note that Basecamp enforces a per-account project limit and answers 507 once you are at it — on the Free plan that limit is 1 |
| basecamp_update_project | Rename a project or change its description. Reads first and resends untouched fields, so a partial update stays partial |
To-dos
| Tool | What it does |
| --- | --- |
| basecamp_list_todolists | The to-do lists in a project |
| basecamp_create_todolist | Create a to-do list in a project's To-dos tool, resolving the todoset automatically. A project's To-dos tool starts empty, so a list has to exist before basecamp_create_todo can add anything |
| basecamp_list_todos | To-dos in a list; completed: true returns the completed ones instead |
| basecamp_create_todo | Add a to-do, optionally assigned and dated |
| basecamp_update_todo | Change a to-do's title, description, assignees or dates. Basecamp's API clears any field you omit, so this tool reads the to-do first and resends its current values for anything you didn't supply — a partial update stays partial. Pass null for due_on/starts_on to clear a date, or [] for assignee_ids to clear assignees |
| basecamp_move_todo | Move a to-do into another list in the same project, or reorder it within its current one. The to-do keeps its ID, comments, assignees and history — the tool for restructuring a project, since deleting and recreating loses all of that |
| basecamp_complete_todo | Complete a to-do, or reopen it with completed: false |
| basecamp_delete_todo | Move a to-do to the project's trash. Basecamp has no permanent delete through the API — the to-do leaves the list but stays recoverable from the project's trash for a limited period (about 30 days) before Basecamp purges it |
Messages
| Tool | What it does |
| --- | --- |
| basecamp_list_messages | Message board posts, newest first |
| basecamp_get_message | One post in full |
| basecamp_create_message | Post to the message board, or save a draft |
Documents
| Tool | What it does |
| --- | --- |
| basecamp_list_documents | Documents in the project's Docs & Files vault |
| basecamp_get_document | One document in full |
| basecamp_create_document | Create a document, published or drafted |
Campfire
| Tool | What it does |
| --- | --- |
| basecamp_list_chat_lines | Recent lines from the project's chat room |
| basecamp_post_chat_line | Post a line to the chat room |
Comments
| Tool | What it does |
| --- | --- |
| basecamp_list_comments | Comments on any to-do, message or document |
| basecamp_create_comment | Comment on any to-do, message or document |
People
| Tool | What it does |
| --- | --- |
| basecamp_list_people | Everyone in the account, or just one project's people |
| basecamp_whoami | The identity the server is acting as |
Setup
1. Register an OAuth application
Create an app at https://launchpad.37signals.com/integrations. Any redirect URI works, including
one nothing is listening on — you only need to read the authorization code out of the address bar.
http://localhost:8989/callback is a fine choice.
Note the Client ID and Client Secret it gives you.
2. Get a refresh token
Open this URL in a browser, substituting your own values:
https://launchpad.37signals.com/authorization/new?type=web_server&client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URIApprove the request. The browser will then fail to load the redirect URI — that is expected, since
nothing is listening there. Copy the code=... parameter out of the address bar and exchange it:
curl -X POST "https://launchpad.37signals.com/authorization/token?type=web_server\
&client_id=YOUR_CLIENT_ID\
&redirect_uri=YOUR_REDIRECT_URI\
&client_secret=YOUR_CLIENT_SECRET\
&code=THE_CODE_FROM_THE_ADDRESS_BAR"Keep the refresh_token from the response — that is the long-lived credential this server needs.
The access_token alongside it lasts two weeks; you don't need to save it, because the server
refreshes its own access tokens from the refresh token as needed.
3. Find your account ID
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
https://launchpad.37signals.com/authorization.jsonThe response lists the Basecamp accounts you belong to. Take the numeric id of the account whose
projects you want to work with (it's the same number that appears in your Basecamp URLs).
4. Install
The package is on npm as @kaiseraraujo/basecamp-mcp-server. You don't have to install it
explicitly — the .mcp.json example in step 6 runs it with npx, which fetches it on first use.
To put the basecamp-mcp-server command on your PATH instead:
npm install -g @kaiseraraujo/basecamp-mcp-serverTo run it from a source checkout — for development, or to pin a local build:
git clone https://github.com/kaiseraraujo/basecamp-mcp-server.git
cd basecamp-mcp-server
npm install
npm run build5. Configure the environment
| Variable | Required | Description |
| --- | --- | --- |
| BASECAMP_CLIENT_ID | yes | OAuth client ID from step 1 |
| BASECAMP_CLIENT_SECRET | yes | OAuth client secret from step 1 |
| BASECAMP_REFRESH_TOKEN | yes | Refresh token from step 2 |
| BASECAMP_ACCOUNT_ID | yes | Numeric account ID from step 3 |
| BASECAMP_USER_AGENT | no | Identifies your integration to Basecamp; they ask for a contact address, e.g. My Integration ([email protected]) |
| BASECAMP_MAX_RETRIES | no | How many times to retry a failed request. Default 2; 0 disables retries |
These are credentials. They belong in your environment or your MCP client's config — never
committed to this repository. The included .gitignore covers .env files, but the safest place
is your client's config outside the repo.
6. Register it with your MCP client
An .mcp.json entry that runs the published package with npx — no path to keep in sync and no
build step:
{
"mcpServers": {
"basecamp": {
"command": "npx",
"args": ["-y", "@kaiseraraujo/basecamp-mcp-server"],
"env": {
"BASECAMP_CLIENT_ID": "your-client-id",
"BASECAMP_CLIENT_SECRET": "your-client-secret",
"BASECAMP_REFRESH_TOKEN": "your-refresh-token",
"BASECAMP_ACCOUNT_ID": "your-account-id",
"BASECAMP_USER_AGENT": "My Integration ([email protected])"
}
}
}
}If you are working from a source checkout instead, point at the built entry point with an absolute path:
{
"mcpServers": {
"basecamp": {
"command": "node",
"args": ["/absolute/path/to/basecamp-mcp-server/dist/index.js"],
"env": {
"BASECAMP_CLIENT_ID": "your-client-id",
"BASECAMP_CLIENT_SECRET": "your-client-secret",
"BASECAMP_REFRESH_TOKEN": "your-refresh-token",
"BASECAMP_ACCOUNT_ID": "your-account-id",
"BASECAMP_USER_AGENT": "My Integration ([email protected])"
}
}
}
}Restart the client and ask it to run basecamp_whoami to confirm the credentials work.
Claude Code cloud environments
In a Claude Code cloud environment the four required BASECAMP_* variables are set as environment
variables on the environment itself rather than in a config file, so setup is two lines:
npm install -g @kaiseraraujo/basecamp-mcp-server
claude mcp add basecamp --scope user -- basecamp-mcp-serverThe server picks the credentials up from the environment, so there is no env block to fill in.
Behaviour worth knowing
Pagination
Basecamp paginates every collection at 15 records and honours no page-size parameter —
per_page, page_size and limit are all ignored on the wire. Every list tool therefore
takes a limit (default 50, max 500) that is served by following the Link header inside
the server, so a 36-item to-do list comes back in one tool call rather than three. Whole
pages are kept, so a response can run up to 14 items past the number you asked for; that
keeps the reported next page exact and non-overlapping. Responses are still capped at
25,000 characters and report has_more when they are cut short.
Retries
Basecamp's edge intermittently returns 502s under concurrent writes, and rate-limits at 50
requests per 10-second window. Failed requests are retried automatically — twice by default,
exponential backoff with full jitter, honouring Retry-After. Set BASECAMP_MAX_RETRIES=0
to turn this off.
Retries are asymmetric on purpose. A GET is retried on 429, 500, 502, 503 and 504, plus
timeouts and dropped connections. A write is retried only on 429/502/503/504, where the
request did not reach the application, and never on a 500 or a lost connection — there the
record may well have been created, and a blind retry would duplicate it.
Other 5xx codes are left alone, since they describe something that will not clear by itself.
Basecamp answers an over-quota project creation with 507, for instance; retrying it would
only burn requests.
Errors carry Basecamp's own x-request-id as [Basecamp request ID: …], which is what
37signals support asks for when a failure needs chasing down.
content is plain text, description is HTML
On basecamp_create_todo and basecamp_update_todo the two fields treat markup in opposite
ways, and it is easy to get caught by:
content(the title) is stored and displayed verbatim. PassingA & Bputs the literal stringA & Bon screen, notA & B. Write the characters you want.descriptionis HTML, and renders the same entity asA & B.
The values are passed through unchanged rather than silently unescaped, since a title can legitimately contain an ampersand-escape.
Development
npm run build # compile TypeScript to dist/
npm start # run the built server on stdio
npm test # build, then run the transport tests
npm run clean # remove dist/test/client.test.mjs swaps Basecamp for a scripted axios adapter, so the pagination and
retry behaviour above is exercised exactly rather than waited on. It needs no credentials.
The server speaks JSON-RPC over stdio, so you can smoke-test it without Basecamp credentials — listing tools never touches the API:
printf '%s\n%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"1.0.0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
| node dist/index.jsSource layout:
src/basecamp.ts— OAuth token refresh, the authenticated HTTP wrapper, pagination, error mapping, and dock-tool resolutionsrc/format.ts— HTML stripping, markdown/JSON rendering, and the response size capsrc/types.ts— Basecamp record shapessrc/index.ts— tool definitionstest/client.test.mjs— transport tests for pagination, retries and error mapping
License
MIT
