feat(cli): make MCP server work without user authentication (#17688)

Part of #17649

---

# Allow MCP server to run without authentication

This PR enhances the MCP server to operate without requiring authentication, making it more flexible for environments where authentication isn't available or necessary. Key changes:

- Replaced `InitClient` with `TryInitClient` to allow the MCP server to start without credentials
- Added graceful handling when URL or authentication is missing
- Made authentication status visible in server logs
- Added logic to skip user-dependent tools when no authenticated user is present
- Made the `coder_report_task` tool available with just an agent token (no user token required)
- Added comprehensive tests to verify operation without authentication

These changes allow the MCP server to function in more environments while still using authentication when available, improving flexibility for CI/CD and other automated environments.
This commit is contained in:
Thomas Kosiewski
2025-05-07 21:53:06 +02:00
committed by GitHub
parent 6ac1bd807c
commit 29bce8d9e6
5 changed files with 253 additions and 23 deletions

View File

@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"errors"
"net/url"
"os"
"path/filepath"
"slices"
@ -361,7 +362,7 @@ func (r *RootCmd) mcpServer() *serpent.Command {
},
Short: "Start the Coder MCP server.",
Middleware: serpent.Chain(
r.InitClient(client),
r.TryInitClient(client),
),
Options: []serpent.Option{
{
@ -396,19 +397,38 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct
fs := afero.NewOsFs()
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)
// Check authentication status
var username string
// Check authentication status first
if client != nil && client.URL != nil && client.SessionToken() != "" {
// Try to validate the client
me, err := client.User(ctx, codersdk.Me)
if err == nil {
username = me.Username
cliui.Infof(inv.Stderr, "Authentication : Successful")
cliui.Infof(inv.Stderr, "User : %s", username)
} else {
// Authentication failed but we have a client URL
cliui.Warnf(inv.Stderr, "Authentication : Failed (%s)", err)
cliui.Warnf(inv.Stderr, "Some tools that require authentication will not be available.")
}
} else {
cliui.Infof(inv.Stderr, "Authentication : None")
}
// Display URL separately from authentication status
if client != nil && client.URL != nil {
cliui.Infof(inv.Stderr, "URL : %s", client.URL.String())
} else {
cliui.Infof(inv.Stderr, "URL : Not configured")
}
cliui.Infof(inv.Stderr, "Instructions : %q", instructions)
if len(allowedTools) > 0 {
cliui.Infof(inv.Stderr, "Allowed Tools : %v", allowedTools)
cliui.Infof(inv.Stderr, "Allowed Tools : %v", allowedTools)
}
cliui.Infof(inv.Stderr, "Press Ctrl+C to stop the server")
@ -431,13 +451,33 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct
// Get the workspace agent token from the environment.
toolOpts := make([]func(*toolsdk.Deps), 0)
var hasAgentClient bool
if agentToken, err := getAgentToken(fs); err == nil && agentToken != "" {
hasAgentClient = true
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(agentToken)
toolOpts = append(toolOpts, toolsdk.WithAgentClient(agentClient))
var agentURL *url.URL
if client != nil && client.URL != nil {
agentURL = client.URL
} else if agntURL, err := getAgentURL(); err == nil {
agentURL = agntURL
}
// First check if we have a valid client URL, which is required for agent client
if agentURL == nil {
cliui.Infof(inv.Stderr, "Agent URL : Not configured")
} else {
cliui.Warnf(inv.Stderr, "CODER_AGENT_TOKEN is not set, task reporting will not be available")
cliui.Infof(inv.Stderr, "Agent URL : %s", agentURL.String())
agentToken, err := getAgentToken(fs)
if err != nil || agentToken == "" {
cliui.Warnf(inv.Stderr, "CODER_AGENT_TOKEN is not set, task reporting will not be available")
} else {
// Happy path: we have both URL and agent token
agentClient := agentsdk.New(agentURL)
agentClient.SetSessionToken(agentToken)
toolOpts = append(toolOpts, toolsdk.WithAgentClient(agentClient))
hasAgentClient = true
}
}
if (client == nil || client.URL == nil || client.SessionToken() == "") && !hasAgentClient {
return xerrors.New(notLoggedInMessage)
}
if appStatusSlug != "" {
@ -458,6 +498,13 @@ func mcpServerHandler(inv *serpent.Invocation, client *codersdk.Client, instruct
cliui.Warnf(inv.Stderr, "Task reporting not available")
continue
}
// Skip user-dependent tools if no authenticated user
if !tool.UserClientOptional && username == "" {
cliui.Warnf(inv.Stderr, "Tool %q requires authentication and will not be available", tool.Tool.Name)
continue
}
if len(allowedTools) == 0 || slices.ContainsFunc(allowedTools, func(t string) bool {
return t == tool.Tool.Name
}) {
@ -730,6 +777,15 @@ func getAgentToken(fs afero.Fs) (string, error) {
return string(bs), nil
}
func getAgentURL() (*url.URL, error) {
urlString, ok := os.LookupEnv("CODER_AGENT_URL")
if !ok || urlString == "" {
return nil, xerrors.New("CODEDR_AGENT_URL is empty")
}
return url.Parse(urlString)
}
// mcpFromSDK adapts a toolsdk.Tool to go-mcp's server.ServerTool.
// It assumes that the tool responds with a valid JSON object.
func mcpFromSDK(sdkTool toolsdk.GenericTool, tb toolsdk.Deps) server.ServerTool {