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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user