From e9b0a2e708abcf88a7fdb60f5c05ecf6be14ff79 Mon Sep 17 00:00:00 2001 From: Reidho Satria Date: Fri, 14 Aug 2026 08:34:22 +0700 Subject: [PATCH] fix(mcp): connect servers after dashboard install --- internal/server/handlers_hub.go | 7 +- internal/server/handlers_mcp.go | 11 +-- internal/server/handlers_subsystems.go | 21 ++++-- internal/server/mcp_refresh_test.go | 93 +++++++++++++++++++++++++- internal/server/server.go | 25 +++---- 5 files changed, 120 insertions(+), 37 deletions(-) diff --git a/internal/server/handlers_hub.go b/internal/server/handlers_hub.go index b9312a2..77abc39 100644 --- a/internal/server/handlers_hub.go +++ b/internal/server/handlers_hub.go @@ -1,7 +1,6 @@ package server import ( - "context" "errors" "net/http" "strings" @@ -159,10 +158,6 @@ func (s *Server) handleHubInstallMCP(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusInternalServerError, err) return } - if s.mcp != nil { - // Connecting can take seconds while npx downloads a package; the - // request must not wait on it, and neither must its context. - go s.mcp.Connect(context.Background(), s.config()) - } + s.refreshMCPConnections(r.Context()) writeJSON(w, http.StatusOK, map[string]any{"ok": true, "missing_keys": missing}) } diff --git a/internal/server/handlers_mcp.go b/internal/server/handlers_mcp.go index 2ced510..84e3d90 100644 --- a/internal/server/handlers_mcp.go +++ b/internal/server/handlers_mcp.go @@ -1,7 +1,6 @@ package server import ( - "context" "errors" "net/http" "strings" @@ -89,11 +88,7 @@ func (s *Server) handleAddMCPServer(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusInternalServerError, err) return } - if s.mcp != nil { - // Connecting can take seconds while npx downloads a package; do it off - // the request so the form returns promptly. - go s.mcp.Connect(context.Background(), s.config()) - } + s.refreshMCPConnections(r.Context()) writeJSON(w, http.StatusOK, map[string]any{"ok": true, "name": name}) } @@ -121,9 +116,7 @@ func (s *Server) handleDeleteMCPServer(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusInternalServerError, err) return } - if s.mcp != nil { - go s.mcp.Connect(context.Background(), s.config()) - } + s.refreshMCPConnections(r.Context()) writeJSON(w, http.StatusOK, map[string]any{"ok": true}) } diff --git a/internal/server/handlers_subsystems.go b/internal/server/handlers_subsystems.go index f1a6466..11001a0 100644 --- a/internal/server/handlers_subsystems.go +++ b/internal/server/handlers_subsystems.go @@ -366,20 +366,27 @@ func (s *Server) handleMCPStatus(w http.ResponseWriter, r *http.Request) { } func (s *Server) handleMCPRefresh(w http.ResponseWriter, r *http.Request) { - // Check the concrete pointer before boxing it: a nil *mcp.Manager inside a - // non-nil mcpRefresher interface would slip past the nil guard in refreshMCP - // and panic on the first field access in Refresh. - if s.mcp == nil { - s.refreshMCP(w, r, nil) - return + refresher := s.mcpRefresh + if refresher == nil { + refresher = s.mcp } - s.refreshMCP(w, r, s.mcp) + s.refreshMCP(w, r, refresher) } type mcpRefresher interface { Refresh(context.Context, *config.Config) []mcp.ServerStatus } +func (s *Server) refreshMCPConnections(ctx context.Context) { + refresher := s.mcpRefresh + if refresher == nil { + refresher = s.mcp + } + if refresher != nil { + refresher.Refresh(ctx, s.config()) + } +} + func (s *Server) refreshMCP(w http.ResponseWriter, r *http.Request, refresher mcpRefresher) { cfg := s.config() if refresher == nil || !cfg.MCP.Enabled { diff --git a/internal/server/mcp_refresh_test.go b/internal/server/mcp_refresh_test.go index 3c56347..7d85f25 100644 --- a/internal/server/mcp_refresh_test.go +++ b/internal/server/mcp_refresh_test.go @@ -3,12 +3,13 @@ package server import ( "context" "encoding/json" + "github.com/enowdev/antares/internal/agent" + "github.com/enowdev/antares/internal/config" + "github.com/enowdev/antares/internal/mcp" "net/http" "net/http/httptest" + "strings" "testing" - - "github.com/enowdev/antares/internal/config" - "github.com/enowdev/antares/internal/mcp" ) type fakeMCPRefresher struct { @@ -25,6 +26,92 @@ func (f *fakeMCPRefresher) Refresh(context.Context, *config.Config) []mcp.Server }} } +type fakeMCPManager struct { + refreshed bool + cfg *config.Config +} + +func (f *fakeMCPManager) Refresh(_ context.Context, cfg *config.Config) []mcp.ServerStatus { + f.refreshed = true + f.cfg = cfg + return nil +} + +func (f *fakeMCPManager) Status(*config.Config) []mcp.ServerStatus { return nil } + +func newMCPMutationServer(t *testing.T) (*Server, *fakeMCPManager) { + t.Helper() + t.Setenv("ANTARES_HOME", t.TempDir()) + cfg := config.Default() + cfg.Server.DashboardPasswordHash = "test-hash" + if err := config.Save(cfg); err != nil { + t.Fatalf("seed config: %v", err) + } + manager := &fakeMCPManager{} + return &Server{cfg: cfg, agent: &agent.Agent{}, mcpRefresh: manager, reloadFn: func() error { return nil }}, manager +} + +func TestAddMCPServerRefreshesManager(t *testing.T) { + s, manager := newMCPMutationServer(t) + req := httptest.NewRequest(http.MethodPost, "/api/mcp/servers", strings.NewReader(`{"name":"manual","transport":"http","url":"http://127.0.0.1:8080/mcp"}`)) + w := httptest.NewRecorder() + + s.handleAddMCPServer(w, req) + if w.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) + } + if !manager.refreshed { + t.Fatal("adding a server did not refresh MCP connections") + } + if _, ok := manager.cfg.MCP.Servers["manual"]; !ok { + t.Fatalf("refresh config = %+v, want manually added server", manager.cfg.MCP.Servers) + } +} + +func TestHubInstallMCPRefreshesManager(t *testing.T) { + s, manager := newMCPMutationServer(t) + req := httptest.NewRequest(http.MethodPost, "/api/hub/mcp/install", strings.NewReader(`{"id":"filesystem"}`)) + w := httptest.NewRecorder() + + s.handleHubInstallMCP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) + } + if !manager.refreshed { + t.Fatal("installing a hub server did not refresh MCP connections") + } + if _, ok := manager.cfg.MCP.Servers["filesystem"]; !ok { + t.Fatalf("refresh config = %+v, want installed hub server", manager.cfg.MCP.Servers) + } +} + +func TestDeleteMCPServerRefreshesManager(t *testing.T) { + s, manager := newMCPMutationServer(t) + cfg := s.cfg + cfg.MCP.Enabled = true + cfg.MCP.Servers = map[string]config.MCPServer{ + "remove-me": {Transport: "http", URL: "http://127.0.0.1:8080/mcp", Enabled: true}, + } + if err := config.Save(cfg); err != nil { + t.Fatalf("seed configured server: %v", err) + } + s.cfg = cfg + req := httptest.NewRequest(http.MethodDelete, "/api/mcp/servers/remove-me", nil) + req.SetPathValue("name", "remove-me") + w := httptest.NewRecorder() + + s.handleDeleteMCPServer(w, req) + if w.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) + } + if !manager.refreshed { + t.Fatal("deleting a server did not refresh MCP connections") + } + if _, ok := manager.cfg.MCP.Servers["remove-me"]; ok { + t.Fatal("refresh config still contains deleted server") + } +} + func TestMCPRefreshHandlerReturnsFreshStatus(t *testing.T) { s := &Server{cfg: &config.Config{MCP: config.MCP{Enabled: true}}} refresher := &fakeMCPRefresher{} diff --git a/internal/server/server.go b/internal/server/server.go index ede1e12..9dcaedd 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -32,18 +32,19 @@ import ( // Server wires the API handlers to the agent and store. type Server struct { - cfg *config.Config - agent *agent.Agent - db store.Store - skills *skills.Manager - cron *cron.Runner - gateway *gateway.Manager - mcp *mcp.Manager - social *socialbrowser.Manager - mux *http.ServeMux - hub *liveHub - wake *wakeQueue - started time.Time + cfg *config.Config + agent *agent.Agent + db store.Store + skills *skills.Manager + cron *cron.Runner + gateway *gateway.Manager + mcp *mcp.Manager + mcpRefresh mcpRefresher + social *socialbrowser.Manager + mux *http.ServeMux + hub *liveHub + wake *wakeQueue + started time.Time // distFS holds the embedded dashboard build, when present. distFS fs.FS