- Notifications
You must be signed in to change notification settings - Fork906
feat(cli): addcoder exp mcp
command#17066
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Merged
Uh oh!
There was an error while loading.Please reload this page.
Merged
Changes fromall commits
Commits
Show all changes
20 commits Select commitHold shift + click to select a range
c034b6f
feat(cli): add experimental MCP server command
johnstcn7759c86
skip exp mcp test on non-linux
johnstcn2f31322
improve tool descriptions
johnstcn41d0b35
reduce test flakeihood
johnstcnde2ba8b
update mcp-go -> v0.17.0
johnstcn5d1e9e7
remove exec command filtering
johnstcn7897e67
merge stop and start command into one
johnstcn3d810c0
return timings in coder_workspace_exec
johnstcn035ab2c
improve argument handling by abusing json.Marshal/Unmarshal
johnstcn9086555
typo
johnstcn86fdd92
feat(cli): add in exp mcp configure commands from kyle/tasks
johnstcne4e7ecc
chore(mcp): return StdioServer directly instead of return an io.Closer
johnstcnf805f3a
chore(kyleosophy): sometimes the right abstraction is no abstraction
johnstcnefedab0
chore(cli/clitest): nicer diff
johnstcn40422c1
add missing handlers
johnstcnf83b0ed
fix mcp server invocation cmd
johnstcn55130f7
keep mcp stuff together
johnstcna8e908a
actually print the diff
johnstcn364ee2f
fix golden file diff
johnstcn883042d
please be fixed
johnstcnFile filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
8 changes: 3 additions & 5 deletionscli/clitest/golden.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletionscli/exp.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
284 changes: 284 additions & 0 deletionscli/exp_mcp.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,284 @@ | ||
package cli | ||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"log" | ||
"os" | ||
"path/filepath" | ||
"cdr.dev/slog" | ||
"cdr.dev/slog/sloggers/sloghuman" | ||
"github.com/coder/coder/v2/cli/cliui" | ||
"github.com/coder/coder/v2/codersdk" | ||
codermcp "github.com/coder/coder/v2/mcp" | ||
"github.com/coder/serpent" | ||
) | ||
func (r *RootCmd) mcpCommand() *serpent.Command { | ||
johnstcn marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
cmd := &serpent.Command{ | ||
Use: "mcp", | ||
Short: "Run the Coder MCP server and configure it to work with AI tools.", | ||
Long: "The Coder MCP server allows you to automatically create workspaces with parameters.", | ||
Handler: func(i *serpent.Invocation) error { | ||
return i.Command.HelpHandler(i) | ||
}, | ||
Children: []*serpent.Command{ | ||
r.mcpConfigure(), | ||
r.mcpServer(), | ||
}, | ||
} | ||
return cmd | ||
} | ||
func (r *RootCmd) mcpConfigure() *serpent.Command { | ||
cmd := &serpent.Command{ | ||
Use: "configure", | ||
Short: "Automatically configure the MCP server.", | ||
Handler: func(i *serpent.Invocation) error { | ||
return i.Command.HelpHandler(i) | ||
}, | ||
Children: []*serpent.Command{ | ||
r.mcpConfigureClaudeDesktop(), | ||
r.mcpConfigureClaudeCode(), | ||
r.mcpConfigureCursor(), | ||
}, | ||
} | ||
return cmd | ||
} | ||
func (*RootCmd) mcpConfigureClaudeDesktop() *serpent.Command { | ||
cmd := &serpent.Command{ | ||
Use: "claude-desktop", | ||
Short: "Configure the Claude Desktop server.", | ||
Handler: func(_ *serpent.Invocation) error { | ||
configPath, err := os.UserConfigDir() | ||
if err != nil { | ||
return err | ||
} | ||
configPath = filepath.Join(configPath, "Claude") | ||
err = os.MkdirAll(configPath, 0o755) | ||
if err != nil { | ||
return err | ||
} | ||
configPath = filepath.Join(configPath, "claude_desktop_config.json") | ||
_, err = os.Stat(configPath) | ||
if err != nil { | ||
if !os.IsNotExist(err) { | ||
return err | ||
} | ||
} | ||
contents := map[string]any{} | ||
data, err := os.ReadFile(configPath) | ||
if err != nil { | ||
if !os.IsNotExist(err) { | ||
return err | ||
} | ||
} else { | ||
err = json.Unmarshal(data, &contents) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
binPath, err := os.Executable() | ||
if err != nil { | ||
return err | ||
} | ||
contents["mcpServers"] = map[string]any{ | ||
"coder": map[string]any{"command": binPath, "args": []string{"exp", "mcp", "server"}}, | ||
} | ||
data, err = json.MarshalIndent(contents, "", " ") | ||
if err != nil { | ||
return err | ||
} | ||
err = os.WriteFile(configPath, data, 0o600) | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
}, | ||
} | ||
return cmd | ||
} | ||
func (*RootCmd) mcpConfigureClaudeCode() *serpent.Command { | ||
cmd := &serpent.Command{ | ||
Use: "claude-code", | ||
Short: "Configure the Claude Code server.", | ||
Handler: func(_ *serpent.Invocation) error { | ||
return nil | ||
}, | ||
} | ||
return cmd | ||
} | ||
func (*RootCmd) mcpConfigureCursor() *serpent.Command { | ||
var project bool | ||
cmd := &serpent.Command{ | ||
Use: "cursor", | ||
Short: "Configure Cursor to use Coder MCP.", | ||
Options: serpent.OptionSet{ | ||
serpent.Option{ | ||
Flag: "project", | ||
Env: "CODER_MCP_CURSOR_PROJECT", | ||
Description: "Use to configure a local project to use the Cursor MCP.", | ||
Value: serpent.BoolOf(&project), | ||
}, | ||
}, | ||
Handler: func(_ *serpent.Invocation) error { | ||
dir, err := os.Getwd() | ||
if err != nil { | ||
return err | ||
} | ||
if !project { | ||
dir, err = os.UserHomeDir() | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
cursorDir := filepath.Join(dir, ".cursor") | ||
err = os.MkdirAll(cursorDir, 0o755) | ||
if err != nil { | ||
return err | ||
} | ||
mcpConfig := filepath.Join(cursorDir, "mcp.json") | ||
_, err = os.Stat(mcpConfig) | ||
contents := map[string]any{} | ||
if err != nil { | ||
if !os.IsNotExist(err) { | ||
return err | ||
} | ||
} else { | ||
data, err := os.ReadFile(mcpConfig) | ||
if err != nil { | ||
return err | ||
} | ||
// The config can be empty, so we don't want to return an error if it is. | ||
if len(data) > 0 { | ||
err = json.Unmarshal(data, &contents) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
mcpServers, ok := contents["mcpServers"].(map[string]any) | ||
if !ok { | ||
mcpServers = map[string]any{} | ||
} | ||
binPath, err := os.Executable() | ||
if err != nil { | ||
return err | ||
} | ||
mcpServers["coder"] = map[string]any{ | ||
"command": binPath, | ||
"args": []string{"exp", "mcp", "server"}, | ||
} | ||
contents["mcpServers"] = mcpServers | ||
data, err := json.MarshalIndent(contents, "", " ") | ||
if err != nil { | ||
return err | ||
} | ||
err = os.WriteFile(mcpConfig, data, 0o600) | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
}, | ||
} | ||
return cmd | ||
} | ||
func (r *RootCmd) mcpServer() *serpent.Command { | ||
var ( | ||
client = new(codersdk.Client) | ||
instructions string | ||
allowedTools []string | ||
) | ||
return &serpent.Command{ | ||
Use: "server", | ||
Handler: func(inv *serpent.Invocation) error { | ||
return mcpServerHandler(inv, client, instructions, allowedTools) | ||
}, | ||
Short: "Start the Coder MCP server.", | ||
Middleware: serpent.Chain( | ||
r.InitClient(client), | ||
), | ||
Options: []serpent.Option{ | ||
{ | ||
Name: "instructions", | ||
Description: "The instructions to pass to the MCP server.", | ||
Flag: "instructions", | ||
Value: serpent.StringOf(&instructions), | ||
}, | ||
{ | ||
Name: "allowed-tools", | ||
Description: "Comma-separated list of allowed tools. If not specified, all tools are allowed.", | ||
Flag: "allowed-tools", | ||
Value: serpent.StringArrayOf(&allowedTools), | ||
}, | ||
}, | ||
} | ||
} | ||
func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instructions string, allowedTools []string) error { | ||
ctx, cancel := context.WithCancel(inv.Context()) | ||
defer cancel() | ||
logger := slog.Make(sloghuman.Sink(inv.Stdout)) | ||
me, err := client.User(ctx, codersdk.Me) | ||
if err != nil { | ||
cliui.Errorf(inv.Stderr, "Failed to log in to the Coder deployment.") | ||
cliui.Errorf(inv.Stderr, "Please check your URL and credentials.") | ||
cliui.Errorf(inv.Stderr, "Tip: Run `coder whoami` to check your credentials.") | ||
return err | ||
} | ||
cliui.Infof(inv.Stderr, "Starting MCP server") | ||
cliui.Infof(inv.Stderr, "User : %s", me.Username) | ||
cliui.Infof(inv.Stderr, "URL : %s", client.URL) | ||
cliui.Infof(inv.Stderr, "Instructions : %q", instructions) | ||
if len(allowedTools) > 0 { | ||
cliui.Infof(inv.Stderr, "Allowed Tools : %v", allowedTools) | ||
} | ||
cliui.Infof(inv.Stderr, "Press Ctrl+C to stop the server") | ||
// Capture the original stdin, stdout, and stderr. | ||
invStdin := inv.Stdin | ||
invStdout := inv.Stdout | ||
invStderr := inv.Stderr | ||
defer func() { | ||
inv.Stdin = invStdin | ||
inv.Stdout = invStdout | ||
inv.Stderr = invStderr | ||
}() | ||
options := []codermcp.Option{ | ||
codermcp.WithInstructions(instructions), | ||
codermcp.WithLogger(&logger), | ||
} | ||
// Add allowed tools option if specified | ||
if len(allowedTools) > 0 { | ||
options = append(options, codermcp.WithAllowedTools(allowedTools)) | ||
} | ||
srv := codermcp.NewStdio(client, options...) | ||
srv.SetErrorLogger(log.New(invStderr, "", log.LstdFlags)) | ||
done := make(chan error) | ||
go func() { | ||
defer close(done) | ||
srvErr := srv.Listen(ctx, invStdin, invStdout) | ||
done <- srvErr | ||
}() | ||
if err := <-done; err != nil { | ||
if !errors.Is(err, context.Canceled) { | ||
cliui.Errorf(inv.Stderr, "Failed to start the MCP server: %s", err) | ||
return err | ||
} | ||
} | ||
return nil | ||
} |
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
Oops, something went wrong.
Uh oh!
There was an error while loading.Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.