package mcp import ( "encoding/json" "strings" "testing" "github.com/google/uuid" "github.com/modelcontextprotocol/go-sdk/mcp" ) func TestAnnotateJSONResult(t *testing.T) { // valid JSON array → wrapped with __renderer + data result := textResult(`[{"slug": "host:hubris", "type": "host"}]`) annotated := annotateJSONResult(result, "entity_card") if len(annotated.Content) != 1 { t.Fatalf("expected 1 content item, got %d", len(annotated.Content)) } tc, ok := annotated.Content[0].(*mcp.TextContent) if !ok { t.Fatal("content is not TextContent") } var wrapper map[string]interface{} if err := json.Unmarshal([]byte(tc.Text), &wrapper); err != nil { t.Fatalf("result is not valid JSON: %v", err) } if wrapper["__renderer"] != "entity_card" { t.Errorf("__renderer = %q, want entity_card", wrapper["__renderer"]) } data, ok := wrapper["data"].([]interface{}) if !ok || len(data) != 1 { t.Fatal("data is not the original array") } } func TestAnnotateJSONResultNoop(t *testing.T) { // empty content → no-op result := &mcp.CallToolResult{Content: []mcp.Content{}} annotated := annotateJSONResult(result, "entity_card") if len(annotated.Content) != 0 { t.Fatal("empty content should be unchanged") } // non-JSON text → no-op (not wrapped) result = textResult("just plain text") annotated = annotateJSONResult(result, "entity_card") tc, _ := annotated.Content[0].(*mcp.TextContent) if strings.Contains(tc.Text, "__renderer") { t.Fatal("non-JSON content should not be annotated") } // textResult with empty string → no-op result = textResult("") annotated = annotateJSONResult(result, "entity_card") tc, _ = annotated.Content[0].(*mcp.TextContent) if tc.Text != "" { t.Fatal("empty text content should be unchanged") } } func TestAnnotateJSONResultPreservesMultipleRows(t *testing.T) { result := textResult(`[{"slug": "a"}, {"slug": "b"}, {"slug": "c"}]`) annotated := annotateJSONResult(result, "lxc_list") tc, _ := annotated.Content[0].(*mcp.TextContent) var wrapper map[string]interface{} json.Unmarshal([]byte(tc.Text), &wrapper) data := wrapper["data"].([]interface{}) if len(data) != 3 { t.Fatalf("expected 3 rows in data, got %d", len(data)) } } // TestNewServerRegistersTools verifies every tool registers with a valid // input schema. The MCP SDK panics at AddTool if a tool omits its object // input schema, so merely constructing the server exercises that contract — // this test would have caught the "missing input schema" panic. func TestNewServerRegistersTools(t *testing.T) { defer func() { if r := recover(); r != nil { t.Fatalf("newServer panicked (tool schema bug?): %v", r) } }() // pool is only used inside tool handlers (invoked per-call), not at // registration time, so a nil pool is safe for this construction test. s := newServer(nil, uuid.Nil) if s == nil { t.Fatal("newServer returned nil") } } func TestObjSchema(t *testing.T) { s := objSchema(prop{"foo", "string", "a foo"}, prop{"n", "integer", "a number"}) if s.Type != "object" { t.Errorf("schema type = %q, want object", s.Type) } if len(s.Properties) != 2 { t.Fatalf("got %d properties, want 2", len(s.Properties)) } if s.Properties["foo"].Type != "string" || s.Properties["n"].Type != "integer" { t.Errorf("property types wrong: %+v", s.Properties) } // empty schema still valid (object with no properties) if objSchema().Type != "object" { t.Error("empty objSchema not an object") } }