Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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
johnstcn merged 20 commits intomainfromcj/exp/mcp
Mar 31, 2025
Merged
Show file tree
Hide file tree
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
johnstcnMar 21, 2025
7759c86
skip exp mcp test on non-linux
johnstcnMar 28, 2025
2f31322
improve tool descriptions
johnstcnMar 28, 2025
41d0b35
reduce test flakeihood
johnstcnMar 28, 2025
de2ba8b
update mcp-go -> v0.17.0
johnstcnMar 28, 2025
5d1e9e7
remove exec command filtering
johnstcnMar 28, 2025
7897e67
merge stop and start command into one
johnstcnMar 28, 2025
3d810c0
return timings in coder_workspace_exec
johnstcnMar 28, 2025
035ab2c
improve argument handling by abusing json.Marshal/Unmarshal
johnstcnMar 28, 2025
9086555
typo
johnstcnMar 28, 2025
86fdd92
feat(cli): add in exp mcp configure commands from kyle/tasks
johnstcnMar 31, 2025
e4e7ecc
chore(mcp): return StdioServer directly instead of return an io.Closer
johnstcnMar 31, 2025
f805f3a
chore(kyleosophy): sometimes the right abstraction is no abstraction
johnstcnMar 31, 2025
efedab0
chore(cli/clitest): nicer diff
johnstcnMar 31, 2025
40422c1
add missing handlers
johnstcnMar 31, 2025
f83b0ed
fix mcp server invocation cmd
johnstcnMar 31, 2025
55130f7
keep mcp stuff together
johnstcnMar 31, 2025
a8e908a
actually print the diff
johnstcnMar 31, 2025
364ee2f
fix golden file diff
johnstcnMar 31, 2025
883042d
please be fixed
johnstcnMar 31, 2025
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
8 changes: 3 additions & 5 deletionscli/clitest/golden.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,7 +11,9 @@ import (
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/cli/config"
Expand DownExpand Up@@ -117,11 +119,7 @@ func TestGoldenFile(t *testing.T, fileName string, actual []byte, replacements m
require.NoError(t, err, "read golden file, run \"make gen/golden-files\" and commit the changes")

expected = normalizeGoldenFile(t, expected)
require.Equal(
t, string(expected), string(actual),
"golden file mismatch: %s, run \"make gen/golden-files\", verify and commit the changes",
goldenPath,
)
assert.Empty(t, cmp.Diff(string(expected), string(actual)), "golden file mismatch (-want +got): %s, run \"make gen/golden-files\", verify and commit the changes", goldenPath)
}

// normalizeGoldenFile replaces any strings that are system or timing dependent
Expand Down
1 change: 1 addition & 0 deletionscli/exp.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ func (r *RootCmd) expCmd() *serpent.Command {
Children: []*serpent.Command{
r.scaletestCmd(),
r.errorExample(),
r.mcpCommand(),
r.promptExample(),
r.rptyCommand(),
},
Expand Down
284 changes: 284 additions & 0 deletionscli/exp_mcp.go
View file
Open in desktop
Original file line numberDiff line numberDiff 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 {
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
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp