Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,7 @@ CMD basecamp lineup list
CMD basecamp lineup update
CMD basecamp login
CMD basecamp logout
CMD basecamp mcp
CMD basecamp me
CMD basecamp messageboards
CMD basecamp messageboards show
Expand Down Expand Up @@ -10701,6 +10702,29 @@ FLAG basecamp logout --stats type=bool
FLAG basecamp logout --styled type=bool
FLAG basecamp logout --todolist type=string
FLAG basecamp logout --verbose type=count
FLAG basecamp mcp --account type=string
FLAG basecamp mcp --agent type=bool
FLAG basecamp mcp --cache-dir type=string
FLAG basecamp mcp --count type=bool
FLAG basecamp mcp --domains type=stringSlice
FLAG basecamp mcp --help type=bool
FLAG basecamp mcp --hints type=bool
FLAG basecamp mcp --ids-only type=bool
FLAG basecamp mcp --in type=string
FLAG basecamp mcp --jq type=string
FLAG basecamp mcp --json type=bool
FLAG basecamp mcp --markdown type=bool
FLAG basecamp mcp --md type=bool
FLAG basecamp mcp --no-hints type=bool
FLAG basecamp mcp --no-stats type=bool
FLAG basecamp mcp --profile type=string
FLAG basecamp mcp --project type=string
FLAG basecamp mcp --quiet type=bool
FLAG basecamp mcp --read-only type=bool
FLAG basecamp mcp --stats type=bool
FLAG basecamp mcp --styled type=bool
FLAG basecamp mcp --todolist type=string
FLAG basecamp mcp --verbose type=count
FLAG basecamp me --account type=string
FLAG basecamp me --agent type=bool
FLAG basecamp me --cache-dir type=string
Expand Down Expand Up @@ -18137,6 +18161,7 @@ SUB basecamp lineup list
SUB basecamp lineup update
SUB basecamp login
SUB basecamp logout
SUB basecamp mcp
SUB basecamp me
SUB basecamp messageboards
SUB basecamp messageboards show
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,31 @@ To pick up a newer plugin version later, refresh the marketplace with

See [install.md](install.md) for step-by-step setup instructions.

### MCP Server

`basecamp mcp` runs an MCP (Model Context Protocol) server on stdin/stdout,
serving Basecamp as tools backed by your signed-in account — no separate
binary or extra credentials. Register it with any MCP client as a stdio
server:

```bash
claude mcp add basecamp -- basecamp mcp
```

Fifteen domain tools (`basecamp_projects`, `basecamp_todos`, `basecamp_cards`,
`basecamp_messages`, `basecamp_campfires`, `basecamp_boosts`,
`basecamp_schedules`, `basecamp_files`, `basecamp_people`,
`basecamp_automation`, `basecamp_reports`, `basecamp_everything`,
`basecamp_clientside`, `basecamp_forwards`, `basecamp_account`) cover the
Basecamp API, scoped to the configured account. Each tool takes
`{"action": "...", "params": {...}}` and serves per-action schemas through
its `describe` action.

```bash
basecamp mcp --read-only # serve only read-only actions
basecamp mcp --domains projects,todos # narrow the served surface (fails closed on unknown keys)
```

## Configuration

```
Expand Down
4 changes: 4 additions & 0 deletions e2e/smoke/smoke_lifecycle.bats
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ load smoke_helper
mark_out_of_scope "Raw API passthrough — tested via specific commands"
}

@test "mcp is out of scope" {
mark_out_of_scope "Long-running MCP stdio server — covered by Go wire tests in internal/mcpserver and internal/commands"
}

@test "skill install is out of scope" {
mark_out_of_scope "Modifies Claude Code config"
}
Expand Down
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
charm.land/lipgloss/v2 v2.0.6
github.com/basecamp/basecamp-sdk/go v0.14.0
github.com/basecamp/cli v0.2.2-0.20260828032417-844e9f965144
github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d
Comment thread
jeremy marked this conversation as resolved.
Comment thread
jeremy marked this conversation as resolved.
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/glamour v1.0.0
github.com/charmbracelet/huh v1.0.0
Expand All @@ -16,6 +17,7 @@ require (
github.com/fsnotify/fsnotify v1.10.1
github.com/gofrs/flock v0.13.0
github.com/itchyny/gojq v0.12.19
github.com/modelcontextprotocol/go-sdk v1.7.0
github.com/sigstore/sigstore-go v1.3.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
Expand Down Expand Up @@ -86,6 +88,7 @@ require (
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/google/certificate-transparency-go v1.3.3 // indirect
github.com/google/go-containerregistry v0.21.7 // indirect
github.com/google/jsonschema-go v0.4.3 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
Expand All @@ -109,6 +112,8 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.11.0 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/sigstore/protobuf-specs v0.5.1 // indirect
github.com/sigstore/rekor v1.5.3 // indirect
Expand All @@ -119,6 +124,7 @@ require (
github.com/transparency-dev/formats v0.1.1 // indirect
github.com/transparency-dev/merkle v0.0.2 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
Expand All @@ -128,8 +134,10 @@ require (
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/term v0.45.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 // indirect
google.golang.org/grpc v1.82.1 // indirect
Expand Down
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ github.com/basecamp/basecamp-sdk/go v0.14.0 h1:Jyzmu3ucqwVe+YNC1zWJBOdPjBaTapyZW
github.com/basecamp/basecamp-sdk/go v0.14.0/go.mod h1:BlEtZTW78rOY5geVtKaCiccYT9Jz+QGgPuXLo2pROWk=
github.com/basecamp/cli v0.2.2-0.20260828032417-844e9f965144 h1:ZJxdRGsb6QyuXNFge6wVx7772e8kDHC9a72aBokaTwM=
github.com/basecamp/cli v0.2.2-0.20260828032417-844e9f965144/go.mod h1:iTBTaWvsPEFIcZfkxQHEfISyJ6sZ7036K6bNx0RY3EE=
github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d h1:zEQVGq1x1nhKMZ2TudFAcSJ32CHT8richI1vQakIKz4=
github.com/basecamp/mcp v0.0.0-20260828100356-2d6f44b51e9d/go.mod h1:Ee2c/q1/pg+5T5741PIuA3s6VJMQC7I0XBNXIHIujzA=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
Expand Down Expand Up @@ -248,6 +250,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-containerregistry v0.21.7 h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU=
github.com/google/go-containerregistry v0.21.7/go.mod h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/trillian v1.7.3 h1:hziW+vo4czis48tzx2GK5xRBl/ZxBA9B0/UR5avXOro=
Expand Down Expand Up @@ -328,6 +332,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44=
github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
Expand Down Expand Up @@ -367,6 +373,10 @@ github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgm
github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k=
github.com/secure-systems-lab/go-securesystemslib v0.11.0 h1:iuCR9kcMFD4QurdKrGvPLoKZLv9YvwPYVr0473BdtFs=
github.com/secure-systems-lab/go-securesystemslib v0.11.0/go.mod h1:+PMOTjUGwHj2vcZ+TFKlb1tXRbrdWE1LYDT5i9JC80Q=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
Expand Down Expand Up @@ -423,6 +433,8 @@ github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG
github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
Expand Down Expand Up @@ -482,6 +494,8 @@ golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.283.0 h1:0lkp8u0MPwJVHqRL+nJlMAoZVVzbmiXmFHXMOTmSPik=
Expand Down
1 change: 1 addition & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ func Execute() {
cmd.AddCommand(commands.NewTUICmd())
cmd.AddCommand(commands.NewBonfireCmd())
cmd.AddCommand(commands.NewAgentHookCmd())
cmd.AddCommand(commands.NewMCPCmd())

// Tier-2 stdin guard: reject a stray literal "-" when stdin is piped,
// everywhere a command doesn't explicitly accept it — except cobra's
Expand Down
1 change: 1 addition & 0 deletions internal/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ func CommandCategories() []CommandCategory {
{Name: "tui", Category: "additional", Description: "Launch the Basecamp workspace", Experimental: true, DevOnly: true},
{Name: "bonfire", Category: "additional", Description: "Multi-chat orchestration", Actions: []string{"split", "layout"}, Experimental: true, DevOnly: true},
{Name: "api", Category: "additional", Description: "Raw API access"},
{Name: "mcp", Category: "additional", Description: "Serve Basecamp to MCP clients over stdio"},
{Name: "help", Category: "additional", Description: "Show help"},
{Name: "version", Category: "additional", Description: "Show version"},
},
Expand Down
1 change: 1 addition & 0 deletions internal/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func buildRootWithAllCommands() *cobra.Command {
root.AddCommand(commands.NewTUICmd())
root.AddCommand(commands.NewProfileCmd())
root.AddCommand(commands.NewBonfireCmd())
root.AddCommand(commands.NewMCPCmd())
commands.InstallDashGuard(root)
root.InitDefaultHelpCmd()
return root
Expand Down
78 changes: 78 additions & 0 deletions internal/commands/mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package commands

import (
"log/slog"
"os"
"os/signal"
"syscall"

"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/spf13/cobra"

"github.com/basecamp/basecamp-cli/internal/appctx"
"github.com/basecamp/basecamp-cli/internal/mcpserver"
"github.com/basecamp/basecamp-cli/internal/output"
)

// mcpTransport is a seam so tests can drive the server over in-memory
// transports instead of the process's stdin/stdout.
var mcpTransport = func() mcp.Transport { return &mcp.StdioTransport{} }

// NewMCPCmd creates the mcp command serving Basecamp over MCP on stdio.
func NewMCPCmd() *cobra.Command {
var readOnly bool
var domains []string

cmd := &cobra.Command{
Use: "mcp",
Short: "Serve Basecamp to MCP clients over stdio",
Long: "Run an MCP (Model Context Protocol) server on stdin/stdout, serving Basecamp\n" +
"projects, todos, cards, messages, and more as tools backed by your signed-in\n" +
"account.\n\n" +
"Register it with an MCP client as a stdio server, e.g.:\n\n" +
" claude mcp add basecamp -- basecamp mcp",
Example: ` basecamp mcp
basecamp mcp --read-only
basecamp mcp --domains projects,todos,cards`,
Args: cobra.NoArgs,
Annotations: map[string]string{
"agent_notes": "Long-running server; stdout speaks the MCP wire protocol. Not for interactive use.",
},
RunE: func(cmd *cobra.Command, args []string) error {
app := appctx.FromContext(cmd.Context())

if !app.Auth.IsAuthenticated() {
return output.ErrAuth("Not authenticated. Run: basecamp auth login")
}
// Stdio belongs to the MCP wire, so the account cannot be
// resolved interactively: require it configured up front, like
// any other account-scoped command running non-interactively.
if err := app.RequireAccount(); err != nil {
return err
}

srv, err := mcpserver.New(app.Account(), mcpserver.Config{ReadOnly: readOnly, Domains: domains})
if err != nil {
return err
}

ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
defer stop()

// Log to stderr: stdout belongs to the MCP wire.
logger := slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), nil))
session, err := srv.BuildMCPServer(logger).Connect(ctx, mcpTransport(), nil)
if err != nil {
return err
Comment thread
jeremy marked this conversation as resolved.
}
logger.Info("MCP server running on stdio", "tools", len(srv.Domains()), "read_only", readOnly)

return session.Wait()
},
}

cmd.Flags().BoolVar(&readOnly, "read-only", false, "Serve only read-only actions")
cmd.Flags().StringSliceVar(&domains, "domains", nil, "Narrow to specific domains (comma-separated; default all)")

return cmd
}
Loading
Loading