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

Commit9127d62

Browse files
committed
add cli exp mcp command
1 parent8445d28 commit9127d62

File tree

6 files changed

+188
-17
lines changed

6 files changed

+188
-17
lines changed

‎cli/exp.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func (r *RootCmd) expCmd() *serpent.Command {
1313
Children: []*serpent.Command{
1414
r.scaletestCmd(),
1515
r.errorExample(),
16+
r.mcpCommand(),
1617
r.promptExample(),
1718
r.rptyCommand(),
1819
},

‎cli/exp_mcp.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"cdr.dev/slog"
8+
"cdr.dev/slog/sloggers/sloghuman"
9+
"github.com/coder/coder/v2/cli/cliui"
10+
"github.com/coder/coder/v2/codersdk"
11+
codermcp"github.com/coder/coder/v2/mcp"
12+
"github.com/coder/serpent"
13+
)
14+
15+
func (r*RootCmd)mcpCommand()*serpent.Command {
16+
var (
17+
client=new(codersdk.Client)
18+
instructionsstring
19+
)
20+
return&serpent.Command{
21+
Use:"mcp",
22+
Handler:func(inv*serpent.Invocation)error {
23+
returnmcpHandler(inv,client,instructions)
24+
},
25+
Short:"Start an MCP server that can be used to interact with a Coder depoyment.",
26+
Middleware:serpent.Chain(
27+
r.InitClient(client),
28+
),
29+
Options: []serpent.Option{
30+
{
31+
Name:"instructions",
32+
Description:"The instructions to pass to the MCP server.",
33+
Flag:"instructions",
34+
Value:serpent.StringOf(&instructions),
35+
},
36+
},
37+
}
38+
}
39+
40+
funcmcpHandler(inv*serpent.Invocation,client*codersdk.Client,instructionsstring)error {
41+
ctx,cancel:=context.WithCancel(inv.Context())
42+
defercancel()
43+
44+
logger:=slog.Make(sloghuman.Sink(inv.Stdout))
45+
46+
me,err:=client.User(ctx,codersdk.Me)
47+
iferr!=nil {
48+
cliui.Errorf(inv.Stderr,"Failed to log in to the Coder deployment.")
49+
cliui.Errorf(inv.Stderr,"Please check your URL and credentials.")
50+
cliui.Errorf(inv.Stderr,"Tip: Run `coder whoami` to check your credentials.")
51+
returnerr
52+
}
53+
cliui.Infof(inv.Stderr,"Starting MCP server")
54+
cliui.Infof(inv.Stderr,"User : %s",me.Username)
55+
cliui.Infof(inv.Stderr,"URL : %s",client.URL)
56+
cliui.Infof(inv.Stderr,"Instructions : %q",instructions)
57+
cliui.Infof(inv.Stderr,"Press Ctrl+C to stop the server")
58+
59+
// Capture the original stdin, stdout, and stderr.
60+
invStdin:=inv.Stdin
61+
invStdout:=inv.Stdout
62+
invStderr:=inv.Stderr
63+
deferfunc() {
64+
inv.Stdin=invStdin
65+
inv.Stdout=invStdout
66+
inv.Stderr=invStderr
67+
}()
68+
69+
closer:=codermcp.New(ctx,client,
70+
codermcp.WithInstructions(instructions),
71+
codermcp.WithLogger(&logger),
72+
codermcp.WithStdin(invStdin),
73+
codermcp.WithStdout(invStdout),
74+
)
75+
76+
<-ctx.Done()
77+
iferr:=closer.Close();err!=nil {
78+
if!errors.Is(err,context.Canceled) {
79+
cliui.Errorf(inv.Stderr,"Failed to stop the MCP server: %s",err)
80+
returnerr
81+
}
82+
}
83+
returnnil
84+
}

‎cli/exp_mcp_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package cli_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/coder/coder/v2/cli/clitest"
12+
"github.com/coder/coder/v2/coderd/coderdtest"
13+
"github.com/coder/coder/v2/pty/ptytest"
14+
"github.com/coder/coder/v2/testutil"
15+
)
16+
17+
funcTestExpMcp(t*testing.T) {
18+
t.Parallel()
19+
20+
t.Run("OK",func(t*testing.T) {
21+
t.Parallel()
22+
23+
ctx:=testutil.Context(t,testutil.WaitShort)
24+
cancelCtx,cancel:=context.WithCancel(ctx)
25+
t.Cleanup(cancel)
26+
27+
client:=coderdtest.New(t,nil)
28+
_=coderdtest.CreateFirstUser(t,client)
29+
inv,root:=clitest.New(t,"exp","mcp")
30+
inv=inv.WithContext(cancelCtx)
31+
32+
pty:=ptytest.New(t)
33+
inv.Stdin=pty.Input()
34+
inv.Stdout=pty.Output()
35+
clitest.SetupConfig(t,client,root)
36+
37+
cmdDone:=tGo(t,func() {
38+
err:=inv.Run()
39+
assert.NoError(t,err)
40+
})
41+
42+
payload:=`{"jsonrpc":"2.0","id":1,"method":"initialize"}`
43+
pty.WriteLine(payload)
44+
_=pty.ReadLine(ctx)// ignore echoed output
45+
output:=pty.ReadLine(ctx)
46+
cancel()
47+
<-cmdDone
48+
49+
// Ensure the initialize output is valid JSON
50+
t.Logf("/initialize output: %s",output)
51+
varinitializeResponsemap[string]interface{}
52+
err:=json.Unmarshal([]byte(output),&initializeResponse)
53+
require.NoError(t,err)
54+
require.Equal(t,"2.0",initializeResponse["jsonrpc"])
55+
require.Equal(t,1.0,initializeResponse["id"])
56+
require.NotNil(t,initializeResponse["result"])
57+
})
58+
59+
t.Run("NoCredentials",func(t*testing.T) {
60+
t.Parallel()
61+
62+
ctx:=testutil.Context(t,testutil.WaitShort)
63+
cancelCtx,cancel:=context.WithCancel(ctx)
64+
t.Cleanup(cancel)
65+
66+
client:=coderdtest.New(t,nil)
67+
inv,root:=clitest.New(t,"exp","mcp")
68+
inv=inv.WithContext(cancelCtx)
69+
70+
pty:=ptytest.New(t)
71+
inv.Stdin=pty.Input()
72+
inv.Stdout=pty.Output()
73+
clitest.SetupConfig(t,client,root)
74+
75+
err:=inv.Run()
76+
assert.ErrorContains(t,err,"your session has expired")
77+
})
78+
}

‎mcp/mcp.go

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,41 +21,40 @@ type mcpOptions struct {
2121
out io.Writer
2222
instructionsstring
2323
logger*slog.Logger
24-
client*codersdk.Client
2524
}
2625

26+
// Option is a function that configures the MCP server.
2727
typeOptionfunc(*mcpOptions)
2828

29+
// WithInstructions sets the instructions for the MCP server.
2930
funcWithInstructions(instructionsstring)Option {
3031
returnfunc(o*mcpOptions) {
3132
o.instructions=instructions
3233
}
3334
}
3435

36+
// WithLogger sets the logger for the MCP server.
3537
funcWithLogger(logger*slog.Logger)Option {
3638
returnfunc(o*mcpOptions) {
3739
o.logger=logger
3840
}
3941
}
4042

43+
// WithStdin sets the input reader for the MCP server.
4144
funcWithStdin(in io.Reader)Option {
4245
returnfunc(o*mcpOptions) {
4346
o.in=in
4447
}
4548
}
4649

50+
// WithStdout sets the output writer for the MCP server.
4751
funcWithStdout(out io.Writer)Option {
4852
returnfunc(o*mcpOptions) {
4953
o.out=out
5054
}
5155
}
5256

53-
funcWithClient(client*codersdk.Client)Option {
54-
returnfunc(o*mcpOptions) {
55-
o.client=client
56-
}
57-
}
58-
57+
// New creates a new MCP server with the given client and options.
5958
funcNew(ctx context.Context,client*codersdk.Client,opts...Option) io.Closer {
6059
options:=&mcpOptions{
6160
in:os.Stdin,
@@ -75,10 +74,10 @@ func New(ctx context.Context, client *codersdk.Client, opts ...Option) io.Closer
7574

7675
logger:=slog.Make(sloghuman.Sink(os.Stdout))
7776

78-
mcptools.RegisterCoderReportTask(mcpSrv,options.client,logger)
79-
mcptools.RegisterCoderWhoami(mcpSrv,options.client)
80-
mcptools.RegisterCoderListWorkspaces(mcpSrv,options.client)
81-
mcptools.RegisterCoderWorkspaceExec(mcpSrv,options.client)
77+
mcptools.RegisterCoderReportTask(mcpSrv,client,logger)
78+
mcptools.RegisterCoderWhoami(mcpSrv,client)
79+
mcptools.RegisterCoderListWorkspaces(mcpSrv,client)
80+
mcptools.RegisterCoderWorkspaceExec(mcpSrv,client)
8281

8382
srv:=server.NewStdioServer(mcpSrv)
8483
srv.SetErrorLogger(log.New(options.out,"",log.LstdFlags))

‎mcp/tools/tools_coder.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,12 @@ Good Summaries:
7171

7272
// Example payload:
7373
// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_report_task", "arguments": {"summary": "I'm working on the login page.", "link": "https://github.com/coder/coder/pull/1234", "emoji": "🔍", "done": false, "coder_url": "http://localhost:3000", "coder_session_token": "REDACTED"}}}
74-
funchandleCoderReportTask(log slog.Logger,_*codersdk.Client) mcpserver.ToolHandlerFunc {
74+
funchandleCoderReportTask(log slog.Logger,client*codersdk.Client) mcpserver.ToolHandlerFunc {
7575
returnfunc(ctx context.Context,request mcp.CallToolRequest) (*mcp.CallToolResult,error) {
76+
ifclient==nil {
77+
returnnil,xerrors.New("developer error: client is required")
78+
}
79+
7680
args:=request.Params.Arguments
7781

7882
summary,ok:=args["summary"].(string)
@@ -122,6 +126,9 @@ func handleCoderReportTask(log slog.Logger, _ *codersdk.Client) mcpserver.ToolHa
122126
// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_whoami", "arguments": {"coder_url": "http://localhost:3000", "coder_session_token": "REDACTED"}}}
123127
funchandleCoderWhoami(client*codersdk.Client) mcpserver.ToolHandlerFunc {
124128
returnfunc(ctx context.Context,_ mcp.CallToolRequest) (*mcp.CallToolResult,error) {
129+
ifclient==nil {
130+
returnnil,xerrors.New("developer error: client is required")
131+
}
125132
me,err:=client.User(ctx,codersdk.Me)
126133
iferr!=nil {
127134
returnnil,xerrors.Errorf("Failed to fetch the current user: %s",err.Error())
@@ -144,6 +151,9 @@ func handleCoderWhoami(client *codersdk.Client) mcpserver.ToolHandlerFunc {
144151
// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_list_workspaces", "arguments": {"owner": "me", "offset": 0, "limit": 10, "coder_url": "http://localhost:3000", "coder_session_token": "REDACTED"}}}
145152
funchandleCoderListWorkspaces(client*codersdk.Client) mcpserver.ToolHandlerFunc {
146153
returnfunc(ctx context.Context,request mcp.CallToolRequest) (*mcp.CallToolResult,error) {
154+
ifclient==nil {
155+
returnnil,xerrors.New("developer error: client is required")
156+
}
147157
args:=request.Params.Arguments
148158

149159
owner,ok:=args["owner"].(string)
@@ -187,6 +197,9 @@ func handleCoderListWorkspaces(client *codersdk.Client) mcpserver.ToolHandlerFun
187197
// {"jsonrpc":"2.0","id":1,"method":"tools/call", "params": {"name": "coder_workspace_exec", "arguments": {"workspace": "dev", "command": "ps -ef", "coder_url": "http://localhost:3000", "coder_session_token": "REDACTED"}}}
188198
funchandleCoderWorkspaceExec(client*codersdk.Client) mcpserver.ToolHandlerFunc {
189199
returnfunc(ctx context.Context,request mcp.CallToolRequest) (*mcp.CallToolResult,error) {
200+
ifclient==nil {
201+
returnnil,xerrors.New("developer error: client is required")
202+
}
190203
args:=request.Params.Arguments
191204

192205
wsArg,ok:=args["workspace"].(string)

‎mcp/tools/tools_coder_test.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323

2424
// These tests are dependent on the state of the coder server.
2525
// Running them in parallel is prone to racy behavior.
26-
// nolint:tparallel
26+
// nolint:tparallel,paralleltest
2727
funcTestCoderTools(t*testing.T) {
2828
t.Parallel()
2929

@@ -60,7 +60,6 @@ func TestCoderTools(t *testing.T) {
6060
mcptools.RegisterCoderWorkspaceExec(mcpSrv,memberClient)
6161

6262
t.Run("coder_report_task",func(t*testing.T) {
63-
// nolint:tparallel
6463
// When: the coder_report_task tool is called
6564
ctr:=makeJSONRPCRequest(t,"tools/call","coder_report_task",map[string]any{
6665
"summary":"Test summary",
@@ -82,7 +81,6 @@ func TestCoderTools(t *testing.T) {
8281
})
8382

8483
t.Run("coder_whoami",func(t*testing.T) {
85-
// nolint:tparallel
8684
// When: the coder_whoami tool is called
8785
me,err:=memberClient.User(ctx,codersdk.Me)
8886
require.NoError(t,err)
@@ -101,7 +99,6 @@ func TestCoderTools(t *testing.T) {
10199
})
102100

103101
t.Run("coder_list_workspaces",func(t*testing.T) {
104-
// nolint:tparallel
105102
// When: the coder_list_workspaces tool is called
106103
ctr:=makeJSONRPCRequest(t,"tools/call","coder_list_workspaces",map[string]any{
107104
"coder_url":client.URL.String(),
@@ -123,7 +120,6 @@ func TestCoderTools(t *testing.T) {
123120
})
124121

125122
t.Run("coder_workspace_exec",func(t*testing.T) {
126-
// nolint:tparallel
127123
// When: the coder_workspace_exec tools is called with a command
128124
randString:=testutil.GetRandomName(t)
129125
ctr:=makeJSONRPCRequest(t,"tools/call","coder_workspace_exec",map[string]any{

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp