package adminapi import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" ) func adminMCP(t *testing.T, a *API, method string, params any) map[string]any { t.Helper() b, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": method, "params": params}) rec := httptest.NewRecorder() a.MCP(rec, httptest.NewRequest(http.MethodPost, "/api/v1/mcp", bytes.NewReader(b))) var resp struct { Result map[string]any `json:"result"` } if rec.Code != 200 || json.Unmarshal(rec.Body.Bytes(), &resp) != nil { t.Fatalf("%s: %d %s", method, rec.Code, rec.Body.String()) } return resp.Result } func TestTheClusterIsAReadOnlyMCPServer(t *testing.T) { a, _ := setup(t, testModels()) if init := adminMCP(t, a, "initialize", map[string]any{"protocolVersion": "2025-06-18"}); init["protocolVersion"] != "2025-06-18" { t.Errorf("initialize %v", init) } tools := adminMCP(t, a, "tools/list", map[string]any{})["tools"].([]any) var names []string for _, tl := range tools { m := tl.(map[string]any) names = append(names, m["name"].(string)) if m["annotations"].(map[string]any)["readOnlyHint"] != true { t.Errorf("%s is not marked read only", m["name"]) } } if got := strings.Join(names, ","); got != "cluster_status,list_nodes,list_models,list_replicas,explain_placement,node_logs" { t.Errorf("tools %s", got) } // Each answers what the API would. for _, name := range []string{"cluster_status", "list_nodes", "list_models", "list_replicas"} { res := adminMCP(t, a, "tools/call", map[string]any{"name": name, "arguments": map[string]any{}}) text := res["content"].([]any)[0].(map[string]any)["text"].(string) if res["isError"] == true || !json.Valid([]byte(text)) { t.Errorf("%s: %v", name, res) } } if res := adminMCP(t, a, "tools/call", map[string]any{"name": "explain_placement", "arguments": map[string]any{}}); res["isError"] != true { t.Errorf("explain_placement without a model: %v", res) } if res := adminMCP(t, a, "tools/call", map[string]any{"name": "remove_model", "arguments": map[string]any{}}); res["isError"] != true { t.Errorf("a tool that would change something: %v", res) } } // The gateway reaches it in this process, as llmgw:admin. func TestTheClustersOwnMCPServerIsReachedInProcess(t *testing.T) { a, _ := setup(t, testModels()) c := &http.Client{Transport: InProcess(http.HandlerFunc(a.MCP))} b, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}) resp, err := c.Post(InProcessURL, "application/json", bytes.NewReader(b)) if err != nil || resp.StatusCode != 200 { t.Fatalf("%v %v", resp, err) } defer resp.Body.Close() var out struct { Result struct { Tools []any `json:"tools"` } `json:"result"` } if json.NewDecoder(resp.Body).Decode(&out) != nil || len(out.Result.Tools) != 6 { t.Errorf("tools %+v", out) } }