diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json new file mode 100644 index 0000000..17c2f46 --- /dev/null +++ b/.cursor-plugin/marketplace.json @@ -0,0 +1,17 @@ +{ + "name": "37signals", + "owner": { + "name": "37signals", + "email": "support@37signals.com" + }, + "metadata": { + "description": "37signals plugins for Cursor: Basecamp through its official hosted MCP server" + }, + "plugins": [ + { + "name": "basecamp", + "source": "plugins/cursor/basecamp", + "description": "Find, read, and update projects, to-dos, cards, messages, docs, and schedules." + } + ] +} diff --git a/README.md b/README.md index 036072c..3d92acd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 37signals Claude Code Plugins +# 37signals agent plugins [Claude Code plugin](https://docs.anthropic.com/en/docs/claude-code/plugins) marketplace for Basecamp, HEY, and Fizzy — skills, hooks, and commands that @@ -6,10 +6,22 @@ let Claude interact with your 37signals products. We also share our internal development skills as plugins. ``` -claude plugin marketplace add basecamp/claude-plugins +claude plugin marketplace add basecamp/plugins claude plugin install ``` +This repository was `basecamp/claude-plugins` until 2026-10-12. GitHub +redirects the old name, and the marketplace is still `37signals`, so existing +installs keep working. + +## Cursor + +`.cursor-plugin/marketplace.json` lists the Basecamp plugin for Cursor, +`plugins/cursor/basecamp`: Basecamp's official hosted MCP server at +`https://mcp.basecamp.com/mcp`, with no API key to configure. Cursor's +Marketplace also feeds Grok Bot. Install it from **Cursor Settings → Plugins** +once it's listed. + ## Available plugins | Plugin | Description | @@ -67,9 +79,10 @@ This repo is mostly a thin marketplace index. Plugin source code (skills, hooks, commands) lives in each product's own repo. To contribute, open issues or PRs there. -The one plugin with files here, `plugins/basecamp` (the hosted plugin, not -listed in the marketplace), is a mirror. Its source -of truth is `plugins/claude/basecamp` in the (private) Basecamp connector +The plugins with files here, `plugins/basecamp` (the hosted plugin for +Claude, not listed in the Claude marketplace) and `plugins/cursor/basecamp` +(the same server for Cursor), are mirrors. Their source of truth is +`plugins/claude/basecamp` and `plugins/cursor/basecamp` in the (private) Basecamp connector repository, which tests it against the tools the connector actually serves. Don't edit the mirror; change it upstream, then run `bin/sync-basecamp-plugin ` to copy and validate it, and commit diff --git a/bin/check-marketplace b/bin/check-marketplace index 2c53564..76591df 100755 --- a/bin/check-marketplace +++ b/bin/check-marketplace @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Checks .claude-plugin/marketplace.json. Run by CI on every push and pull request. +# Checks .claude-plugin/marketplace.json and .cursor-plugin/marketplace.json. Run by CI on every push and pull request. # # 1. It parses, and every plugin name is unique. # 2. A plugin named "basecamp", if listed, has exactly the same source as @@ -53,3 +53,32 @@ if alias is not None: sys.exit(1 if failed else 0) PY echo "marketplace.json OK" + +# Cursor: .cursor-plugin/marketplace.json parses, names are unique, and each +# source is a folder here whose .cursor-plugin/plugin.json has the same name. +python3 - "$root" <<'PY' +import collections, json, os, sys + +root = sys.argv[1] +with open(os.path.join(root, ".cursor-plugin", "marketplace.json")) as f: + plugins = json.load(f)["plugins"] + +failed = False +for name, count in collections.Counter(p["name"] for p in plugins).items(): + if count > 1: + print(f"error: Cursor plugin name {name!r} is listed {count} times", file=sys.stderr) + failed = True +for p in plugins: + manifest = os.path.join(root, p["source"], ".cursor-plugin", "plugin.json") + if not os.path.isfile(manifest): + print(f"error: Cursor plugin {p['name']!r}: no {manifest}", file=sys.stderr) + failed = True + continue + with open(manifest) as f: + if json.load(f)["name"] != p["name"]: + print(f"error: Cursor plugin {p['name']!r}: plugin.json name differs", file=sys.stderr) + failed = True + +sys.exit(1 if failed else 0) +PY +echo "Cursor marketplace.json OK" diff --git a/bin/sync-basecamp-plugin b/bin/sync-basecamp-plugin index 9e00d4d..90e2da2 100755 --- a/bin/sync-basecamp-plugin +++ b/bin/sync-basecamp-plugin @@ -1,12 +1,14 @@ #!/usr/bin/env bash -# Mirror the hosted Basecamp plugin from basecamp-mcp-server, its source of truth. +# Mirror the hosted Basecamp plugins (Claude and Cursor) from basecamp-mcp-server, +# their source of truth. # # bin/sync-basecamp-plugin copy, validate, print the source commit # bin/sync-basecamp-plugin --check exit 1 if plugins/basecamp differs # -# The source is plugins/claude/basecamp in that checkout. Sync from a clean -# checkout of its main branch, and put the printed commit in the commit message. -# Never edit plugins/basecamp here: change it upstream and sync. +# The sources are plugins/claude/basecamp and plugins/cursor/basecamp in that +# checkout, mirrored to plugins/basecamp and plugins/cursor/basecamp. Sync from a +# clean checkout of its main branch, and put the printed commit in the commit +# message. Never edit the mirrors here: change them upstream and sync. set -euo pipefail check=0 @@ -22,36 +24,52 @@ fi root="$(cd "$(dirname "$0")/.." && pwd)" upstream="$(cd "$1" && pwd)" -src="$upstream/plugins/claude/basecamp" -dest="$root/plugins/basecamp" -if [[ ! -f "$src/.claude-plugin/plugin.json" ]]; then - echo "error: $src is not the Basecamp plugin (no .claude-plugin/plugin.json)" >&2 - exit 2 -fi +# source (in basecamp-mcp-server) → mirror (here), and the manifest that proves it +pairs=( + "plugins/claude/basecamp:plugins/basecamp:.claude-plugin/plugin.json" + "plugins/cursor/basecamp:plugins/cursor/basecamp:.cursor-plugin/plugin.json" +) -if [[ $check -eq 1 ]]; then - if diff -r "$src" "$dest"; then - echo "plugins/basecamp matches $(git -C "$upstream" rev-parse --short HEAD)" - else - echo "plugins/basecamp has drifted from basecamp-mcp-server; run bin/sync-basecamp-plugin" >&2 - exit 1 +for pair in "${pairs[@]}"; do + IFS=: read -r rel dest_rel manifest <<<"$pair" + if [[ ! -f "$upstream/$rel/$manifest" ]]; then + echo "error: $upstream/$rel is not a Basecamp plugin (no $manifest)" >&2 + exit 2 fi - exit 0 -fi +done -if [[ -n "$(git -C "$upstream" status --porcelain -- plugins/claude/basecamp)" ]]; then - echo "error: uncommitted changes under $src; sync from a committed source" >&2 - exit 2 +if [[ $check -eq 1 ]]; then + drifted=0 + for pair in "${pairs[@]}"; do + IFS=: read -r rel dest_rel manifest <<<"$pair" + if ! diff -r "$upstream/$rel" "$root/$dest_rel"; then + echo "$dest_rel has drifted from basecamp-mcp-server; run bin/sync-basecamp-plugin" >&2 + drifted=1 + fi + done + [[ $drifted -eq 0 ]] && echo "mirrors match $(git -C "$upstream" rev-parse --short HEAD)" + exit $drifted fi -rm -rf "$dest" -mkdir -p "$dest" -cp -R "$src/." "$dest/" -find "$dest" -name .DS_Store -delete +for pair in "${pairs[@]}"; do + IFS=: read -r rel dest_rel manifest <<<"$pair" + if [[ -n "$(git -C "$upstream" status --porcelain -- "$rel")" ]]; then + echo "error: uncommitted changes under $upstream/$rel; sync from a committed source" >&2 + exit 2 + fi +done + +for pair in "${pairs[@]}"; do + IFS=: read -r rel dest_rel manifest <<<"$pair" + rm -rf "${root:?}/$dest_rel" + mkdir -p "$root/$dest_rel" + cp -R "$upstream/$rel/." "$root/$dest_rel/" + find "$root/$dest_rel" -name .DS_Store -delete +done if command -v claude >/dev/null 2>&1; then - claude plugin validate "$dest" + claude plugin validate "$root/plugins/basecamp" else echo "claude not installed; skipping claude plugin validate" >&2 fi diff --git a/plugins/basecamp/.claude-plugin/plugin.json b/plugins/basecamp/.claude-plugin/plugin.json index 0468464..05027bd 100644 --- a/plugins/basecamp/.claude-plugin/plugin.json +++ b/plugins/basecamp/.claude-plugin/plugin.json @@ -1,16 +1,21 @@ { "name": "basecamp", "displayName": "Basecamp", - "version": "0.1.0", + "version": "0.2.0", "description": "Work in your Basecamp projects from Claude: catch up on a project, recap your week, triage your Hey! menu, plan work into to-dos and cards, draft a check-in answer, and prep for a meeting, through Basecamp's own hosted connector.", "author": { "name": "37signals", "email": "support@37signals.com", "url": "https://basecamp.com" }, - "homepage": "https://basecamp.com", - "repository": "https://github.com/basecamp/claude-plugins", + "homepage": "https://basecamp.com/ai", + "repository": "https://github.com/basecamp/plugins", "license": "MIT", + "icon": "./assets/logo.png", + "documentationUrl": "https://basecamp.com/ai", + "supportUrl": "https://basecamp.com/support", + "privacyPolicyUrl": "https://37signals.com/policies/privacy", + "termsOfServiceUrl": "https://37signals.com/policies/terms", "keywords": [ "basecamp", "projects", diff --git a/plugins/basecamp/LICENSE b/plugins/basecamp/LICENSE new file mode 100644 index 0000000..cd55c4e --- /dev/null +++ b/plugins/basecamp/LICENSE @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) 2026 37signals, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +The Basecamp name and logo are trademarks of 37signals, LLC and are not +licensed under this license. diff --git a/plugins/basecamp/assets/logo.png b/plugins/basecamp/assets/logo.png new file mode 100644 index 0000000..017b876 Binary files /dev/null and b/plugins/basecamp/assets/logo.png differ diff --git a/plugins/cursor/basecamp/.cursor-plugin/plugin.json b/plugins/cursor/basecamp/.cursor-plugin/plugin.json new file mode 100644 index 0000000..9df4c7a --- /dev/null +++ b/plugins/cursor/basecamp/.cursor-plugin/plugin.json @@ -0,0 +1,38 @@ +{ + "name": "basecamp", + "displayName": "Basecamp", + "version": "0.2.0", + "minClientVersions": { + "cursor": "3.13.0" + }, + "description": "Find, read, and update projects, to-dos, cards, messages, docs, and schedules.", + "author": { + "name": "37signals", + "email": "support@37signals.com" + }, + "homepage": "https://basecamp.com/ai", + "repository": "https://github.com/basecamp/plugins", + "license": "MIT", + "logo": "assets/logo.png", + "keywords": [ + "basecamp", + "37signals", + "projects", + "to-dos", + "todos", + "cards", + "messages", + "campfire", + "project management", + "productivity", + "mcp" + ], + "category": "integrations", + "tags": [ + "basecamp", + "project management", + "productivity", + "mcp" + ], + "mcpServers": "./mcp.json" +} diff --git a/plugins/cursor/basecamp/LICENSE b/plugins/cursor/basecamp/LICENSE new file mode 100644 index 0000000..cd55c4e --- /dev/null +++ b/plugins/cursor/basecamp/LICENSE @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) 2026 37signals, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +The Basecamp name and logo are trademarks of 37signals, LLC and are not +licensed under this license. diff --git a/plugins/cursor/basecamp/README.md b/plugins/cursor/basecamp/README.md new file mode 100644 index 0000000..dde4fbe --- /dev/null +++ b/plugins/cursor/basecamp/README.md @@ -0,0 +1,57 @@ +# Basecamp + +Cursor plugin that connects agents to [Basecamp](https://basecamp.com) through Basecamp's official remote [Model Context Protocol](https://modelcontextprotocol.io/) server, built and run by 37signals. + +Find, read, and update projects, to-dos, cards, messages, comments, documents, schedules, check-ins, and Campfire chats in the signed-in Basecamp account. + +## Install + +1. Open **Cursor Settings → Plugins**. +2. Search for **Basecamp**. +3. Click **Install**, then complete the Basecamp sign-in prompt. + +Or run `/add-plugin basecamp` in chat. + +## MCP + +```json +{ + "mcpServers": { + "basecamp": { + "type": "http", + "url": "https://mcp.basecamp.com/mcp", + "placement": "server" + } + } +} +``` + +Auth is OAuth 2.1 with Dynamic Client Registration and PKCE. Cursor registers itself and prompts for Basecamp sign-in when the plugin connects — there is no API key or client ID to configure. On the consent screen you choose the Basecamp account and Read-only or Full access. + +## What agents can do + +| Category | Capabilities | +| --- | --- | +| My work | Assignments, Hey! notifications, schedule, and what's new across projects | +| To-dos and cards | Create, assign, complete, and move to-dos and Card Table cards | +| Messages and docs | Read and post messages, comments, and documents | +| Schedules, check-ins, and chat | Add schedule entries, read and answer check-ins, and read and post in Campfire | +| Search | `search` across the account and `get_by_url` for any Basecamp link | + +Every Basecamp operation is its own tool, annotated read-only, additive (creates) or destructive. The hosted runtime is the source of truth for tool names and schemas. + +## Notes + +- Tool calls run as the Basecamp person who authorizes the connection, with their permissions. +- Most removals go to the Basecamp trash, where they can be restored. Permanent deletes ask for confirmation where the client supports it. +- Disconnect any time from **Connected apps** in your Basecamp profile menu. + +## Docs + +- Basecamp and AI assistants: https://basecamp.com/ai +- Server URL: https://mcp.basecamp.com/mcp +- Basecamp CLI and agent skill: https://github.com/basecamp/basecamp-cli + +## License + +MIT, © 37signals. The Basecamp name and logo are trademarks of 37signals and aren't licensed under it. diff --git a/plugins/cursor/basecamp/assets/logo.png b/plugins/cursor/basecamp/assets/logo.png new file mode 100644 index 0000000..017b876 Binary files /dev/null and b/plugins/cursor/basecamp/assets/logo.png differ diff --git a/plugins/cursor/basecamp/mcp.json b/plugins/cursor/basecamp/mcp.json new file mode 100644 index 0000000..fa1447b --- /dev/null +++ b/plugins/cursor/basecamp/mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "basecamp": { + "type": "http", + "url": "https://mcp.basecamp.com/mcp", + "placement": "server" + } + } +}