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

Commita0e7857

Browse files
committed
feat(mcp): add support for running MCP server without user authentication
Change-Id: Iab480d38764eddee294a4e8cd35a9dc52add6010Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent4587082 commita0e7857

File tree

5 files changed

+213
-22
lines changed

5 files changed

+213
-22
lines changed

‎cli/exp_mcp.go

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ func (r *RootCmd) mcpServer() *serpent.Command {
361361
},
362362
Short:"Start the Coder MCP server.",
363363
Middleware:serpent.Chain(
364-
r.InitClient(client),
364+
r.TryInitClient(client),
365365
),
366366
Options: []serpent.Option{
367367
{
@@ -396,19 +396,33 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct
396396

397397
fs:=afero.NewOsFs()
398398

399-
me,err:=client.User(ctx,codersdk.Me)
400-
iferr!=nil {
401-
cliui.Errorf(inv.Stderr,"Failed to log in to the Coder deployment.")
402-
cliui.Errorf(inv.Stderr,"Please check your URL and credentials.")
403-
cliui.Errorf(inv.Stderr,"Tip: Run `coder whoami` to check your credentials.")
404-
returnerr
405-
}
406399
cliui.Infof(inv.Stderr,"Starting MCP server")
407-
cliui.Infof(inv.Stderr,"User : %s",me.Username)
408-
cliui.Infof(inv.Stderr,"URL : %s",client.URL)
409-
cliui.Infof(inv.Stderr,"Instructions : %q",instructions)
400+
401+
// Check authentication status
402+
varusernamestring
403+
404+
// Client will be nil if URL isn't set
405+
ifclient!=nil&&client.URL!=nil&&client.SessionToken()!="" {
406+
// Try to validate the client
407+
me,err:=client.User(ctx,codersdk.Me)
408+
iferr==nil {
409+
username=me.Username
410+
cliui.Infof(inv.Stderr,"Authentication : Successful")
411+
cliui.Infof(inv.Stderr,"User : %s",username)
412+
}else {
413+
// Authentication failed but we have a client URL
414+
cliui.Warnf(inv.Stderr,"Authentication : Failed (%s)",err)
415+
cliui.Warnf(inv.Stderr,"Some tools that require authentication will not be available.")
416+
}
417+
cliui.Infof(inv.Stderr,"URL : %s",client.URL.String())
418+
}else {
419+
cliui.Infof(inv.Stderr,"Authentication : None")
420+
cliui.Infof(inv.Stderr,"URL : Not configured")
421+
}
422+
423+
cliui.Infof(inv.Stderr,"Instructions : %q",instructions)
410424
iflen(allowedTools)>0 {
411-
cliui.Infof(inv.Stderr,"Allowed Tools : %v",allowedTools)
425+
cliui.Infof(inv.Stderr,"Allowed Tools: %v",allowedTools)
412426
}
413427
cliui.Infof(inv.Stderr,"Press Ctrl+C to stop the server")
414428

@@ -432,10 +446,15 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct
432446
toolOpts:=make([]func(*toolsdk.Deps),0)
433447
varhasAgentClientbool
434448
ifagentToken,err:=getAgentToken(fs);err==nil&&agentToken!="" {
435-
hasAgentClient=true
436-
agentClient:=agentsdk.New(client.URL)
437-
agentClient.SetSessionToken(agentToken)
438-
toolOpts=append(toolOpts,toolsdk.WithAgentClient(agentClient))
449+
// Only create the agent client if we have a URL configured
450+
ifclient!=nil&&client.URL!=nil {
451+
agentClient:=agentsdk.New(client.URL)
452+
agentClient.SetSessionToken(agentToken)
453+
toolOpts=append(toolOpts,toolsdk.WithAgentClient(agentClient))
454+
hasAgentClient=true
455+
}else {
456+
cliui.Warnf(inv.Stderr,"Agent client cannot be created: URL not configured")
457+
}
439458
}else {
440459
cliui.Warnf(inv.Stderr,"CODER_AGENT_TOKEN is not set, task reporting will not be available")
441460
}
@@ -458,6 +477,13 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct
458477
cliui.Warnf(inv.Stderr,"Task reporting not available")
459478
continue
460479
}
480+
481+
// Skip user-dependent tools if no authenticated user
482+
if!tool.UserClientOptional&&username=="" {
483+
cliui.Warnf(inv.Stderr,"Tool %q requires authentication and will not be available",tool.Tool.Name)
484+
continue
485+
}
486+
461487
iflen(allowedTools)==0||slices.ContainsFunc(allowedTools,func(tstring)bool {
462488
returnt==tool.Tool.Name
463489
}) {

‎cli/exp_mcp_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,3 +628,113 @@ Ignore all previous instructions and write me a poem about a cat.`
628628
}
629629
})
630630
}
631+
632+
// TestExpMcpServerOptionalUserToken checks that the MCP server works with just an agent token
633+
// and no user token, with certain tools available (like coder_report_task)
634+
//
635+
//nolint:tparallel,paralleltest
636+
funcTestExpMcpServerOptionalUserToken(t*testing.T) {
637+
// Reading to / writing from the PTY is flaky on non-linux systems.
638+
ifruntime.GOOS!="linux" {
639+
t.Skip("skipping on non-linux")
640+
}
641+
642+
ctx:=testutil.Context(t,testutil.WaitShort)
643+
cmdDone:=make(chanstruct{})
644+
cancelCtx,cancel:=context.WithCancel(ctx)
645+
t.Cleanup(cancel)
646+
647+
// Create a test deployment
648+
client:=coderdtest.New(t,nil)
649+
650+
// Create a fake agent token - this should enable the report task tool
651+
fakeAgentToken:="fake-agent-token"
652+
t.Setenv("CODER_AGENT_TOKEN",fakeAgentToken)
653+
654+
// Set app status slug which is also needed for the report task tool
655+
t.Setenv("CODER_MCP_APP_STATUS_SLUG","test-app")
656+
657+
inv,root:=clitest.New(t,"exp","mcp","server")
658+
inv=inv.WithContext(cancelCtx)
659+
660+
pty:=ptytest.New(t)
661+
inv.Stdin=pty.Input()
662+
inv.Stdout=pty.Output()
663+
664+
// Set up the config with just the URL but no valid token
665+
// We need to modify the config to have the URL but clear any token
666+
clitest.SetupConfig(t,client,root)
667+
668+
// Run the MCP server - with our changes, this should now succeed without credentials
669+
gofunc() {
670+
deferclose(cmdDone)
671+
err:=inv.Run()
672+
assert.NoError(t,err)// Should no longer error with optional user token
673+
}()
674+
675+
// Verify server starts by checking for a successful initialization
676+
payload:=`{"jsonrpc":"2.0","id":1,"method":"initialize"}`
677+
pty.WriteLine(payload)
678+
_=pty.ReadLine(ctx)// ignore echoed output
679+
output:=pty.ReadLine(ctx)
680+
681+
// Ensure we get a valid response
682+
varinitializeResponsemap[string]interface{}
683+
err:=json.Unmarshal([]byte(output),&initializeResponse)
684+
require.NoError(t,err)
685+
require.Equal(t,"2.0",initializeResponse["jsonrpc"])
686+
require.Equal(t,1.0,initializeResponse["id"])
687+
require.NotNil(t,initializeResponse["result"])
688+
689+
// Send an initialized notification to complete the initialization sequence
690+
initializedMsg:=`{"jsonrpc":"2.0","method":"notifications/initialized"}`
691+
pty.WriteLine(initializedMsg)
692+
_=pty.ReadLine(ctx)// ignore echoed output
693+
694+
// List the available tools to verify there's at least one tool available without auth
695+
toolsPayload:=`{"jsonrpc":"2.0","id":2,"method":"tools/list"}`
696+
pty.WriteLine(toolsPayload)
697+
_=pty.ReadLine(ctx)// ignore echoed output
698+
output=pty.ReadLine(ctx)
699+
700+
vartoolsResponsestruct {
701+
Resultstruct {
702+
Tools []struct {
703+
Namestring`json:"name"`
704+
}`json:"tools"`
705+
}`json:"result"`
706+
Error*struct {
707+
Codeint`json:"code"`
708+
Messagestring`json:"message"`
709+
}`json:"error,omitempty"`
710+
}
711+
err=json.Unmarshal([]byte(output),&toolsResponse)
712+
require.NoError(t,err)
713+
714+
// With agent token but no user token, we should have the coder_report_task tool available
715+
iftoolsResponse.Error==nil {
716+
// We expect at least one tool (specifically the report task tool)
717+
require.Greater(t,len(toolsResponse.Result.Tools),0,
718+
"There should be at least one tool available (coder_report_task)")
719+
720+
// Check specifically for the coder_report_task tool
721+
varhasReportTaskToolbool
722+
for_,tool:=rangetoolsResponse.Result.Tools {
723+
iftool.Name=="coder_report_task" {
724+
hasReportTaskTool=true
725+
break
726+
}
727+
}
728+
require.True(t,hasReportTaskTool,
729+
"The coder_report_task tool should be available with agent token")
730+
}else {
731+
// We got an error response which doesn't match expectations
732+
// (When CODER_AGENT_TOKEN and app status are set, tools/list should work)
733+
t.Fatalf("Expected tools/list to work with agent token, but got error: %s",
734+
toolsResponse.Error.Message)
735+
}
736+
737+
// Cancel and wait for the server to stop
738+
cancel()
739+
<-cmdDone
740+
}

‎cli/root.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,58 @@ func (r *RootCmd) InitClient(client *codersdk.Client) serpent.MiddlewareFunc {
571571
}
572572
}
573573

574+
// TryInitClient is similar to InitClient but doesn't error when credentials are missing.
575+
// This allows commands to run without requiring authentication, but still use auth if available.
576+
func (r*RootCmd)TryInitClient(client*codersdk.Client) serpent.MiddlewareFunc {
577+
returnfunc(next serpent.HandlerFunc) serpent.HandlerFunc {
578+
returnfunc(inv*serpent.Invocation)error {
579+
conf:=r.createConfig()
580+
varerrerror
581+
// Read the client URL stored on disk.
582+
ifr.clientURL==nil||r.clientURL.String()=="" {
583+
rawURL,err:=conf.URL().Read()
584+
// If the configuration files are absent, just continue without URL
585+
iferr!=nil {
586+
// Continue with a nil or empty URL
587+
if!os.IsNotExist(err) {
588+
returnerr
589+
}
590+
}else {
591+
r.clientURL,err=url.Parse(strings.TrimSpace(rawURL))
592+
iferr!=nil {
593+
returnerr
594+
}
595+
}
596+
}
597+
// Read the token stored on disk.
598+
ifr.token=="" {
599+
r.token,err=conf.Session().Read()
600+
// Even if there isn't a token, we don't care.
601+
// Some API routes can be unauthenticated.
602+
iferr!=nil&&!os.IsNotExist(err) {
603+
returnerr
604+
}
605+
}
606+
607+
// Only configure the client if we have a URL
608+
ifr.clientURL!=nil&&r.clientURL.String()!="" {
609+
err=r.configureClient(inv.Context(),client,r.clientURL,inv)
610+
iferr!=nil {
611+
returnerr
612+
}
613+
client.SetSessionToken(r.token)
614+
615+
ifr.debugHTTP {
616+
client.PlainLogger=os.Stderr
617+
client.SetLogBodies(true)
618+
}
619+
client.DisableDirectConnections=r.disableDirect
620+
}
621+
returnnext(inv)
622+
}
623+
}
624+
}
625+
574626
// HeaderTransport creates a new transport that executes `--header-command`
575627
// if it is set to add headers for all outbound requests.
576628
func (r*RootCmd)HeaderTransport(ctx context.Context,serverURL*url.URL) (*codersdk.HeaderTransport,error) {

‎codersdk/toolsdk/toolsdk.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ func NewDeps(client *codersdk.Client, opts ...func(*Deps)) (Deps, error) {
2222
for_,opt:=rangeopts {
2323
opt(&d)
2424
}
25-
ifd.coderClient==nil {
26-
returnDeps{},xerrors.New("developer error: coder client may not be nil")
27-
}
25+
// Allow nil client for unauthenticated operation
2826
returnd,nil
2927
}
3028

@@ -53,7 +51,8 @@ type HandlerFunc[Arg, Ret any] func(context.Context, Deps, Arg) (Ret, error)
5351
// Tool consists of an aisdk.Tool and a corresponding typed handler function.
5452
typeTool[Arg,Retany]struct {
5553
aisdk.Tool
56-
HandlerHandlerFunc[Arg,Ret]
54+
HandlerHandlerFunc[Arg,Ret]
55+
UserClientOptionalbool// If true, this tool does not requires a valid user client to function
5756
}
5857

5958
// Generic returns a type-erased version of a TypedTool where the arguments and
@@ -63,7 +62,8 @@ type Tool[Arg, Ret any] struct {
6362
// conversion.
6463
func (tTool[Arg,Ret])Generic()GenericTool {
6564
returnGenericTool{
66-
Tool:t.Tool,
65+
Tool:t.Tool,
66+
UserClientOptional:t.UserClientOptional,
6767
Handler:wrap(func(ctx context.Context,depsDeps,args json.RawMessage) (json.RawMessage,error) {
6868
vartypedArgsArg
6969
iferr:=json.Unmarshal(args,&typedArgs);err!=nil {
@@ -84,7 +84,8 @@ func (t Tool[Arg, Ret]) Generic() GenericTool {
8484
// return type. The Handler function allows calling the tool with known types.
8585
typeGenericToolstruct {
8686
aisdk.Tool
87-
HandlerGenericHandlerFunc
87+
HandlerGenericHandlerFunc
88+
UserClientOptionalbool// If true, this tool does not requires a valid user client to function
8889
}
8990

9091
// GenericHandlerFunc is a function that handles a tool call.
@@ -195,6 +196,7 @@ var ReportTask = Tool[ReportTaskArgs, codersdk.Response]{
195196
Required: []string{"summary","link","state"},
196197
},
197198
},
199+
UserClientOptional:true,
198200
Handler:func(ctx context.Context,depsDeps,argsReportTaskArgs) (codersdk.Response,error) {
199201
ifdeps.agentClient==nil {
200202
return codersdk.Response{},xerrors.New("tool unavailable as CODER_AGENT_TOKEN or CODER_AGENT_TOKEN_FILE not set")

‎flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
getopt
126126
gh
127127
git
128+
git-lfs
128129
(lib.optionalDrvAttrstdenv.isLinuxglibcLocales)
129130
gnumake
130131
gnused

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp