From a9637f65b66589b62816191021241fac875923a3 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Sun, 5 Apr 2026 23:25:29 +0530 Subject: [PATCH 01/11] feat: add MCP file support for ACP server configuration --- cmd/server/server.go | 8 ++++++++ lib/httpapi/setup.go | 3 ++- x/acpio/acpio.go | 47 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/cmd/server/server.go b/cmd/server/server.go index cc53aa17..d35dd86a 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -136,6 +136,11 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er } experimentalACP := viper.GetBool(FlagExperimentalACP) + acpMCPFile := viper.GetString(FlagMCPFile) + + if acpMCPFile != "" && !experimentalACP { + return xerrors.Errorf("--mcp-file requires --experimental-acp requires to be set") + } if experimentalACP && (saveState || loadState) { return xerrors.Errorf("ACP mode doesn't support state persistence") @@ -169,6 +174,7 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er acpResult, err = httpapi.SetupACP(ctx, httpapi.SetupACPConfig{ Program: agent, ProgramArgs: argsToPass[1:], + MCPFilePath: acpMCPFile, }) if err != nil { return xerrors.Errorf("failed to setup ACP: %w", err) @@ -382,6 +388,7 @@ const ( FlagSaveState = "save-state" FlagPidFile = "pid-file" FlagExperimentalACP = "experimental-acp" + FlagMCPFile = "mcp-file" ) func CreateServerCmd() *cobra.Command { @@ -425,6 +432,7 @@ func CreateServerCmd() *cobra.Command { {FlagSaveState, "", false, "Save state to state-file on shutdown (defaults to true when state-file is set)", "bool"}, {FlagPidFile, "", "", "Path to file where the server process ID will be written for shutdown scripts", "string"}, {FlagExperimentalACP, "", false, "Use experimental ACP transport instead of PTY", "bool"}, + {FlagMCPFile, "", "", "MCP file for the ACP server", "string"}, } for _, spec := range flagSpecs { diff --git a/lib/httpapi/setup.go b/lib/httpapi/setup.go index b505d533..d4eb7e18 100644 --- a/lib/httpapi/setup.go +++ b/lib/httpapi/setup.go @@ -53,6 +53,7 @@ func SetupProcess(ctx context.Context, config SetupProcessConfig) (*termexec.Pro type SetupACPConfig struct { Program string ProgramArgs []string + MCPFilePath string Clock quartz.Clock } @@ -88,7 +89,7 @@ func SetupACP(ctx context.Context, config SetupACPConfig) (*SetupACPResult, erro return nil, fmt.Errorf("failed to start process: %w", err) } - agentIO, err := acpio.NewWithPipes(ctx, stdin, stdout, logger, os.Getwd) + agentIO, err := acpio.NewWithPipes(ctx, stdin, stdout, logger, os.Getwd, config.MCPFilePath) if err != nil { _ = cmd.Process.Kill() return nil, fmt.Errorf("failed to initialize ACP connection: %w", err) diff --git a/x/acpio/acpio.go b/x/acpio/acpio.go index 1a440132..68fb32df 100644 --- a/x/acpio/acpio.go +++ b/x/acpio/acpio.go @@ -2,14 +2,17 @@ package acpio import ( "context" + "encoding/json" "fmt" "io" "log/slog" + "os" "strings" "sync" acp "github.com/coder/acp-go-sdk" st "github.com/coder/agentapi/lib/screentracker" + "golang.org/x/xerrors" ) // Compile-time assertion that ACPAgentIO implements st.AgentIO @@ -31,6 +34,10 @@ type acpClient struct { agentIO *ACPAgentIO } +type McpConfig struct { + McpServers []acp.McpServer `json:"mcpServers"` +} + var _ acp.Client = (*acpClient)(nil) func (c *acpClient) SessionUpdate(ctx context.Context, params acp.SessionNotification) error { @@ -131,7 +138,7 @@ func (a *ACPAgentIO) SetOnChunk(fn func(chunk string)) { } // NewWithPipes creates an ACPAgentIO connected via the provided pipes -func NewWithPipes(ctx context.Context, toAgent io.Writer, fromAgent io.Reader, logger *slog.Logger, getwd func() (string, error)) (*ACPAgentIO, error) { +func NewWithPipes(ctx context.Context, toAgent io.Writer, fromAgent io.Reader, logger *slog.Logger, getwd func() (string, error), mcpFilePath string) (*ACPAgentIO, error) { if logger == nil { logger = slog.Default() } @@ -154,6 +161,12 @@ func NewWithPipes(ctx context.Context, toAgent io.Writer, fromAgent io.Reader, l } logger.Debug("ACP initialized", "protocolVersion", initResp.ProtocolVersion) + // Prepare the MCPs for the session + supportedMCPList, err := getSupportedMCPConfig(mcpFilePath, logger, &initResp) + if err != nil { + return nil, err + } + // Create a session cwd, err := getwd() if err != nil { @@ -162,7 +175,7 @@ func NewWithPipes(ctx context.Context, toAgent io.Writer, fromAgent io.Reader, l } sessResp, err := conn.NewSession(ctx, acp.NewSessionRequest{ Cwd: cwd, - McpServers: []acp.McpServer{}, + McpServers: supportedMCPList, }) if err != nil { logger.Error("Failed to create ACP session", "error", err) @@ -174,6 +187,36 @@ func NewWithPipes(ctx context.Context, toAgent io.Writer, fromAgent io.Reader, l return agentIO, nil } +func getSupportedMCPConfig(mcpFilePath string, logger *slog.Logger, initResp *acp.InitializeResponse) ([]acp.McpServer, error) { + mcpFile, err := os.Open(mcpFilePath) + if err != nil { + return nil, xerrors.Errorf("Failed to open mcp file: %v", err) + } + + defer func() { + if closeErr := mcpFile.Close(); closeErr != nil { + logger.Error("Failed to close mcp file", "error", err) + } + }() + + var allMcpList McpConfig + decoder := json.NewDecoder(mcpFile) + + if err = decoder.Decode(&allMcpList); err != nil { + return nil, xerrors.Errorf("Failed to decode mcp file: %v", err) + } + + // Only send the MCPs that are supported by the agents + var supportedMCPList []acp.McpServer + for _, mcp := range allMcpList.McpServers { + if (mcp.Http != nil && !initResp.AgentCapabilities.McpCapabilities.Http) || (mcp.Sse != nil && !initResp.AgentCapabilities.McpCapabilities.Sse) { + continue + } + supportedMCPList = append(supportedMCPList, mcp) + } + return supportedMCPList, err +} + // Write sends a message to the agent via ACP prompt func (a *ACPAgentIO) Write(data []byte) (int, error) { text := string(data) From f607202cb3585f3ed5a386257b5ee71a00f6d679 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Sun, 5 Apr 2026 23:48:10 +0530 Subject: [PATCH 02/11] feat: add tests for MCP file handling and validation in server commands --- cmd/server/server_test.go | 52 ++++++++++ e2e/echo_test.go | 38 +++++++ x/acpio/acpio.go | 4 + x/acpio/acpio_test.go | 1 + x/acpio/mcp_internal_test.go | 190 +++++++++++++++++++++++++++++++++++ 5 files changed, 285 insertions(+) create mode 100644 x/acpio/mcp_internal_test.go diff --git a/cmd/server/server_test.go b/cmd/server/server_test.go index 29eb65b4..30846b4e 100644 --- a/cmd/server/server_test.go +++ b/cmd/server/server_test.go @@ -799,3 +799,55 @@ func TestServerCmd_AllowedOrigins(t *testing.T) { }) } } + +func TestServerCmd_MCPFileFlag(t *testing.T) { + t.Run("mcp-file default is empty", func(t *testing.T) { + isolateViper(t) + + serverCmd := CreateServerCmd() + setupCommandOutput(t, serverCmd) + serverCmd.SetArgs([]string{"--exit", "dummy-command"}) + err := serverCmd.Execute() + require.NoError(t, err) + + assert.Equal(t, "", viper.GetString(FlagMCPFile)) + }) + + t.Run("mcp-file can be set via CLI flag", func(t *testing.T) { + isolateViper(t) + + serverCmd := CreateServerCmd() + setupCommandOutput(t, serverCmd) + serverCmd.SetArgs([]string{"--mcp-file", "/path/to/mcp.json", "--exit", "dummy-command"}) + err := serverCmd.Execute() + require.NoError(t, err) + + assert.Equal(t, "/path/to/mcp.json", viper.GetString(FlagMCPFile)) + }) + + t.Run("mcp-file can be set via environment variable", func(t *testing.T) { + isolateViper(t) + t.Setenv("AGENTAPI_MCP_FILE", "/env/path/to/mcp.json") + + serverCmd := CreateServerCmd() + setupCommandOutput(t, serverCmd) + serverCmd.SetArgs([]string{"--exit", "dummy-command"}) + err := serverCmd.Execute() + require.NoError(t, err) + + assert.Equal(t, "/env/path/to/mcp.json", viper.GetString(FlagMCPFile)) + }) + + t.Run("CLI flag overrides environment variable", func(t *testing.T) { + isolateViper(t) + t.Setenv("AGENTAPI_MCP_FILE", "/env/path/to/mcp.json") + + serverCmd := CreateServerCmd() + setupCommandOutput(t, serverCmd) + serverCmd.SetArgs([]string{"--mcp-file", "/cli/path/to/mcp.json", "--exit", "dummy-command"}) + err := serverCmd.Execute() + require.NoError(t, err) + + assert.Equal(t, "/cli/path/to/mcp.json", viper.GetString(FlagMCPFile)) + }) +} diff --git a/e2e/echo_test.go b/e2e/echo_test.go index 1ef567a0..d88eb2d0 100644 --- a/e2e/echo_test.go +++ b/e2e/echo_test.go @@ -556,3 +556,41 @@ func getFreePort() (int, error) { return l.Addr().(*net.TCPAddr).Port, nil } + +func TestServerFlagValidation(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + t.Run("mcp-file requires experimental-acp", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + defer cancel() + + binaryPath := os.Getenv("AGENTAPI_BINARY_PATH") + if binaryPath == "" { + cwd, err := os.Getwd() + require.NoError(t, err, "Failed to get current working directory") + binaryPath = filepath.Join(cwd, "..", "out", "agentapi") + t.Logf("Building binary at %s", binaryPath) + buildCmd := exec.CommandContext(ctx, "go", "build", "-o", binaryPath, ".") + buildCmd.Dir = filepath.Join(cwd, "..") + require.NoError(t, buildCmd.Run(), "Failed to build binary") + } + + // Create a temporary MCP file + tmpDir := t.TempDir() + mcpFile := filepath.Join(tmpDir, "mcp.json") + err := os.WriteFile(mcpFile, []byte(`{"mcpServers": []}`), 0o644) + require.NoError(t, err, "Failed to create temp MCP file") + + // Run the server with --mcp-file but WITHOUT --experimental-acp + cmd := exec.CommandContext(ctx, binaryPath, "server", + "--mcp-file", mcpFile, + "--", "echo", "test") + + output, err := cmd.CombinedOutput() + require.Error(t, err, "Expected server to fail when --mcp-file is used without --experimental-acp") + require.Contains(t, string(output), "--mcp-file requires --experimental-acp", + "Expected error message about --mcp-file requiring --experimental-acp, got: %s", string(output)) + }) +} diff --git a/x/acpio/acpio.go b/x/acpio/acpio.go index 68fb32df..c3009019 100644 --- a/x/acpio/acpio.go +++ b/x/acpio/acpio.go @@ -188,6 +188,10 @@ func NewWithPipes(ctx context.Context, toAgent io.Writer, fromAgent io.Reader, l } func getSupportedMCPConfig(mcpFilePath string, logger *slog.Logger, initResp *acp.InitializeResponse) ([]acp.McpServer, error) { + if mcpFilePath == "" { + return []acp.McpServer{}, nil + } + mcpFile, err := os.Open(mcpFilePath) if err != nil { return nil, xerrors.Errorf("Failed to open mcp file: %v", err) diff --git a/x/acpio/acpio_test.go b/x/acpio/acpio_test.go index 2e91c547..728c2f27 100644 --- a/x/acpio/acpio_test.go +++ b/x/acpio/acpio_test.go @@ -70,6 +70,7 @@ func newTestPair(t *testing.T, agent *testAgent) *acpio.ACPAgentIO { clientToAgentW, agentToClientR, nil, func() (string, error) { return os.TempDir(), nil }, + "", // no MCP file ) require.NoError(t, err) diff --git a/x/acpio/mcp_internal_test.go b/x/acpio/mcp_internal_test.go new file mode 100644 index 00000000..d7a2203c --- /dev/null +++ b/x/acpio/mcp_internal_test.go @@ -0,0 +1,190 @@ +package acpio + +import ( + "log/slog" + "os" + "path/filepath" + "testing" + + acp "github.com/coder/acp-go-sdk" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetSupportedMCPConfig(t *testing.T) { + logger := slog.Default() + + t.Run("empty file path returns empty slice", func(t *testing.T) { + initResp := &acp.InitializeResponse{} + result, err := getSupportedMCPConfig("", logger, initResp) + require.NoError(t, err) + assert.Empty(t, result) + }) + + t.Run("file not found returns error", func(t *testing.T) { + initResp := &acp.InitializeResponse{} + _, err := getSupportedMCPConfig("/nonexistent/path/mcp.json", logger, initResp) + require.Error(t, err) + assert.Contains(t, err.Error(), "Failed to open mcp file") + }) + + t.Run("invalid JSON returns error", func(t *testing.T) { + tmpDir := t.TempDir() + mcpFile := filepath.Join(tmpDir, "invalid.json") + err := os.WriteFile(mcpFile, []byte("not valid json"), 0o644) + require.NoError(t, err) + + initResp := &acp.InitializeResponse{} + _, err = getSupportedMCPConfig(mcpFile, logger, initResp) + require.Error(t, err) + assert.Contains(t, err.Error(), "Failed to decode mcp file") + }) + + t.Run("stdio servers always included", func(t *testing.T) { + tmpDir := t.TempDir() + mcpFile := filepath.Join(tmpDir, "mcp.json") + mcpContent := `{ + "mcpServers": [ + { + "name": "test-stdio", + "command": "/usr/bin/test", + "args": ["--stdio"], + "env": [] + } + ] + }` + err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) + require.NoError(t, err) + + initResp := &acp.InitializeResponse{ + AgentCapabilities: acp.AgentCapabilities{ + McpCapabilities: acp.McpCapabilities{ + Http: false, + Sse: false, + }, + }, + } + result, err := getSupportedMCPConfig(mcpFile, logger, initResp) + require.NoError(t, err) + assert.Len(t, result, 1) + assert.NotNil(t, result[0].Stdio) + assert.Equal(t, "test-stdio", result[0].Stdio.Name) + }) + + t.Run("http servers filtered when capability is false", func(t *testing.T) { + tmpDir := t.TempDir() + mcpFile := filepath.Join(tmpDir, "mcp.json") + mcpContent := `{ + "mcpServers": [ + { + "type": "http", + "name": "test-http", + "url": "https://example.com/mcp", + "headers": [] + } + ] + }` + err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) + require.NoError(t, err) + + initResp := &acp.InitializeResponse{ + AgentCapabilities: acp.AgentCapabilities{ + McpCapabilities: acp.McpCapabilities{ + Http: false, + Sse: false, + }, + }, + } + result, err := getSupportedMCPConfig(mcpFile, logger, initResp) + require.NoError(t, err) + assert.Empty(t, result) + }) + + t.Run("http servers included when capability is true", func(t *testing.T) { + tmpDir := t.TempDir() + mcpFile := filepath.Join(tmpDir, "mcp.json") + mcpContent := `{ + "mcpServers": [ + { + "type": "http", + "name": "test-http", + "url": "https://example.com/mcp", + "headers": [] + } + ] + }` + err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) + require.NoError(t, err) + + initResp := &acp.InitializeResponse{ + AgentCapabilities: acp.AgentCapabilities{ + McpCapabilities: acp.McpCapabilities{ + Http: true, + Sse: false, + }, + }, + } + result, err := getSupportedMCPConfig(mcpFile, logger, initResp) + require.NoError(t, err) + assert.Len(t, result, 1) + assert.NotNil(t, result[0].Http) + assert.Equal(t, "test-http", result[0].Http.Name) + }) + + t.Run("mixed servers filtered correctly", func(t *testing.T) { + tmpDir := t.TempDir() + mcpFile := filepath.Join(tmpDir, "mcp.json") + mcpContent := `{ + "mcpServers": [ + { + "name": "stdio-server", + "command": "/usr/bin/stdio-mcp", + "args": [], + "env": [] + }, + { + "type": "http", + "name": "http-server", + "url": "https://example.com/mcp", + "headers": [] + } + ] + }` + err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) + require.NoError(t, err) + + // With HTTP capability disabled, only stdio should be included + initResp := &acp.InitializeResponse{ + AgentCapabilities: acp.AgentCapabilities{ + McpCapabilities: acp.McpCapabilities{ + Http: false, + Sse: false, + }, + }, + } + result, err := getSupportedMCPConfig(mcpFile, logger, initResp) + require.NoError(t, err) + assert.Len(t, result, 1) + assert.NotNil(t, result[0].Stdio) + assert.Equal(t, "stdio-server", result[0].Stdio.Name) + + // With HTTP capability enabled, both should be included + initResp.AgentCapabilities.McpCapabilities.Http = true + result, err = getSupportedMCPConfig(mcpFile, logger, initResp) + require.NoError(t, err) + assert.Len(t, result, 2) + }) + + t.Run("empty mcpServers array returns empty slice", func(t *testing.T) { + tmpDir := t.TempDir() + mcpFile := filepath.Join(tmpDir, "mcp.json") + mcpContent := `{"mcpServers": []}` + err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) + require.NoError(t, err) + + initResp := &acp.InitializeResponse{} + result, err := getSupportedMCPConfig(mcpFile, logger, initResp) + require.NoError(t, err) + assert.Empty(t, result) + }) +} From 2fbafff12b277122891c085e7a897d243db5db8e Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 6 Apr 2026 07:29:05 +0530 Subject: [PATCH 03/11] fix: improve error messages for MCP file handling in server commands --- cmd/server/server.go | 2 +- x/acpio/acpio.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/server/server.go b/cmd/server/server.go index d35dd86a..ba8b82c8 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -139,7 +139,7 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er acpMCPFile := viper.GetString(FlagMCPFile) if acpMCPFile != "" && !experimentalACP { - return xerrors.Errorf("--mcp-file requires --experimental-acp requires to be set") + return xerrors.Errorf("--mcp-file requires --experimental-acp") } if experimentalACP && (saveState || loadState) { diff --git a/x/acpio/acpio.go b/x/acpio/acpio.go index c3009019..8027522e 100644 --- a/x/acpio/acpio.go +++ b/x/acpio/acpio.go @@ -194,12 +194,12 @@ func getSupportedMCPConfig(mcpFilePath string, logger *slog.Logger, initResp *ac mcpFile, err := os.Open(mcpFilePath) if err != nil { - return nil, xerrors.Errorf("Failed to open mcp file: %v", err) + return nil, xerrors.Errorf("Failed to open mcp file: %w", err) } defer func() { if closeErr := mcpFile.Close(); closeErr != nil { - logger.Error("Failed to close mcp file", "error", err) + logger.Error("Failed to close mcp file", "path", mcpFilePath, "error", closeErr) } }() @@ -207,7 +207,7 @@ func getSupportedMCPConfig(mcpFilePath string, logger *slog.Logger, initResp *ac decoder := json.NewDecoder(mcpFile) if err = decoder.Decode(&allMcpList); err != nil { - return nil, xerrors.Errorf("Failed to decode mcp file: %v", err) + return nil, xerrors.Errorf("Failed to decode mcp file: %w", err) } // Only send the MCPs that are supported by the agents From 12ecfa4f14595acb2d62a71fa21707b4723d38c3 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 6 Apr 2026 08:09:21 +0530 Subject: [PATCH 04/11] refactor: update MCP file handling to support new configuration format and improve error messages --- x/acpio/acpio.go | 35 ++++--- x/acpio/mcp_internal_test.go | 186 +++++++++++++++++++++++++++++------ 2 files changed, 180 insertions(+), 41 deletions(-) diff --git a/x/acpio/acpio.go b/x/acpio/acpio.go index 8027522e..680c282e 100644 --- a/x/acpio/acpio.go +++ b/x/acpio/acpio.go @@ -34,10 +34,6 @@ type acpClient struct { agentIO *ACPAgentIO } -type McpConfig struct { - McpServers []acp.McpServer `json:"mcpServers"` -} - var _ acp.Client = (*acpClient)(nil) func (c *acpClient) SessionUpdate(ctx context.Context, params acp.SessionNotification) error { @@ -194,7 +190,7 @@ func getSupportedMCPConfig(mcpFilePath string, logger *slog.Logger, initResp *ac mcpFile, err := os.Open(mcpFilePath) if err != nil { - return nil, xerrors.Errorf("Failed to open mcp file: %w", err) + return nil, xerrors.Errorf("failed to open mcp file: %w", err) } defer func() { @@ -203,22 +199,35 @@ func getSupportedMCPConfig(mcpFilePath string, logger *slog.Logger, initResp *ac } }() - var allMcpList McpConfig + var claudeConfig AgentapiMcpConfig decoder := json.NewDecoder(mcpFile) - if err = decoder.Decode(&allMcpList); err != nil { - return nil, xerrors.Errorf("Failed to decode mcp file: %w", err) + if err = decoder.Decode(&claudeConfig); err != nil { + return nil, xerrors.Errorf("failed to decode mcp file: %w", err) } - // Only send the MCPs that are supported by the agents + // Convert MCP format to ACP format and filter by agent capabilities var supportedMCPList []acp.McpServer - for _, mcp := range allMcpList.McpServers { - if (mcp.Http != nil && !initResp.AgentCapabilities.McpCapabilities.Http) || (mcp.Sse != nil && !initResp.AgentCapabilities.McpCapabilities.Sse) { + for name, server := range claudeConfig.McpServers { + mcpServer, err := server.convertAgentapiMcpToAcp(name) + if err != nil { + logger.Warn("Skipping invalid MCP server", "name", name, "error", err) + continue + } + + // Filter based on agent capabilities + if mcpServer.Http != nil && !initResp.AgentCapabilities.McpCapabilities.Http { + logger.Debug("Skipping HTTP MCP server (agent doesn't support HTTP)", "name", name) + continue + } + if mcpServer.Sse != nil && !initResp.AgentCapabilities.McpCapabilities.Sse { + logger.Debug("Skipping SSE MCP server (agent doesn't support SSE)", "name", name) continue } - supportedMCPList = append(supportedMCPList, mcp) + + supportedMCPList = append(supportedMCPList, mcpServer) } - return supportedMCPList, err + return supportedMCPList, nil } // Write sends a message to the agent via ACP prompt diff --git a/x/acpio/mcp_internal_test.go b/x/acpio/mcp_internal_test.go index d7a2203c..a4245cb1 100644 --- a/x/acpio/mcp_internal_test.go +++ b/x/acpio/mcp_internal_test.go @@ -25,7 +25,7 @@ func TestGetSupportedMCPConfig(t *testing.T) { initResp := &acp.InitializeResponse{} _, err := getSupportedMCPConfig("/nonexistent/path/mcp.json", logger, initResp) require.Error(t, err) - assert.Contains(t, err.Error(), "Failed to open mcp file") + assert.Contains(t, err.Error(), "failed to open mcp file") }) t.Run("invalid JSON returns error", func(t *testing.T) { @@ -37,21 +37,23 @@ func TestGetSupportedMCPConfig(t *testing.T) { initResp := &acp.InitializeResponse{} _, err = getSupportedMCPConfig(mcpFile, logger, initResp) require.Error(t, err) - assert.Contains(t, err.Error(), "Failed to decode mcp file") + assert.Contains(t, err.Error(), "failed to decode mcp file") }) t.Run("stdio servers always included", func(t *testing.T) { tmpDir := t.TempDir() mcpFile := filepath.Join(tmpDir, "mcp.json") + // Claude MCP format: mcpServers is a map with server name as key mcpContent := `{ - "mcpServers": [ - { - "name": "test-stdio", + "mcpServers": { + "test-stdio": { "command": "/usr/bin/test", "args": ["--stdio"], - "env": [] + "env": { + "DEBUG": "true" + } } - ] + } }` err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) require.NoError(t, err) @@ -69,20 +71,27 @@ func TestGetSupportedMCPConfig(t *testing.T) { assert.Len(t, result, 1) assert.NotNil(t, result[0].Stdio) assert.Equal(t, "test-stdio", result[0].Stdio.Name) + assert.Equal(t, "/usr/bin/test", result[0].Stdio.Command) + assert.Equal(t, []string{"--stdio"}, result[0].Stdio.Args) + // Check env was converted correctly + assert.Len(t, result[0].Stdio.Env, 1) + assert.Equal(t, "DEBUG", result[0].Stdio.Env[0].Name) + assert.Equal(t, "true", result[0].Stdio.Env[0].Value) }) t.Run("http servers filtered when capability is false", func(t *testing.T) { tmpDir := t.TempDir() mcpFile := filepath.Join(tmpDir, "mcp.json") mcpContent := `{ - "mcpServers": [ - { + "mcpServers": { + "test-http": { "type": "http", - "name": "test-http", "url": "https://example.com/mcp", - "headers": [] + "headers": { + "Authorization": "Bearer token123" + } } - ] + } }` err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) require.NoError(t, err) @@ -104,14 +113,15 @@ func TestGetSupportedMCPConfig(t *testing.T) { tmpDir := t.TempDir() mcpFile := filepath.Join(tmpDir, "mcp.json") mcpContent := `{ - "mcpServers": [ - { + "mcpServers": { + "test-http": { "type": "http", - "name": "test-http", "url": "https://example.com/mcp", - "headers": [] + "headers": { + "Authorization": "Bearer token123" + } } - ] + } }` err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) require.NoError(t, err) @@ -129,26 +139,28 @@ func TestGetSupportedMCPConfig(t *testing.T) { assert.Len(t, result, 1) assert.NotNil(t, result[0].Http) assert.Equal(t, "test-http", result[0].Http.Name) + assert.Equal(t, "https://example.com/mcp", result[0].Http.Url) + // Check headers were converted correctly + assert.Len(t, result[0].Http.Headers, 1) + assert.Equal(t, "Authorization", result[0].Http.Headers[0].Name) + assert.Equal(t, "Bearer token123", result[0].Http.Headers[0].Value) }) t.Run("mixed servers filtered correctly", func(t *testing.T) { tmpDir := t.TempDir() mcpFile := filepath.Join(tmpDir, "mcp.json") mcpContent := `{ - "mcpServers": [ - { - "name": "stdio-server", + "mcpServers": { + "stdio-server": { "command": "/usr/bin/stdio-mcp", - "args": [], - "env": [] + "args": [] }, - { + "http-server": { "type": "http", - "name": "http-server", "url": "https://example.com/mcp", - "headers": [] + "headers": {} } - ] + } }` err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) require.NoError(t, err) @@ -175,10 +187,10 @@ func TestGetSupportedMCPConfig(t *testing.T) { assert.Len(t, result, 2) }) - t.Run("empty mcpServers array returns empty slice", func(t *testing.T) { + t.Run("empty mcpServers object returns empty slice", func(t *testing.T) { tmpDir := t.TempDir() mcpFile := filepath.Join(tmpDir, "mcp.json") - mcpContent := `{"mcpServers": []}` + mcpContent := `{"mcpServers": {}}` err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) require.NoError(t, err) @@ -187,4 +199,122 @@ func TestGetSupportedMCPConfig(t *testing.T) { require.NoError(t, err) assert.Empty(t, result) }) + + t.Run("server without command or url returns error", func(t *testing.T) { + tmpDir := t.TempDir() + mcpFile := filepath.Join(tmpDir, "mcp.json") + mcpContent := `{ + "mcpServers": { + "invalid-server": { + "args": ["--foo"] + } + } + }` + err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) + require.NoError(t, err) + + initResp := &acp.InitializeResponse{} + result, err := getSupportedMCPConfig(mcpFile, logger, initResp) + require.NoError(t, err) + // Invalid servers are skipped with a warning, not an error + assert.Empty(t, result) + }) + + t.Run("http server inferred from url field", func(t *testing.T) { + tmpDir := t.TempDir() + mcpFile := filepath.Join(tmpDir, "mcp.json") + // No explicit type, but has url - should be inferred as http + mcpContent := `{ + "mcpServers": { + "inferred-http": { + "url": "https://example.com/mcp" + } + } + }` + err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) + require.NoError(t, err) + + initResp := &acp.InitializeResponse{ + AgentCapabilities: acp.AgentCapabilities{ + McpCapabilities: acp.McpCapabilities{ + Http: true, + }, + }, + } + result, err := getSupportedMCPConfig(mcpFile, logger, initResp) + require.NoError(t, err) + assert.Len(t, result, 1) + assert.NotNil(t, result[0].Http) + assert.Equal(t, "inferred-http", result[0].Http.Name) + }) +} + +func TestConvertAgentapiMcpToAcp(t *testing.T) { + t.Run("converts stdio server correctly", func(t *testing.T) { + server := AgentapiMcpServer{ + Command: "/usr/bin/mcp-server", + Args: []string{"--arg1", "--arg2"}, + Env: map[string]string{ + "KEY1": "value1", + "KEY2": "value2", + }, + } + + result, err := server.convertAgentapiMcpToAcp("my-server") + require.NoError(t, err) + require.NotNil(t, result.Stdio) + assert.Equal(t, "my-server", result.Stdio.Name) + assert.Equal(t, "/usr/bin/mcp-server", result.Stdio.Command) + assert.Equal(t, []string{"--arg1", "--arg2"}, result.Stdio.Args) + assert.Len(t, result.Stdio.Env, 2) + }) + + t.Run("converts http server correctly", func(t *testing.T) { + server := AgentapiMcpServer{ + Type: "http", + URL: "https://api.example.com/mcp", + Headers: map[string]string{ + "Authorization": "Bearer token", + "X-Custom": "value", + }, + } + + result, err := server.convertAgentapiMcpToAcp("api-server") + require.NoError(t, err) + require.NotNil(t, result.Http) + assert.Equal(t, "api-server", result.Http.Name) + assert.Equal(t, "https://api.example.com/mcp", result.Http.Url) + assert.Len(t, result.Http.Headers, 2) + }) + + t.Run("returns error for stdio without command", func(t *testing.T) { + server := AgentapiMcpServer{ + Type: "stdio", + Args: []string{"--arg"}, + } + + _, err := server.convertAgentapiMcpToAcp("bad-server") + require.Error(t, err) + assert.Contains(t, err.Error(), "missing command") + }) + + t.Run("returns error for http without url", func(t *testing.T) { + server := AgentapiMcpServer{ + Type: "http", + } + + _, err := server.convertAgentapiMcpToAcp("bad-server") + require.Error(t, err) + assert.Contains(t, err.Error(), "missing url") + }) + + t.Run("returns error for unsupported type", func(t *testing.T) { + server := AgentapiMcpServer{ + Type: "websocket", + } + + _, err := server.convertAgentapiMcpToAcp("bad-server") + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported server type") + }) } From 8183c5f8e8801cbecc112e25fbedea9d7e0a98d8 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 6 Apr 2026 08:10:00 +0530 Subject: [PATCH 05/11] refactor: rename variable for MCP configuration to improve clarity --- x/acpio/acpio.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x/acpio/acpio.go b/x/acpio/acpio.go index 680c282e..d1bc2e0f 100644 --- a/x/acpio/acpio.go +++ b/x/acpio/acpio.go @@ -199,16 +199,16 @@ func getSupportedMCPConfig(mcpFilePath string, logger *slog.Logger, initResp *ac } }() - var claudeConfig AgentapiMcpConfig + var mcpConfig AgentapiMcpConfig decoder := json.NewDecoder(mcpFile) - if err = decoder.Decode(&claudeConfig); err != nil { + if err = decoder.Decode(&mcpConfig); err != nil { return nil, xerrors.Errorf("failed to decode mcp file: %w", err) } // Convert MCP format to ACP format and filter by agent capabilities var supportedMCPList []acp.McpServer - for name, server := range claudeConfig.McpServers { + for name, server := range mcpConfig.McpServers { mcpServer, err := server.convertAgentapiMcpToAcp(name) if err != nil { logger.Warn("Skipping invalid MCP server", "name", name, "error", err) From bf483733d4f3ed937a175abb41ae680ad6e3eb3e Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 6 Apr 2026 08:17:07 +0530 Subject: [PATCH 06/11] docs: add MCP configuration section to README for experimental ACP transport --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index e6a75618..c424663b 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,39 @@ agentapi server --allowed-origins 'https://example.com,http://localhost:3000' -- AGENTAPI_ALLOWED_ORIGINS='https://example.com http://localhost:3000' agentapi server -- claude ``` +#### MCP configuration (experimental) + +When using the experimental ACP transport (`--experimental-acp`), you can provide MCP servers to the agent via a JSON configuration file using the `--mcp-file` flag. + +The file uses the same format as Claude's MCP configuration: + +```json +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"], + "env": { + "DEBUG": "true" + } + }, + "api-server": { + "type": "http", + "url": "https://api.example.com/mcp", + "headers": { + "Authorization": "Bearer token" + } + } + } +} +``` + +Example usage: + +```bash +agentapi server --experimental-acp --mcp-file ./mcp.json -- claude +``` + ### `agentapi attach` Attach to a running agent's terminal session. From 28158f36778a3a37c74d88934299643f8accd9b4 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 6 Apr 2026 08:20:17 +0530 Subject: [PATCH 07/11] feat: add AgentapiMcpConfig and AgentapiMcpServer types with conversion logic for ACP format --- x/acpio/mcp.go | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 x/acpio/mcp.go diff --git a/x/acpio/mcp.go b/x/acpio/mcp.go new file mode 100644 index 00000000..3f55ad18 --- /dev/null +++ b/x/acpio/mcp.go @@ -0,0 +1,85 @@ +package acpio + +import ( + "github.com/coder/acp-go-sdk" + "golang.org/x/xerrors" +) + +// AgentapiMcpConfig represents the Claude MCP JSON format where mcpServers is a map +// with server names as keys. +type AgentapiMcpConfig struct { + McpServers map[string]AgentapiMcpServer `json:"mcpServers"` +} + +// AgentapiMcpServer represents a single MCP server in Claude's format. +type AgentapiMcpServer struct { + // Type can be "stdio" or "http". Defaults to "stdio" if not specified. + Type string `json:"type,omitempty"` + // Stdio transport fields + Command string `json:"command,omitempty"` + Args []string `json:"args,omitempty"` + Env map[string]string `json:"env,omitempty"` + // HTTP transport fields + URL string `json:"url,omitempty"` + Headers map[string]string `json:"headers,omitempty"` +} + +// convertAgentapiMcpToAcp converts a Claude MCP server config to the ACP format. +func (a *AgentapiMcpServer) convertAgentapiMcpToAcp(name string) (acp.McpServer, error) { + serverType := a.Type + if serverType == "" { + // Default to stdio if no type specified and command is present + if a.Command != "" { + serverType = "stdio" + } else if a.URL != "" { + serverType = "http" + } + } + + switch serverType { + case "stdio", "": + if a.Command == "" { + return acp.McpServer{}, xerrors.Errorf("stdio server %q missing command", name) + } + // Convert env map to []EnvVariable + var envVars []acp.EnvVariable + for key, value := range a.Env { + envVars = append(envVars, acp.EnvVariable{ + Name: key, + Value: value, + }) + } + return acp.McpServer{ + Stdio: &acp.McpServerStdio{ + Name: name, + Command: a.Command, + Args: a.Args, + Env: envVars, + }, + }, nil + + case "http": + if a.URL == "" { + return acp.McpServer{}, xerrors.Errorf("http server %q missing url", name) + } + // Convert headers map to []HttpHeader + var headers []acp.HttpHeader + for key, value := range a.Headers { + headers = append(headers, acp.HttpHeader{ + Name: key, + Value: value, + }) + } + return acp.McpServer{ + Http: &acp.McpServerHttp{ + Name: name, + Type: "http", + Url: a.URL, + Headers: headers, + }, + }, nil + + default: + return acp.McpServer{}, xerrors.Errorf("unsupported server type %q for server %q", serverType, name) + } +} From a9d80dc595e726c3122170799bb14df54202f189 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 6 Apr 2026 08:23:44 +0530 Subject: [PATCH 08/11] refactor: update test case description for clarity in MCP server command handling --- x/acpio/mcp_internal_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x/acpio/mcp_internal_test.go b/x/acpio/mcp_internal_test.go index a4245cb1..98cdfc90 100644 --- a/x/acpio/mcp_internal_test.go +++ b/x/acpio/mcp_internal_test.go @@ -200,7 +200,7 @@ func TestGetSupportedMCPConfig(t *testing.T) { assert.Empty(t, result) }) - t.Run("server without command or url returns error", func(t *testing.T) { + t.Run("server without command or url is skipped", func(t *testing.T) { tmpDir := t.TempDir() mcpFile := filepath.Join(tmpDir, "mcp.json") mcpContent := `{ From f31782ba51adf1464b312c53016f4badd7eb33bd Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 6 Apr 2026 08:25:22 +0530 Subject: [PATCH 09/11] chore: rename --- cmd/server/server.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/server/server.go b/cmd/server/server.go index ba8b82c8..b2ceef3e 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -136,9 +136,9 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er } experimentalACP := viper.GetBool(FlagExperimentalACP) - acpMCPFile := viper.GetString(FlagMCPFile) + mcpFile := viper.GetString(FlagMCPFile) - if acpMCPFile != "" && !experimentalACP { + if mcpFile != "" && !experimentalACP { return xerrors.Errorf("--mcp-file requires --experimental-acp") } @@ -174,7 +174,7 @@ func runServer(ctx context.Context, logger *slog.Logger, argsToPass []string) er acpResult, err = httpapi.SetupACP(ctx, httpapi.SetupACPConfig{ Program: agent, ProgramArgs: argsToPass[1:], - MCPFilePath: acpMCPFile, + MCPFilePath: mcpFile, }) if err != nil { return xerrors.Errorf("failed to setup ACP: %w", err) From c891eceae169ee7d0c6895573e82aa4f2c971bd0 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 6 Apr 2026 10:26:24 +0530 Subject: [PATCH 10/11] feat: enhance MCP server configuration with support for SSE type and update JSON structure --- README.md | 1 + e2e/echo_test.go | 2 +- x/acpio/mcp.go | 56 ++++++++++++++++++------------------ x/acpio/mcp_internal_test.go | 1 + 4 files changed, 31 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index c424663b..22a40e52 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ The file uses the same format as Claude's MCP configuration: ```json { "mcpServers": { + "type": "stdio", "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"], diff --git a/e2e/echo_test.go b/e2e/echo_test.go index d88eb2d0..65c2bdd8 100644 --- a/e2e/echo_test.go +++ b/e2e/echo_test.go @@ -580,7 +580,7 @@ func TestServerFlagValidation(t *testing.T) { // Create a temporary MCP file tmpDir := t.TempDir() mcpFile := filepath.Join(tmpDir, "mcp.json") - err := os.WriteFile(mcpFile, []byte(`{"mcpServers": []}`), 0o644) + err := os.WriteFile(mcpFile, []byte(`{"mcpServers": {}}`), 0o644) require.NoError(t, err, "Failed to create temp MCP file") // Run the server with --mcp-file but WITHOUT --experimental-acp diff --git a/x/acpio/mcp.go b/x/acpio/mcp.go index 3f55ad18..2a4ac9f6 100644 --- a/x/acpio/mcp.go +++ b/x/acpio/mcp.go @@ -1,6 +1,8 @@ package acpio import ( + "slices" + "github.com/coder/acp-go-sdk" "golang.org/x/xerrors" ) @@ -13,13 +15,13 @@ type AgentapiMcpConfig struct { // AgentapiMcpServer represents a single MCP server in Claude's format. type AgentapiMcpServer struct { - // Type can be "stdio" or "http". Defaults to "stdio" if not specified. - Type string `json:"type,omitempty"` + // Type can be "stdio", "sse" or "http" + Type string `json:"type"` // Stdio transport fields Command string `json:"command,omitempty"` Args []string `json:"args,omitempty"` Env map[string]string `json:"env,omitempty"` - // HTTP transport fields + // HTTP | SSE transport fields URL string `json:"url,omitempty"` Headers map[string]string `json:"headers,omitempty"` } @@ -27,17 +29,9 @@ type AgentapiMcpServer struct { // convertAgentapiMcpToAcp converts a Claude MCP server config to the ACP format. func (a *AgentapiMcpServer) convertAgentapiMcpToAcp(name string) (acp.McpServer, error) { serverType := a.Type - if serverType == "" { - // Default to stdio if no type specified and command is present - if a.Command != "" { - serverType = "stdio" - } else if a.URL != "" { - serverType = "http" - } - } + acpMCPServer := acp.McpServer{} - switch serverType { - case "stdio", "": + if serverType == "stdio" { if a.Command == "" { return acp.McpServer{}, xerrors.Errorf("stdio server %q missing command", name) } @@ -49,16 +43,14 @@ func (a *AgentapiMcpServer) convertAgentapiMcpToAcp(name string) (acp.McpServer, Value: value, }) } - return acp.McpServer{ - Stdio: &acp.McpServerStdio{ - Name: name, - Command: a.Command, - Args: a.Args, - Env: envVars, - }, - }, nil - case "http": + acpMCPServer.Stdio = &acp.McpServerStdio{ + Name: name, + Command: a.Command, + Args: a.Args, + Env: envVars, + } + } else if slices.Contains([]string{"http", "sse"}, serverType) { if a.URL == "" { return acp.McpServer{}, xerrors.Errorf("http server %q missing url", name) } @@ -70,16 +62,24 @@ func (a *AgentapiMcpServer) convertAgentapiMcpToAcp(name string) (acp.McpServer, Value: value, }) } - return acp.McpServer{ - Http: &acp.McpServerHttp{ + + if serverType == "sse" { + acpMCPServer.Sse = &acp.McpServerSse{ + Name: name, + Type: "sse", + Url: a.URL, + Headers: headers, + } + } else { + acpMCPServer.Http = &acp.McpServerHttp{ Name: name, Type: "http", Url: a.URL, Headers: headers, - }, - }, nil - - default: + } + } + } else { return acp.McpServer{}, xerrors.Errorf("unsupported server type %q for server %q", serverType, name) } + return acpMCPServer, nil } diff --git a/x/acpio/mcp_internal_test.go b/x/acpio/mcp_internal_test.go index 98cdfc90..9f81bb9b 100644 --- a/x/acpio/mcp_internal_test.go +++ b/x/acpio/mcp_internal_test.go @@ -252,6 +252,7 @@ func TestGetSupportedMCPConfig(t *testing.T) { func TestConvertAgentapiMcpToAcp(t *testing.T) { t.Run("converts stdio server correctly", func(t *testing.T) { server := AgentapiMcpServer{ + Type: "stdio", Command: "/usr/bin/mcp-server", Args: []string{"--arg1", "--arg2"}, Env: map[string]string{ From c24b19f3bf95894e113fe35af611ce8b878b0aea Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Mon, 6 Apr 2026 11:05:03 +0530 Subject: [PATCH 11/11] feat: add warning for empty MCP file and update test cases for clarity --- README.md | 2 +- x/acpio/acpio.go | 5 ++ x/acpio/mcp_internal_test.go | 136 ++++++----------------------------- 3 files changed, 29 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index 22a40e52..e3071efc 100644 --- a/README.md +++ b/README.md @@ -143,8 +143,8 @@ The file uses the same format as Claude's MCP configuration: ```json { "mcpServers": { - "type": "stdio", "filesystem": { + "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"], "env": { diff --git a/x/acpio/acpio.go b/x/acpio/acpio.go index d1bc2e0f..14b0854e 100644 --- a/x/acpio/acpio.go +++ b/x/acpio/acpio.go @@ -203,6 +203,11 @@ func getSupportedMCPConfig(mcpFilePath string, logger *slog.Logger, initResp *ac decoder := json.NewDecoder(mcpFile) if err = decoder.Decode(&mcpConfig); err != nil { + // If file is empty, warn and continue with empty config + if err == io.EOF { + logger.Warn("MCP file is empty, continuing with no MCP servers", "path", mcpFilePath) + return []acp.McpServer{}, nil + } return nil, xerrors.Errorf("failed to decode mcp file: %w", err) } diff --git a/x/acpio/mcp_internal_test.go b/x/acpio/mcp_internal_test.go index 9f81bb9b..6b652954 100644 --- a/x/acpio/mcp_internal_test.go +++ b/x/acpio/mcp_internal_test.go @@ -40,118 +40,25 @@ func TestGetSupportedMCPConfig(t *testing.T) { assert.Contains(t, err.Error(), "failed to decode mcp file") }) - t.Run("stdio servers always included", func(t *testing.T) { + t.Run("empty file returns warning and empty slice", func(t *testing.T) { tmpDir := t.TempDir() - mcpFile := filepath.Join(tmpDir, "mcp.json") - // Claude MCP format: mcpServers is a map with server name as key - mcpContent := `{ - "mcpServers": { - "test-stdio": { - "command": "/usr/bin/test", - "args": ["--stdio"], - "env": { - "DEBUG": "true" - } - } - } - }` - err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) + mcpFile := filepath.Join(tmpDir, "empty.json") + err := os.WriteFile(mcpFile, []byte(""), 0o644) require.NoError(t, err) - initResp := &acp.InitializeResponse{ - AgentCapabilities: acp.AgentCapabilities{ - McpCapabilities: acp.McpCapabilities{ - Http: false, - Sse: false, - }, - }, - } - result, err := getSupportedMCPConfig(mcpFile, logger, initResp) - require.NoError(t, err) - assert.Len(t, result, 1) - assert.NotNil(t, result[0].Stdio) - assert.Equal(t, "test-stdio", result[0].Stdio.Name) - assert.Equal(t, "/usr/bin/test", result[0].Stdio.Command) - assert.Equal(t, []string{"--stdio"}, result[0].Stdio.Args) - // Check env was converted correctly - assert.Len(t, result[0].Stdio.Env, 1) - assert.Equal(t, "DEBUG", result[0].Stdio.Env[0].Name) - assert.Equal(t, "true", result[0].Stdio.Env[0].Value) - }) - - t.Run("http servers filtered when capability is false", func(t *testing.T) { - tmpDir := t.TempDir() - mcpFile := filepath.Join(tmpDir, "mcp.json") - mcpContent := `{ - "mcpServers": { - "test-http": { - "type": "http", - "url": "https://example.com/mcp", - "headers": { - "Authorization": "Bearer token123" - } - } - } - }` - err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) - require.NoError(t, err) - - initResp := &acp.InitializeResponse{ - AgentCapabilities: acp.AgentCapabilities{ - McpCapabilities: acp.McpCapabilities{ - Http: false, - Sse: false, - }, - }, - } + initResp := &acp.InitializeResponse{} result, err := getSupportedMCPConfig(mcpFile, logger, initResp) require.NoError(t, err) assert.Empty(t, result) }) - t.Run("http servers included when capability is true", func(t *testing.T) { - tmpDir := t.TempDir() - mcpFile := filepath.Join(tmpDir, "mcp.json") - mcpContent := `{ - "mcpServers": { - "test-http": { - "type": "http", - "url": "https://example.com/mcp", - "headers": { - "Authorization": "Bearer token123" - } - } - } - }` - err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) - require.NoError(t, err) - - initResp := &acp.InitializeResponse{ - AgentCapabilities: acp.AgentCapabilities{ - McpCapabilities: acp.McpCapabilities{ - Http: true, - Sse: false, - }, - }, - } - result, err := getSupportedMCPConfig(mcpFile, logger, initResp) - require.NoError(t, err) - assert.Len(t, result, 1) - assert.NotNil(t, result[0].Http) - assert.Equal(t, "test-http", result[0].Http.Name) - assert.Equal(t, "https://example.com/mcp", result[0].Http.Url) - // Check headers were converted correctly - assert.Len(t, result[0].Http.Headers, 1) - assert.Equal(t, "Authorization", result[0].Http.Headers[0].Name) - assert.Equal(t, "Bearer token123", result[0].Http.Headers[0].Value) - }) - - t.Run("mixed servers filtered correctly", func(t *testing.T) { + t.Run("servers filtered correctly", func(t *testing.T) { tmpDir := t.TempDir() mcpFile := filepath.Join(tmpDir, "mcp.json") mcpContent := `{ "mcpServers": { "stdio-server": { + "type": "stdio", "command": "/usr/bin/stdio-mcp", "args": [] }, @@ -206,6 +113,7 @@ func TestGetSupportedMCPConfig(t *testing.T) { mcpContent := `{ "mcpServers": { "invalid-server": { + "type": "stdio", "args": ["--foo"] } } @@ -220,32 +128,24 @@ func TestGetSupportedMCPConfig(t *testing.T) { assert.Empty(t, result) }) - t.Run("http server inferred from url field", func(t *testing.T) { + t.Run("server without type is skipped", func(t *testing.T) { tmpDir := t.TempDir() mcpFile := filepath.Join(tmpDir, "mcp.json") - // No explicit type, but has url - should be inferred as http mcpContent := `{ "mcpServers": { - "inferred-http": { - "url": "https://example.com/mcp" + "no-type-server": { + "command": "/usr/bin/test" } } }` err := os.WriteFile(mcpFile, []byte(mcpContent), 0o644) require.NoError(t, err) - initResp := &acp.InitializeResponse{ - AgentCapabilities: acp.AgentCapabilities{ - McpCapabilities: acp.McpCapabilities{ - Http: true, - }, - }, - } + initResp := &acp.InitializeResponse{} result, err := getSupportedMCPConfig(mcpFile, logger, initResp) require.NoError(t, err) - assert.Len(t, result, 1) - assert.NotNil(t, result[0].Http) - assert.Equal(t, "inferred-http", result[0].Http.Name) + // Servers without type are skipped with a warning + assert.Empty(t, result) }) } @@ -318,4 +218,14 @@ func TestConvertAgentapiMcpToAcp(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "unsupported server type") }) + + t.Run("returns error for missing type", func(t *testing.T) { + server := AgentapiMcpServer{ + Command: "/usr/bin/test", + } + + _, err := server.convertAgentapiMcpToAcp("no-type-server") + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported server type") + }) }