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

Commit19a504c

Browse files
committed
feat(cli): add experimental MCP server command
1 parentc679991 commit19a504c

File tree

12 files changed

+1441
-1
lines changed

12 files changed

+1441
-1
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: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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+
allowedTools []string
20+
allowedExecCommands []string
21+
)
22+
return&serpent.Command{
23+
Use:"mcp",
24+
Handler:func(inv*serpent.Invocation)error {
25+
returnmcpHandler(inv,client,instructions,allowedTools,allowedExecCommands)
26+
},
27+
Short:"Start an MCP server that can be used to interact with a Coder depoyment.",
28+
Middleware:serpent.Chain(
29+
r.InitClient(client),
30+
),
31+
Options: []serpent.Option{
32+
{
33+
Name:"instructions",
34+
Description:"The instructions to pass to the MCP server.",
35+
Flag:"instructions",
36+
Value:serpent.StringOf(&instructions),
37+
},
38+
{
39+
Name:"allowed-tools",
40+
Description:"Comma-separated list of allowed tools. If not specified, all tools are allowed.",
41+
Flag:"allowed-tools",
42+
Value:serpent.StringArrayOf(&allowedTools),
43+
},
44+
{
45+
Name:"allowed-exec-commands",
46+
Description:"Comma-separated list of allowed commands for workspace execution. If not specified, all commands are allowed.",
47+
Flag:"allowed-exec-commands",
48+
Value:serpent.StringArrayOf(&allowedExecCommands),
49+
},
50+
},
51+
}
52+
}
53+
54+
funcmcpHandler(inv*serpent.Invocation,client*codersdk.Client,instructionsstring,allowedTools []string,allowedExecCommands []string)error {
55+
ctx,cancel:=context.WithCancel(inv.Context())
56+
defercancel()
57+
58+
logger:=slog.Make(sloghuman.Sink(inv.Stdout))
59+
60+
me,err:=client.User(ctx,codersdk.Me)
61+
iferr!=nil {
62+
cliui.Errorf(inv.Stderr,"Failed to log in to the Coder deployment.")
63+
cliui.Errorf(inv.Stderr,"Please check your URL and credentials.")
64+
cliui.Errorf(inv.Stderr,"Tip: Run `coder whoami` to check your credentials.")
65+
returnerr
66+
}
67+
cliui.Infof(inv.Stderr,"Starting MCP server")
68+
cliui.Infof(inv.Stderr,"User : %s",me.Username)
69+
cliui.Infof(inv.Stderr,"URL : %s",client.URL)
70+
cliui.Infof(inv.Stderr,"Instructions : %q",instructions)
71+
iflen(allowedTools)>0 {
72+
cliui.Infof(inv.Stderr,"Allowed Tools : %v",allowedTools)
73+
}
74+
iflen(allowedExecCommands)>0 {
75+
cliui.Infof(inv.Stderr,"Allowed Exec Commands : %v",allowedExecCommands)
76+
}
77+
cliui.Infof(inv.Stderr,"Press Ctrl+C to stop the server")
78+
79+
// Capture the original stdin, stdout, and stderr.
80+
invStdin:=inv.Stdin
81+
invStdout:=inv.Stdout
82+
invStderr:=inv.Stderr
83+
deferfunc() {
84+
inv.Stdin=invStdin
85+
inv.Stdout=invStdout
86+
inv.Stderr=invStderr
87+
}()
88+
89+
options:= []codermcp.Option{
90+
codermcp.WithInstructions(instructions),
91+
codermcp.WithLogger(&logger),
92+
codermcp.WithStdin(invStdin),
93+
codermcp.WithStdout(invStdout),
94+
}
95+
96+
// Add allowed tools option if specified
97+
iflen(allowedTools)>0 {
98+
options=append(options,codermcp.WithAllowedTools(allowedTools))
99+
}
100+
101+
// Add allowed exec commands option if specified
102+
iflen(allowedExecCommands)>0 {
103+
options=append(options,codermcp.WithAllowedExecCommands(allowedExecCommands))
104+
}
105+
106+
closer:=codermcp.New(ctx,client,options...)
107+
108+
<-ctx.Done()
109+
iferr:=closer.Close();err!=nil {
110+
if!errors.Is(err,context.Canceled) {
111+
cliui.Errorf(inv.Stderr,"Failed to stop the MCP server: %s",err)
112+
returnerr
113+
}
114+
}
115+
returnnil
116+
}

‎cli/exp_mcp_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package cli_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"slices"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/coder/coder/v2/cli/clitest"
13+
"github.com/coder/coder/v2/coderd/coderdtest"
14+
"github.com/coder/coder/v2/pty/ptytest"
15+
"github.com/coder/coder/v2/testutil"
16+
)
17+
18+
funcTestExpMcp(t*testing.T) {
19+
t.Parallel()
20+
21+
t.Run("AllowedTools",func(t*testing.T) {
22+
t.Parallel()
23+
24+
ctx:=testutil.Context(t,testutil.WaitShort)
25+
cancelCtx,cancel:=context.WithCancel(ctx)
26+
t.Cleanup(cancel)
27+
28+
// Given: a running coder deployment
29+
client:=coderdtest.New(t,nil)
30+
_=coderdtest.CreateFirstUser(t,client)
31+
32+
// Given: we run the exp mcp command with allowed tools set
33+
inv,root:=clitest.New(t,"exp","mcp","--allowed-tools=coder_whoami,coder_list_templates")
34+
inv=inv.WithContext(cancelCtx)
35+
36+
pty:=ptytest.New(t)
37+
inv.Stdin=pty.Input()
38+
inv.Stdout=pty.Output()
39+
clitest.SetupConfig(t,client,root)
40+
41+
cmdDone:=make(chanstruct{})
42+
gofunc() {
43+
deferclose(cmdDone)
44+
err:=inv.Run()
45+
assert.NoError(t,err)
46+
}()
47+
48+
// When: we send a tools/list request
49+
toolsPayload:=`{"jsonrpc":"2.0","id":2,"method":"tools/list"}`
50+
pty.WriteLine(toolsPayload)
51+
_=pty.ReadLine(ctx)// ignore echoed output
52+
output:=pty.ReadLine(ctx)
53+
54+
cancel()
55+
<-cmdDone
56+
57+
// Then: we should only see the allowed tools in the response
58+
vartoolsResponsestruct {
59+
Resultstruct {
60+
Tools []struct {
61+
Namestring`json:"name"`
62+
}`json:"tools"`
63+
}`json:"result"`
64+
}
65+
err:=json.Unmarshal([]byte(output),&toolsResponse)
66+
require.NoError(t,err)
67+
require.Len(t,toolsResponse.Result.Tools,2,"should have exactly 2 tools")
68+
foundTools:=make([]string,0,2)
69+
for_,tool:=rangetoolsResponse.Result.Tools {
70+
foundTools=append(foundTools,tool.Name)
71+
}
72+
slices.Sort(foundTools)
73+
require.Equal(t, []string{"coder_list_templates","coder_whoami"},foundTools)
74+
})
75+
76+
t.Run("OK",func(t*testing.T) {
77+
t.Parallel()
78+
79+
ctx:=testutil.Context(t,testutil.WaitShort)
80+
cancelCtx,cancel:=context.WithCancel(ctx)
81+
t.Cleanup(cancel)
82+
83+
client:=coderdtest.New(t,nil)
84+
_=coderdtest.CreateFirstUser(t,client)
85+
inv,root:=clitest.New(t,"exp","mcp")
86+
inv=inv.WithContext(cancelCtx)
87+
88+
pty:=ptytest.New(t)
89+
inv.Stdin=pty.Input()
90+
inv.Stdout=pty.Output()
91+
clitest.SetupConfig(t,client,root)
92+
93+
cmdDone:=make(chanstruct{})
94+
gofunc() {
95+
deferclose(cmdDone)
96+
err:=inv.Run()
97+
assert.NoError(t,err)
98+
}()
99+
100+
payload:=`{"jsonrpc":"2.0","id":1,"method":"initialize"}`
101+
pty.WriteLine(payload)
102+
_=pty.ReadLine(ctx)// ignore echoed output
103+
output:=pty.ReadLine(ctx)
104+
cancel()
105+
<-cmdDone
106+
107+
// Ensure the initialize output is valid JSON
108+
t.Logf("/initialize output: %s",output)
109+
varinitializeResponsemap[string]interface{}
110+
err:=json.Unmarshal([]byte(output),&initializeResponse)
111+
require.NoError(t,err)
112+
require.Equal(t,"2.0",initializeResponse["jsonrpc"])
113+
require.Equal(t,1.0,initializeResponse["id"])
114+
require.NotNil(t,initializeResponse["result"])
115+
})
116+
117+
t.Run("NoCredentials",func(t*testing.T) {
118+
t.Parallel()
119+
120+
ctx:=testutil.Context(t,testutil.WaitShort)
121+
cancelCtx,cancel:=context.WithCancel(ctx)
122+
t.Cleanup(cancel)
123+
124+
client:=coderdtest.New(t,nil)
125+
inv,root:=clitest.New(t,"exp","mcp")
126+
inv=inv.WithContext(cancelCtx)
127+
128+
pty:=ptytest.New(t)
129+
inv.Stdin=pty.Input()
130+
inv.Stdout=pty.Output()
131+
clitest.SetupConfig(t,client,root)
132+
133+
err:=inv.Run()
134+
assert.ErrorContains(t,err,"your session has expired")
135+
})
136+
}

‎go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ require (
320320
github.com/google/nftablesv0.2.0// indirect
321321
github.com/google/pprofv0.0.0-20230817174616-7a8ec2ada47b// indirect
322322
github.com/google/s2a-gov0.1.9// indirect
323-
github.com/google/shlexv0.0.0-20191202100458-e7afc7fbc510// indirect
323+
github.com/google/shlexv0.0.0-20191202100458-e7afc7fbc510
324324
github.com/googleapis/enterprise-certificate-proxyv0.3.6// indirect
325325
github.com/googleapis/gax-go/v2v2.14.1// indirect
326326
github.com/gorilla/cssv1.0.1// indirect
@@ -480,3 +480,7 @@ require (
480480
github.com/golang-jwt/jwt/v5v5.2.2// indirect
481481
github.com/xo/terminfov0.0.0-20220910002029-abceb7e1c41e// indirect
482482
)
483+
484+
requiregithub.com/mark3labs/mcp-gov0.15.0
485+
486+
requiregithub.com/yosida95/uritemplate/v3v3.0.2// indirect

‎go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,8 @@ github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1r
658658
github.com/makeworld-the-better-one/dither/v2v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
659659
github.com/marekm4/color-extractorv1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0=
660660
github.com/marekm4/color-extractorv1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA=
661+
github.com/mark3labs/mcp-gov0.15.0 h1:lViiC4dk6chJHZccezaTzZLMOQVUXJDGNQPtzExr5NQ=
662+
github.com/mark3labs/mcp-gov0.15.0/go.mod h1:xBB350hekQsJAK7gJAii8bcEoWemboLm2mRm5/+KBaU=
661663
github.com/mattn/go-colorablev0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
662664
github.com/mattn/go-colorablev0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
663665
github.com/mattn/go-colorablev0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -972,6 +974,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
972974
github.com/xyproto/randomstringv1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
973975
github.com/yashtewari/glob-intersectionv0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
974976
github.com/yashtewari/glob-intersectionv0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
977+
github.com/yosida95/uritemplate/v3v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
978+
github.com/yosida95/uritemplate/v3v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
975979
github.com/yudai/gojsondiffv1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
976980
github.com/yudai/gojsondiffv1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
977981
github.com/yudai/golcsv0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp