feat(mcp): operators can switch MCP (and OAuth) off

Admin switch in the system settings, MCP_ENABLED fixes it. Off: the
endpoint answers 404, OAuth is off, the API key settings hide the MCP tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-01 21:11:53 +01:00
1 parent 39a04082fc
commit c6f28aa816
17 files changed
+197 -30

No files matched your search

+67
View File
@@ -288,3 +288,70 @@ func TestMCPCreateDeleteAndOrigin(t *testing.T) {
t.Fatalf("webhook:task key tools: %s", names)
}
}
// Operators can switch MCP off: in the admin settings, or fixed by MCP_ENABLED. OAuth goes with it.
func TestMCPSwitchOff(t *testing.T) {
srv, _, session := setupTestServer(t)
srv.cfg.BaseURL = "https://ms.example/"
srv.cfg.BaseURLSet = true
router := srv.Router()
const list = `{"jsonrpc":"2.0","id":1,"method":"tools/list"}`
status := func() map[string]interface{} {
_, _, st := oauthDo(t, router, http.MethodGet, "/api/v1/auth/status", "", "", "")
return st
}
if code, _ := rpc(t, router, session, list); code != http.StatusOK {
t.Fatalf("MCP should be on by default, got %d", code)
}
if st := status(); st["mcpEnabled"] != true || st["mcpConnectorUrl"] == "" {
t.Fatalf("status with MCP on: %v", st)
}
if code, _, _ := oauthDo(t, router, http.MethodPut, "/api/v1/admin/settings", session, "application/json", `{"mcpEnabled":false}`); code != http.StatusOK {
t.Fatalf("switch off: %d", code)
}
if code, _ := rpc(t, router, session, list); code != http.StatusNotFound {
t.Fatalf("MCP switched off must answer 404, got %d", code)
}
code, h, _ := oauthDo(t, router, http.MethodPost, mcpPath, "", "application/json", list)
if code != http.StatusNotFound || h.Get("WWW-Authenticate") != "" {
t.Fatalf("no login hint while MCP is off: %d %q", code, h.Get("WWW-Authenticate"))
}
if code, _, _ := oauthDo(t, router, http.MethodGet, "/.well-known/oauth-authorization-server", "", "", ""); code != http.StatusNotFound {
t.Fatalf("OAuth must be off with MCP, got %d", code)
}
if st := status(); st["mcpEnabled"] != false || st["mcpConnectorUrl"] != "" {
t.Fatalf("status with MCP off: %v", st)
}
// Saving other settings (without the field) leaves the switch as it is
if code, _, _ := oauthDo(t, router, http.MethodPut, "/api/v1/admin/settings", session, "application/json", `{"allowPrivateWebhooks":true}`); code != http.StatusOK {
t.Fatalf("save other settings: %d", code)
}
if code, _ := rpc(t, router, session, list); code != http.StatusNotFound {
t.Fatalf("MCP must stay off, got %d", code)
}
// MCP_ENABLED wins over the stored switch and locks it
on := true
srv.cfg.MCPEnabled = &on
if code, _ := rpc(t, router, session, list); code != http.StatusOK {
t.Fatalf("MCP_ENABLED=true must win, got %d", code)
}
_, _, adm := oauthDo(t, router, http.MethodGet, "/api/v1/admin/settings", session, "", "")
if adm["mcpEnabled"] != true || adm["mcpConfiguredViaEnv"] != true {
t.Fatalf("admin settings must show the environment's value: %v", adm)
}
off := false
srv.cfg.MCPEnabled = &off
if code, _, _ := oauthDo(t, router, http.MethodPut, "/api/v1/admin/settings", session, "application/json", `{"mcpEnabled":true}`); code != http.StatusOK {
t.Fatalf("save: %d", code)
}
if code, _ := rpc(t, router, session, list); code != http.StatusNotFound {
t.Fatalf("MCP_ENABLED=false must win, got %d", code)
}
srv.cfg.MCPEnabled = nil
if code, _ := rpc(t, router, session, list); code != http.StatusNotFound {
t.Fatalf("the locked switch must not have changed the stored value, got %d", code)
}
}