Change list_alerts (MCP tool) and alerts (CLI command) to show only active (non-silenced, non-inhibited) alerts by default. Add state=all option and --all CLI flag to show all alerts when needed. - MCP: list_alerts with no state param now returns active alerts only - MCP: list_alerts with state=all returns all alerts (previous default) - CLI: alerts command defaults to active, --all shows everything - Add tests for new default behavior and state=all option - Update README with new CLI examples - Bump version to 0.3.0 - Clarify version bumping rules in CLAUDE.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
660 lines
18 KiB
Go
660 lines
18 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.t-juice.club/torjus/labmcp/internal/mcp"
|
|
)
|
|
|
|
// setupTestServer creates a test MCP server with monitoring handlers backed by test HTTP servers.
|
|
func setupTestServer(t *testing.T, promHandler, amHandler http.HandlerFunc, lokiHandler ...http.HandlerFunc) (*mcp.Server, func()) {
|
|
t.Helper()
|
|
|
|
promSrv := httptest.NewServer(promHandler)
|
|
amSrv := httptest.NewServer(amHandler)
|
|
|
|
logger := log.New(io.Discard, "", 0)
|
|
config := mcp.DefaultMonitoringConfig()
|
|
server := mcp.NewGenericServer(logger, config)
|
|
|
|
prom := NewPrometheusClient(promSrv.URL)
|
|
am := NewAlertmanagerClient(amSrv.URL)
|
|
|
|
var loki *LokiClient
|
|
var lokiSrv *httptest.Server
|
|
if len(lokiHandler) > 0 && lokiHandler[0] != nil {
|
|
lokiSrv = httptest.NewServer(lokiHandler[0])
|
|
loki = NewLokiClient(lokiSrv.URL)
|
|
}
|
|
|
|
RegisterHandlers(server, prom, am, loki, HandlerOptions{EnableSilences: true})
|
|
|
|
cleanup := func() {
|
|
promSrv.Close()
|
|
amSrv.Close()
|
|
if lokiSrv != nil {
|
|
lokiSrv.Close()
|
|
}
|
|
}
|
|
|
|
return server, cleanup
|
|
}
|
|
|
|
func TestHandler_ListAlerts(t *testing.T) {
|
|
server, cleanup := setupTestServer(t,
|
|
nil,
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`[
|
|
{
|
|
"annotations": {"summary": "Node is down"},
|
|
"endsAt": "2024-01-01T01:00:00Z",
|
|
"fingerprint": "fp1",
|
|
"receivers": [{"name": "default"}],
|
|
"startsAt": "2024-01-01T00:00:00Z",
|
|
"status": {"inhibitedBy": [], "silencedBy": [], "state": "active"},
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"generatorURL": "",
|
|
"labels": {"alertname": "NodeDown", "severity": "critical"}
|
|
}
|
|
]`))
|
|
},
|
|
)
|
|
defer cleanup()
|
|
|
|
result := callTool(t, server, "list_alerts", map[string]interface{}{})
|
|
if result.IsError {
|
|
t.Fatalf("unexpected error: %s", result.Content[0].Text)
|
|
}
|
|
if !strings.Contains(result.Content[0].Text, "NodeDown") {
|
|
t.Errorf("expected output to contain 'NodeDown', got: %s", result.Content[0].Text)
|
|
}
|
|
if !strings.Contains(result.Content[0].Text, "1 alert") {
|
|
t.Errorf("expected output to contain '1 alert', got: %s", result.Content[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestHandler_ListAlertsDefaultsToActive(t *testing.T) {
|
|
// Test that list_alerts with no state param defaults to active filters
|
|
server, cleanup := setupTestServer(t,
|
|
nil,
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
// Default should apply active filters
|
|
if q.Get("active") != "true" {
|
|
t.Errorf("expected default active=true, got %s", q.Get("active"))
|
|
}
|
|
if q.Get("silenced") != "false" {
|
|
t.Errorf("expected default silenced=false, got %s", q.Get("silenced"))
|
|
}
|
|
if q.Get("inhibited") != "false" {
|
|
t.Errorf("expected default inhibited=false, got %s", q.Get("inhibited"))
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`[]`))
|
|
},
|
|
)
|
|
defer cleanup()
|
|
|
|
result := callTool(t, server, "list_alerts", map[string]interface{}{})
|
|
if result.IsError {
|
|
t.Fatalf("unexpected error: %s", result.Content[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestHandler_ListAlertsStateAll(t *testing.T) {
|
|
// Test that list_alerts with state=all applies no filters
|
|
server, cleanup := setupTestServer(t,
|
|
nil,
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
// state=all should not set any filter params
|
|
if q.Get("active") != "" {
|
|
t.Errorf("expected no active param for state=all, got %s", q.Get("active"))
|
|
}
|
|
if q.Get("silenced") != "" {
|
|
t.Errorf("expected no silenced param for state=all, got %s", q.Get("silenced"))
|
|
}
|
|
if q.Get("inhibited") != "" {
|
|
t.Errorf("expected no inhibited param for state=all, got %s", q.Get("inhibited"))
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`[
|
|
{
|
|
"annotations": {},
|
|
"endsAt": "2024-01-01T01:00:00Z",
|
|
"fingerprint": "fp1",
|
|
"receivers": [{"name": "default"}],
|
|
"startsAt": "2024-01-01T00:00:00Z",
|
|
"status": {"inhibitedBy": [], "silencedBy": [], "state": "active"},
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"generatorURL": "",
|
|
"labels": {"alertname": "ActiveAlert", "severity": "critical"}
|
|
},
|
|
{
|
|
"annotations": {},
|
|
"endsAt": "2024-01-01T01:00:00Z",
|
|
"fingerprint": "fp2",
|
|
"receivers": [{"name": "default"}],
|
|
"startsAt": "2024-01-01T00:00:00Z",
|
|
"status": {"inhibitedBy": [], "silencedBy": ["s1"], "state": "suppressed"},
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"generatorURL": "",
|
|
"labels": {"alertname": "SilencedAlert", "severity": "warning"}
|
|
}
|
|
]`))
|
|
},
|
|
)
|
|
defer cleanup()
|
|
|
|
result := callTool(t, server, "list_alerts", map[string]interface{}{
|
|
"state": "all",
|
|
})
|
|
if result.IsError {
|
|
t.Fatalf("unexpected error: %s", result.Content[0].Text)
|
|
}
|
|
if !strings.Contains(result.Content[0].Text, "2 alert") {
|
|
t.Errorf("expected output to contain '2 alert', got: %s", result.Content[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestHandler_GetAlert(t *testing.T) {
|
|
server, cleanup := setupTestServer(t,
|
|
nil,
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`[
|
|
{
|
|
"annotations": {"summary": "Found it"},
|
|
"endsAt": "2024-01-01T01:00:00Z",
|
|
"fingerprint": "target-fp",
|
|
"receivers": [{"name": "default"}],
|
|
"startsAt": "2024-01-01T00:00:00Z",
|
|
"status": {"inhibitedBy": [], "silencedBy": [], "state": "active"},
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"generatorURL": "",
|
|
"labels": {"alertname": "TestAlert", "severity": "warning"}
|
|
},
|
|
{
|
|
"annotations": {},
|
|
"endsAt": "2024-01-01T01:00:00Z",
|
|
"fingerprint": "other-fp",
|
|
"receivers": [{"name": "default"}],
|
|
"startsAt": "2024-01-01T00:00:00Z",
|
|
"status": {"inhibitedBy": [], "silencedBy": [], "state": "active"},
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"generatorURL": "",
|
|
"labels": {"alertname": "OtherAlert", "severity": "info"}
|
|
}
|
|
]`))
|
|
},
|
|
)
|
|
defer cleanup()
|
|
|
|
result := callTool(t, server, "get_alert", map[string]interface{}{
|
|
"fingerprint": "target-fp",
|
|
})
|
|
if result.IsError {
|
|
t.Fatalf("unexpected error: %s", result.Content[0].Text)
|
|
}
|
|
if !strings.Contains(result.Content[0].Text, "TestAlert") {
|
|
t.Errorf("expected output to contain 'TestAlert', got: %s", result.Content[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestHandler_GetAlertNotFound(t *testing.T) {
|
|
server, cleanup := setupTestServer(t,
|
|
nil,
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`[]`))
|
|
},
|
|
)
|
|
defer cleanup()
|
|
|
|
result := callTool(t, server, "get_alert", map[string]interface{}{
|
|
"fingerprint": "nonexistent",
|
|
})
|
|
if !result.IsError {
|
|
t.Error("expected error result for nonexistent fingerprint")
|
|
}
|
|
}
|
|
|
|
func TestHandler_Query(t *testing.T) {
|
|
server, cleanup := setupTestServer(t,
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/query" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"status": "success",
|
|
"data": {
|
|
"resultType": "vector",
|
|
"result": [
|
|
{
|
|
"metric": {"__name__": "up", "job": "node"},
|
|
"value": [1234567890, "1"]
|
|
}
|
|
]
|
|
}
|
|
}`))
|
|
},
|
|
nil,
|
|
)
|
|
defer cleanup()
|
|
|
|
result := callTool(t, server, "query", map[string]interface{}{
|
|
"promql": "up",
|
|
})
|
|
if result.IsError {
|
|
t.Fatalf("unexpected error: %s", result.Content[0].Text)
|
|
}
|
|
if !strings.Contains(result.Content[0].Text, "node") {
|
|
t.Errorf("expected output to contain 'node', got: %s", result.Content[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestHandler_ListTargets(t *testing.T) {
|
|
server, cleanup := setupTestServer(t,
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/targets" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"status": "success",
|
|
"data": {
|
|
"activeTargets": [
|
|
{
|
|
"labels": {"instance": "localhost:9090", "job": "prometheus"},
|
|
"scrapePool": "prometheus",
|
|
"scrapeUrl": "http://localhost:9090/metrics",
|
|
"globalUrl": "http://localhost:9090/metrics",
|
|
"lastError": "",
|
|
"lastScrape": "2024-01-01T00:00:00Z",
|
|
"lastScrapeDuration": 0.015,
|
|
"health": "up",
|
|
"scrapeInterval": "15s",
|
|
"scrapeTimeout": "10s"
|
|
}
|
|
],
|
|
"droppedTargets": []
|
|
}
|
|
}`))
|
|
},
|
|
nil,
|
|
)
|
|
defer cleanup()
|
|
|
|
result := callTool(t, server, "list_targets", map[string]interface{}{})
|
|
if result.IsError {
|
|
t.Fatalf("unexpected error: %s", result.Content[0].Text)
|
|
}
|
|
if !strings.Contains(result.Content[0].Text, "prometheus") {
|
|
t.Errorf("expected output to contain 'prometheus', got: %s", result.Content[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestHandler_SearchMetrics(t *testing.T) {
|
|
server, cleanup := setupTestServer(t,
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch r.URL.Path {
|
|
case "/api/v1/label/__name__/values":
|
|
_, _ = w.Write([]byte(`{
|
|
"status": "success",
|
|
"data": ["node_cpu_seconds_total", "node_memory_MemTotal_bytes", "up"]
|
|
}`))
|
|
case "/api/v1/metadata":
|
|
_, _ = w.Write([]byte(`{
|
|
"status": "success",
|
|
"data": {
|
|
"node_cpu_seconds_total": [{"type": "counter", "help": "CPU time", "unit": ""}],
|
|
"node_memory_MemTotal_bytes": [{"type": "gauge", "help": "Total memory", "unit": "bytes"}]
|
|
}
|
|
}`))
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
},
|
|
nil,
|
|
)
|
|
defer cleanup()
|
|
|
|
result := callTool(t, server, "search_metrics", map[string]interface{}{
|
|
"query": "node",
|
|
})
|
|
if result.IsError {
|
|
t.Fatalf("unexpected error: %s", result.Content[0].Text)
|
|
}
|
|
if !strings.Contains(result.Content[0].Text, "node_cpu") {
|
|
t.Errorf("expected output to contain 'node_cpu', got: %s", result.Content[0].Text)
|
|
}
|
|
// "up" should be filtered out since it doesn't match "node"
|
|
if strings.Contains(result.Content[0].Text, "| up |") {
|
|
t.Errorf("expected 'up' to be filtered out, got: %s", result.Content[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestHandler_ListSilences(t *testing.T) {
|
|
server, cleanup := setupTestServer(t,
|
|
nil,
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v2/silences" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`[
|
|
{
|
|
"id": "s1",
|
|
"matchers": [{"name": "alertname", "value": "Test", "isRegex": false}],
|
|
"startsAt": "2024-01-01T00:00:00Z",
|
|
"endsAt": "2024-01-01T02:00:00Z",
|
|
"createdBy": "admin",
|
|
"comment": "Testing",
|
|
"status": {"state": "active"}
|
|
},
|
|
{
|
|
"id": "s2",
|
|
"matchers": [{"name": "job", "value": "node", "isRegex": false}],
|
|
"startsAt": "2023-01-01T00:00:00Z",
|
|
"endsAt": "2023-01-01T02:00:00Z",
|
|
"createdBy": "admin",
|
|
"comment": "Old",
|
|
"status": {"state": "expired"}
|
|
}
|
|
]`))
|
|
},
|
|
)
|
|
defer cleanup()
|
|
|
|
result := callTool(t, server, "list_silences", map[string]interface{}{})
|
|
if result.IsError {
|
|
t.Fatalf("unexpected error: %s", result.Content[0].Text)
|
|
}
|
|
// Should show active silence but filter out expired
|
|
if !strings.Contains(result.Content[0].Text, "s1") {
|
|
t.Errorf("expected active silence s1 in output, got: %s", result.Content[0].Text)
|
|
}
|
|
if strings.Contains(result.Content[0].Text, "s2") {
|
|
t.Errorf("expected expired silence s2 to be filtered out, got: %s", result.Content[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestHandler_ToolCount(t *testing.T) {
|
|
server, cleanup := setupTestServer(t,
|
|
func(w http.ResponseWriter, r *http.Request) {},
|
|
func(w http.ResponseWriter, r *http.Request) {},
|
|
)
|
|
defer cleanup()
|
|
|
|
tools := listTools(t, server)
|
|
// Without Loki: 7 base + 1 silence = 8
|
|
if len(tools) != 8 {
|
|
t.Errorf("expected 8 tools with silences enabled (no Loki), got %d", len(tools))
|
|
for _, tool := range tools {
|
|
t.Logf(" tool: %s", tool.Name)
|
|
}
|
|
}
|
|
|
|
// Verify create_silence is present
|
|
found := false
|
|
for _, tool := range tools {
|
|
if tool.Name == "create_silence" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("expected create_silence tool when silences enabled")
|
|
}
|
|
}
|
|
|
|
func TestHandler_ToolCountWithLoki(t *testing.T) {
|
|
server, cleanup := setupTestServer(t,
|
|
func(w http.ResponseWriter, r *http.Request) {},
|
|
func(w http.ResponseWriter, r *http.Request) {},
|
|
func(w http.ResponseWriter, r *http.Request) {},
|
|
)
|
|
defer cleanup()
|
|
|
|
tools := listTools(t, server)
|
|
// With Loki: 7 base + 1 silence + 3 loki = 11
|
|
if len(tools) != 11 {
|
|
t.Errorf("expected 11 tools with silences and Loki enabled, got %d", len(tools))
|
|
for _, tool := range tools {
|
|
t.Logf(" tool: %s", tool.Name)
|
|
}
|
|
}
|
|
|
|
// Verify Loki tools are present
|
|
lokiTools := map[string]bool{"query_logs": false, "list_labels": false, "list_label_values": false}
|
|
for _, tool := range tools {
|
|
if _, ok := lokiTools[tool.Name]; ok {
|
|
lokiTools[tool.Name] = true
|
|
}
|
|
}
|
|
for name, found := range lokiTools {
|
|
if !found {
|
|
t.Errorf("expected %s tool when Loki enabled", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandler_ToolCountWithoutSilences(t *testing.T) {
|
|
promSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
|
amSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
|
defer promSrv.Close()
|
|
defer amSrv.Close()
|
|
|
|
logger := log.New(io.Discard, "", 0)
|
|
config := mcp.DefaultMonitoringConfig()
|
|
server := mcp.NewGenericServer(logger, config)
|
|
|
|
prom := NewPrometheusClient(promSrv.URL)
|
|
am := NewAlertmanagerClient(amSrv.URL)
|
|
RegisterHandlers(server, prom, am, nil, HandlerOptions{EnableSilences: false})
|
|
|
|
tools := listTools(t, server)
|
|
if len(tools) != 7 {
|
|
t.Errorf("expected 7 tools without silences, got %d", len(tools))
|
|
for _, tool := range tools {
|
|
t.Logf(" tool: %s", tool.Name)
|
|
}
|
|
}
|
|
|
|
// Verify create_silence is NOT present
|
|
for _, tool := range tools {
|
|
if tool.Name == "create_silence" {
|
|
t.Error("expected create_silence tool to be absent when silences disabled")
|
|
}
|
|
}
|
|
}
|
|
|
|
func listTools(t *testing.T, server *mcp.Server) []mcp.Tool {
|
|
t.Helper()
|
|
|
|
req := &mcp.Request{
|
|
JSONRPC: "2.0",
|
|
ID: 1,
|
|
Method: "tools/list",
|
|
}
|
|
resp := server.HandleRequest(context.Background(), req)
|
|
if resp == nil {
|
|
t.Fatal("expected response, got nil")
|
|
}
|
|
if resp.Error != nil {
|
|
t.Fatalf("unexpected error: %s", resp.Error.Message)
|
|
}
|
|
|
|
resultJSON, err := json.Marshal(resp.Result)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal result: %v", err)
|
|
}
|
|
|
|
var listResult mcp.ListToolsResult
|
|
if err := json.Unmarshal(resultJSON, &listResult); err != nil {
|
|
t.Fatalf("failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
return listResult.Tools
|
|
}
|
|
|
|
func TestHandler_QueryLogs(t *testing.T) {
|
|
server, cleanup := setupTestServer(t,
|
|
nil,
|
|
nil,
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/loki/api/v1/query_range" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"status": "success",
|
|
"data": {
|
|
"resultType": "streams",
|
|
"result": [
|
|
{
|
|
"stream": {"job": "varlogs", "filename": "/var/log/syslog"},
|
|
"values": [
|
|
["1704067200000000000", "Jan 1 00:00:00 host kernel: test message"]
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}`))
|
|
},
|
|
)
|
|
defer cleanup()
|
|
|
|
result := callTool(t, server, "query_logs", map[string]interface{}{
|
|
"logql": `{job="varlogs"}`,
|
|
})
|
|
if result.IsError {
|
|
t.Fatalf("unexpected error: %s", result.Content[0].Text)
|
|
}
|
|
if !strings.Contains(result.Content[0].Text, "varlogs") {
|
|
t.Errorf("expected output to contain 'varlogs', got: %s", result.Content[0].Text)
|
|
}
|
|
if !strings.Contains(result.Content[0].Text, "test message") {
|
|
t.Errorf("expected output to contain 'test message', got: %s", result.Content[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestHandler_ListLabels(t *testing.T) {
|
|
server, cleanup := setupTestServer(t,
|
|
nil,
|
|
nil,
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/loki/api/v1/labels" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"status": "success",
|
|
"data": ["job", "instance", "filename"]
|
|
}`))
|
|
},
|
|
)
|
|
defer cleanup()
|
|
|
|
result := callTool(t, server, "list_labels", map[string]interface{}{})
|
|
if result.IsError {
|
|
t.Fatalf("unexpected error: %s", result.Content[0].Text)
|
|
}
|
|
if !strings.Contains(result.Content[0].Text, "3 label") {
|
|
t.Errorf("expected output to contain '3 label', got: %s", result.Content[0].Text)
|
|
}
|
|
if !strings.Contains(result.Content[0].Text, "job") {
|
|
t.Errorf("expected output to contain 'job', got: %s", result.Content[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestHandler_ListLabelValues(t *testing.T) {
|
|
server, cleanup := setupTestServer(t,
|
|
nil,
|
|
nil,
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/loki/api/v1/label/job/values" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{
|
|
"status": "success",
|
|
"data": ["varlogs", "nginx", "systemd"]
|
|
}`))
|
|
},
|
|
)
|
|
defer cleanup()
|
|
|
|
result := callTool(t, server, "list_label_values", map[string]interface{}{
|
|
"label": "job",
|
|
})
|
|
if result.IsError {
|
|
t.Fatalf("unexpected error: %s", result.Content[0].Text)
|
|
}
|
|
if !strings.Contains(result.Content[0].Text, "3 value") {
|
|
t.Errorf("expected output to contain '3 value', got: %s", result.Content[0].Text)
|
|
}
|
|
if !strings.Contains(result.Content[0].Text, "nginx") {
|
|
t.Errorf("expected output to contain 'nginx', got: %s", result.Content[0].Text)
|
|
}
|
|
}
|
|
|
|
// callTool is a test helper that calls a tool through the MCP server.
|
|
func callTool(t *testing.T, server *mcp.Server, name string, args map[string]interface{}) mcp.CallToolResult {
|
|
t.Helper()
|
|
|
|
params := mcp.CallToolParams{
|
|
Name: name,
|
|
Arguments: args,
|
|
}
|
|
paramsJSON, err := json.Marshal(params)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal params: %v", err)
|
|
}
|
|
|
|
req := &mcp.Request{
|
|
JSONRPC: "2.0",
|
|
ID: 1,
|
|
Method: "tools/call",
|
|
Params: paramsJSON,
|
|
}
|
|
|
|
resp := server.HandleRequest(context.Background(), req)
|
|
if resp == nil {
|
|
t.Fatal("expected response, got nil")
|
|
}
|
|
if resp.Error != nil {
|
|
t.Fatalf("JSON-RPC error: %s", resp.Error.Message)
|
|
}
|
|
|
|
resultJSON, err := json.Marshal(resp.Result)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal result: %v", err)
|
|
}
|
|
|
|
var result mcp.CallToolResult
|
|
if err := json.Unmarshal(resultJSON, &result); err != nil {
|
|
t.Fatalf("failed to unmarshal result: %v", err)
|
|
}
|
|
|
|
return result
|
|
}
|