fix(backend): pass HTTP MCP servers to ACP agents via AssumeMcpHttp (#1152)

* fix(backend): pass HTTP MCP servers to ACP agents via AssumeMcpHttp

* test(backend): cover AssumeMcpHttp override path with effectiveMcpCapabilities helper
This commit is contained in:
José Almeida
2026-05-29 12:58:51 +01:00
committed by GitHub
parent 96057e25f4
commit b109c84e4e
15 changed files with 93 additions and 9 deletions
@@ -180,6 +180,7 @@ type RuntimeConfig struct {
ModelFlag Param // e.g. NewParam("--model", "{model}")
WorkspaceFlag string // e.g. "--workspace-root"
AssumeMcpSse bool // Override: assume agent supports SSE MCP servers even if not advertised
AssumeMcpHttp bool // Override: assume agent supports HTTP MCP servers even if not advertised
ProjectSkillDir string // CWD-relative path for project-level skills (e.g. ".claude/skills")
UserSkillDir string // home-relative path for user-level skills (e.g. ".claude/skills")
}
@@ -101,6 +101,7 @@ func (a *Auggie) Runtime() *RuntimeConfig {
Protocol: agent.ProtocolACP,
WorkspaceFlag: "--workspace-root",
AssumeMcpSse: true,
AssumeMcpHttp: true,
ProjectSkillDir: ".agents/skills",
SessionConfig: SessionConfig{
NativeSessionResume: true,
@@ -52,6 +52,7 @@ type CreateInstanceRequest struct {
TaskID string `json:"task_id,omitempty"` // Task ID for MCP plan tool calls (server-side injection)
DisableAskQuestion bool `json:"disable_ask_question,omitempty"` // Disable ask_user_question MCP tool (TUI agents)
AssumeMcpSse bool `json:"assume_mcp_sse,omitempty"` // Assume agent supports SSE MCP servers
AssumeMcpHttp bool `json:"assume_mcp_http,omitempty"` // Assume agent supports HTTP MCP servers
McpMode string `json:"mcp_mode,omitempty"` // MCP tool mode: "task" (default) or "config"
}
@@ -202,9 +202,11 @@ func (cm *ContainerManager) createInstanceAndClient(
}
disableAskQuestion := agents.IsPassthroughOnly(config.AgentConfig)
assumeMcpSse := false
assumeMcpHttp := false
if config.AgentConfig != nil {
if rt := config.AgentConfig.Runtime(); rt != nil {
assumeMcpSse = rt.AssumeMcpSse
assumeMcpHttp = rt.AssumeMcpHttp
}
}
@@ -219,6 +221,7 @@ func (cm *ContainerManager) createInstanceAndClient(
SessionID: config.SessionID,
DisableAskQuestion: disableAskQuestion,
AssumeMcpSse: assumeMcpSse,
AssumeMcpHttp: assumeMcpHttp,
McpMode: config.McpMode,
}
@@ -443,11 +443,13 @@ func buildReconnectCreateInstanceRequest(req *ExecutorCreateRequest, instanceID
agentType := ""
disableAskQuestion := false
assumeMcpSse := false
assumeMcpHttp := false
if req.AgentConfig != nil {
agentType = req.AgentConfig.ID()
disableAskQuestion = agents.IsPassthroughOnly(req.AgentConfig)
if rt := req.AgentConfig.Runtime(); rt != nil {
assumeMcpSse = rt.AssumeMcpSse
assumeMcpHttp = rt.AssumeMcpHttp
}
}
return &agentctl.CreateInstanceRequest{
@@ -461,6 +463,7 @@ func buildReconnectCreateInstanceRequest(req *ExecutorCreateRequest, instanceID
TaskID: req.TaskID,
DisableAskQuestion: disableAskQuestion,
AssumeMcpSse: assumeMcpSse,
AssumeMcpHttp: assumeMcpHttp,
McpMode: req.McpMode,
}
}
@@ -515,7 +515,8 @@ func TestBuildReconnectCreateInstanceRequest(t *testing.T) {
id: "codex",
enabled: true,
runtimeConfig: &agents.RuntimeConfig{
AssumeMcpSse: true,
AssumeMcpSse: true,
AssumeMcpHttp: true,
},
},
Env: map[string]string{
@@ -547,6 +548,9 @@ func TestBuildReconnectCreateInstanceRequest(t *testing.T) {
if !got.AssumeMcpSse {
t.Fatal("expected AssumeMcpSse to be propagated")
}
if !got.AssumeMcpHttp {
t.Fatal("expected AssumeMcpHttp to be propagated")
}
if got.McpMode != "config" {
t.Fatalf("McpMode = %q, want config", got.McpMode)
}
@@ -96,9 +96,11 @@ func (r *StandaloneExecutor) CreateInstance(ctx context.Context, req *ExecutorCr
}
disableAskQuestion := agents.IsPassthroughOnly(req.AgentConfig)
assumeMcpSse := false
assumeMcpHttp := false
if req.AgentConfig != nil {
if rt := req.AgentConfig.Runtime(); rt != nil {
assumeMcpSse = rt.AssumeMcpSse
assumeMcpHttp = rt.AssumeMcpHttp
}
}
@@ -115,6 +117,7 @@ func (r *StandaloneExecutor) CreateInstance(ctx context.Context, req *ExecutorCr
TaskID: req.TaskID,
DisableAskQuestion: disableAskQuestion,
AssumeMcpSse: assumeMcpSse,
AssumeMcpHttp: assumeMcpHttp,
McpMode: req.McpMode,
}
@@ -267,6 +267,9 @@ type Config struct {
// AssumeMcpSse overrides MCP capability filtering to assume SSE support.
AssumeMcpSse bool
// AssumeMcpHttp overrides MCP capability filtering to assume HTTP support.
AssumeMcpHttp bool
}
// ToSharedConfig converts this Config to the shared.Config used by transport adapters.
@@ -296,5 +299,6 @@ func (c *Config) ToSharedConfig() *shared.Config {
Headers: c.Headers,
Extra: c.Extra,
AssumeMcpSse: c.AssumeMcpSse,
AssumeMcpHttp: c.AssumeMcpHttp,
}
}
@@ -36,10 +36,7 @@ func (a *Adapter) NewSession(ctx context.Context, mcpServers []types.McpServer)
ctx, span := shared.TraceProtocolRequest(ctx, shared.ProtocolACP, a.agentID, "session.new")
defer span.End()
caps := a.capabilities.McpCapabilities
if a.cfg.AssumeMcpSse {
caps.Sse = true
}
caps := effectiveMcpCapabilities(a.capabilities.McpCapabilities, a.cfg)
filteredServers := filterMcpServersByCapabilities(mcpServers, caps, a.logger)
resp, err := conn.NewSession(ctx, acp.NewSessionRequest{
Cwd: a.cfg.WorkDir,
@@ -90,6 +87,24 @@ func (a *Adapter) NewSession(ctx context.Context, mcpServers []types.McpServer)
return sessionID, nil
}
// effectiveMcpCapabilities applies the adapter's AssumeMcpSse/AssumeMcpHttp
// config overrides on top of the capabilities advertised by the agent during
// the ACP handshake. Some agents (e.g. Auggie) support SSE/HTTP MCP transports
// but don't advertise the corresponding capability, which would otherwise cause
// filterMcpServersByCapabilities to drop user-configured remote MCP servers.
func effectiveMcpCapabilities(caps acp.McpCapabilities, cfg *shared.Config) acp.McpCapabilities {
if cfg == nil {
return caps
}
if cfg.AssumeMcpSse {
caps.Sse = true
}
if cfg.AssumeMcpHttp {
caps.Http = true
}
return caps
}
// filterMcpServersByCapabilities removes MCP servers that the agent doesn't support.
// Stdio servers are always allowed; SSE/HTTP servers require the corresponding capability.
// If multiple servers share the same name (e.g., dual SSE+HTTP injection), only the first
@@ -225,10 +240,7 @@ func (a *Adapter) LoadSession(ctx context.Context, sessionID string, mcpServers
defer span.End()
// Filter MCP servers by agent capabilities (same logic as NewSession).
caps := a.capabilities.McpCapabilities
if a.cfg.AssumeMcpSse {
caps.Sse = true
}
caps := effectiveMcpCapabilities(a.capabilities.McpCapabilities, a.cfg)
filteredServers := filterMcpServersByCapabilities(mcpServers, caps, a.logger)
// Suppress history replay notifications during load.
@@ -4,6 +4,7 @@ import (
"testing"
"github.com/coder/acp-go-sdk"
"github.com/kandev/kandev/internal/agentctl/server/adapter/transport/shared"
"github.com/kandev/kandev/internal/agentctl/types"
"github.com/kandev/kandev/internal/common/logger"
)
@@ -390,3 +391,39 @@ func TestFilterMcpServersByCapabilities_DeduplicatesSameName(t *testing.T) {
func newTestLoggerForMcp() *logger.Logger {
return newTestAdapter().logger
}
// --- effectiveMcpCapabilities ---
func TestEffectiveMcpCapabilities_AssumeHTTPAndSSE(t *testing.T) {
// Agent advertises neither SSE nor HTTP.
base := acp.McpCapabilities{Sse: false, Http: false}
// Nil config leaves capabilities untouched.
if got := effectiveMcpCapabilities(base, nil); got.Sse || got.Http {
t.Errorf("nil cfg should not alter caps, got %+v", got)
}
// AssumeMcpHttp forces Http on (and an http server then survives the filter).
caps := effectiveMcpCapabilities(base, &shared.Config{AssumeMcpHttp: true})
if !caps.Http {
t.Fatal("AssumeMcpHttp should force caps.Http = true")
}
if caps.Sse {
t.Error("AssumeMcpHttp should not affect caps.Sse")
}
log := newTestLoggerForMcp()
httpServer := []types.McpServer{{Name: "remote", Type: "http", URL: "https://mcp.example.com/mcp"}}
if got := filterMcpServersByCapabilities(httpServer, caps, log); len(got) != 1 {
t.Fatalf("http server should survive when AssumeMcpHttp set, got %d servers", len(got))
}
// AssumeMcpSse forces Sse on independently.
caps = effectiveMcpCapabilities(base, &shared.Config{AssumeMcpSse: true})
if !caps.Sse {
t.Fatal("AssumeMcpSse should force caps.Sse = true")
}
if caps.Http {
t.Error("AssumeMcpSse should not affect caps.Http")
}
}
@@ -49,6 +49,9 @@ type Config struct {
// AssumeMcpSse overrides MCP capability filtering to assume SSE support.
AssumeMcpSse bool
// AssumeMcpHttp overrides MCP capability filtering to assume HTTP support.
AssumeMcpHttp bool
}
// GetPermissionTimeout returns the configured permission timeout or the default.
@@ -192,6 +192,9 @@ type InstanceConfig struct {
// AssumeMcpSse overrides MCP capability filtering to assume the agent supports SSE.
AssumeMcpSse bool
// AssumeMcpHttp overrides MCP capability filtering to assume the agent supports HTTP.
AssumeMcpHttp bool
// McpMode controls which MCP tools are registered for this instance.
// "task" (default) registers kanban/plan tools; "config" registers config tools.
McpMode string
@@ -348,6 +351,9 @@ func applyOverrides(cfg *InstanceConfig, overrides *InstanceOverrides) {
if overrides.AssumeMcpSse {
cfg.AssumeMcpSse = true
}
if overrides.AssumeMcpHttp {
cfg.AssumeMcpHttp = true
}
if overrides.McpMode != "" {
cfg.McpMode = overrides.McpMode
}
@@ -368,6 +374,7 @@ type InstanceOverrides struct {
TaskID string
DisableAskQuestion bool
AssumeMcpSse bool
AssumeMcpHttp bool
McpMode string
}
@@ -95,6 +95,9 @@ type CreateRequest struct {
// AssumeMcpSse overrides MCP capability filtering to assume SSE support.
AssumeMcpSse bool `json:"assume_mcp_sse,omitempty"`
// AssumeMcpHttp overrides MCP capability filtering to assume HTTP support.
AssumeMcpHttp bool `json:"assume_mcp_http,omitempty"`
// McpMode controls which MCP tools are registered: "task" (default) or "config".
McpMode string `json:"mcp_mode,omitempty"`
}
@@ -93,6 +93,7 @@ func (m *Manager) CreateInstance(ctx context.Context, req *CreateRequest) (*Crea
TaskID: req.TaskID,
DisableAskQuestion: req.DisableAskQuestion,
AssumeMcpSse: req.AssumeMcpSse,
AssumeMcpHttp: req.AssumeMcpHttp,
McpMode: req.McpMode,
}
@@ -634,6 +634,7 @@ func (m *Manager) buildAdapterConfig() error {
McpServers: mcpServers,
AgentID: m.cfg.AgentType, // From registry (e.g., "auggie", "amp", "claude-code")
AssumeMcpSse: m.cfg.AssumeMcpSse,
AssumeMcpHttp: m.cfg.AssumeMcpHttp,
}
// Configure one-shot mode when a continue command is provided.