diff --git a/.gitignore b/.gitignore index c8b68bf..8be0004 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ coverage.txt dist/* _site/* devtui -.idea \ No newline at end of file +.idea +.worktrees diff --git a/README.md b/README.md index 10c6ece..7f570c0 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,40 @@ Or download executable from [GitHub Releases](https://github.com/skatkov/devtui/ --- +## MCP + +Run DevTUI as an MCP server over stdio: + +```bash +devtui mcp +``` + +### MCP build +To include the MCP server, build with: + +```bash +go build -tags mcp ./... +``` + +### Claude Code + +Add DevTUI as an MCP server in `~/.claude/claude.json`: + +```json +{ + "mcpServers": { + "devtui": { + "command": "devtui", + "args": ["mcp"] + } + } +} +``` + +Make sure `devtui` is on your PATH, then restart Claude Code. + +--- + ## Documentation Generator DevTUI includes automated documentation generators for both CLI and TUI interfaces (not complete yet, though). diff --git a/cmd/mcp_disabled.go b/cmd/mcp_disabled.go new file mode 100644 index 0000000..787052f --- /dev/null +++ b/cmd/mcp_disabled.go @@ -0,0 +1,26 @@ +//go:build !mcp + +package cmd + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" +) + +var mcpCmd = &cobra.Command{ + Use: "mcp", + Short: "Run DevTUI as an MCP stdio server", + RunE: func(cmd *cobra.Command, args []string) error { + message := "mcp disabled; rebuild with -tags mcp" + if _, err := fmt.Fprintln(cmd.ErrOrStderr(), message); err != nil { + return err + } + return errors.New(message) + }, +} + +func init() { + rootCmd.AddCommand(mcpCmd) +} diff --git a/cmd/mcp_disabled_test.go b/cmd/mcp_disabled_test.go new file mode 100644 index 0000000..353e22c --- /dev/null +++ b/cmd/mcp_disabled_test.go @@ -0,0 +1,25 @@ +//go:build !mcp + +package cmd + +import ( + "bytes" + "strings" + "testing" +) + +func TestMCPDisabledMessage(t *testing.T) { + cmd := GetRootCmd() + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs([]string{"mcp"}) + + err := cmd.Execute() + if err == nil { + t.Fatalf("expected error when mcp tag disabled") + } + if !strings.Contains(buf.String(), "mcp disabled") { + t.Fatalf("expected disabled message, got: %s", buf.String()) + } +} diff --git a/cmd/mcp_enabled.go b/cmd/mcp_enabled.go new file mode 100644 index 0000000..ae423f4 --- /dev/null +++ b/cmd/mcp_enabled.go @@ -0,0 +1,33 @@ +//go:build mcp + +package cmd + +import ( + mcp "github.com/skatkov/devtui-mcp" + "github.com/spf13/cobra" +) + +var mcpCmd = &cobra.Command{ + Use: "mcp", + Short: "Run DevTUI as an MCP stdio server", + RunE: func(cmd *cobra.Command, args []string) error { + tools := mcp.BuildTools(GetRootCmd()) + server := mcp.NewServer(mcp.ServerConfig{ + Tools: tools, + ServerInfo: mcp.ServerInfo{ + Name: "devtui", + Version: GetVersion(), + }, + Call: func(_ string, params mcp.CallParams) (string, error) { + root := GetRootCmd() + return mcp.ExecuteTool(root, params) + }, + }) + + return mcp.ServeStdio(server, cmd.InOrStdin(), cmd.OutOrStdout()) + }, +} + +func init() { + rootCmd.AddCommand(mcpCmd) +} diff --git a/cmd/mcp_enabled_test.go b/cmd/mcp_enabled_test.go new file mode 100644 index 0000000..3c39ac7 --- /dev/null +++ b/cmd/mcp_enabled_test.go @@ -0,0 +1,40 @@ +//go:build mcp + +package cmd + +import ( + "bytes" + "testing" +) + +func TestMCPCommandListsTools(t *testing.T) { + cmd := GetRootCmd() + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetIn(bytes.NewBufferString("{\"id\":1,\"method\":\"tools/list\"}\n")) + cmd.SetArgs([]string{"mcp"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("execute failed: %v", err) + } + if buf.Len() == 0 { + t.Fatalf("expected output") + } +} + +func TestMCPCommandInitialize(t *testing.T) { + cmd := GetRootCmd() + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetIn(bytes.NewBufferString("{\"id\":1,\"method\":\"initialize\"}\n")) + cmd.SetArgs([]string{"mcp"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("execute failed: %v", err) + } + if buf.Len() == 0 { + t.Fatalf("expected output") + } +} diff --git a/cmd/version.go b/cmd/version.go index d79f4e5..402553e 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -13,6 +13,11 @@ var ( date = "unknown" ) +// GetVersion returns the raw version string. +func GetVersion() string { + return version +} + // GetVersionShort returns a short version string suitable for single-line output. func GetVersionShort() string { return fmt.Sprintf("%s (commit: %s, built: %s)", version, commit, date) diff --git a/go.mod b/go.mod index 6d99360..f9219f6 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 github.com/muesli/reflow v0.3.0 github.com/pelletier/go-toml/v2 v2.2.4 + github.com/skatkov/devtui-mcp v0.0.0-20260130223332-c6b090edb9ea github.com/spf13/cobra v1.10.2 github.com/tiagomelo/go-clipboard v0.1.2 github.com/twpayne/go-jsonstruct/v3 v3.3.0 @@ -98,7 +99,7 @@ require ( github.com/yuin/goldmark v1.7.13 // indirect github.com/yuin/goldmark-emoji v1.0.6 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.44.0 // indirect + golang.org/x/crypto v0.45.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/sync v0.18.0 // indirect diff --git a/go.sum b/go.sum index c925e4a..4bb8a0e 100644 --- a/go.sum +++ b/go.sum @@ -198,6 +198,8 @@ github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/skatkov/devtui-mcp v0.0.0-20260130223332-c6b090edb9ea h1:ZF6Z+sJL5Hgia7l/+uS/ueQLXOouAq+qDljWyGDfRao= +github.com/skatkov/devtui-mcp v0.0.0-20260130223332-c6b090edb9ea/go.mod h1:T/ePBgccAF4mH9BNZPbGPiycoOB8CCvFlQYw9sds1fU= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= @@ -232,8 +234,8 @@ github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9 github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= -golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=