-
-
Notifications
You must be signed in to change notification settings - Fork 80
Expand file tree
/
Copy pathclient_format_test.go
More file actions
144 lines (135 loc) · 4.73 KB
/
Copy pathclient_format_test.go
File metadata and controls
144 lines (135 loc) · 4.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package mcp_server //nolint:testpackage,revive // exercise unexported formatter
import (
"encoding/json"
"strings"
"testing"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func TestFormatToolResult_PrefersStructuredContent(t *testing.T) {
res := &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "# rendered markdown\n\nfoo: bar\n"}},
StructuredContent: map[string]any{
"version": "1.2.3",
"is_read_only": false,
},
}
out, err := formatToolResult("server_info", res, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(out, `"version":"1.2.3"`) || !strings.Contains(out, `"is_read_only":false`) {
t.Errorf("expected JSON serialisation of structured content, got %q", out)
}
if strings.Contains(out, "rendered markdown") {
t.Errorf("rendered text should not leak into stdout: %q", out)
}
}
func TestFormatToolResult_PreferTextReturnsTextContent(t *testing.T) {
// `"prefer_text": true` in the client config must surface the rendered
// text content even when a structured payload is present (issue #669).
res := &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: `{"rows":[{"name":"google"}]}`}},
StructuredContent: map[string]any{
"rows": []any{map[string]any{"name": "google", "extra": "structured-only"}},
},
}
out, err := formatToolResult("list_providers", res, true)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if out != `{"rows":[{"name":"google"}]}` {
t.Errorf("expected text content verbatim, got %q", out)
}
}
func TestFormatToolResult_FallsBackToTextWhenNoStructured(t *testing.T) {
res := &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "plain output"}},
}
out, err := formatToolResult("anything", res, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if out != "plain output" {
t.Errorf("expected fallback text, got %q", out)
}
}
func TestFormatToolResult_IsErrorReturnsErrorWithTextPayload(t *testing.T) {
res := &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{&mcp.TextContent{Text: "tool 'run_mutation_query' refused: server is read-only"}},
}
_, err := formatToolResult("run_mutation_query", res, false)
if err == nil {
t.Fatal("expected error for IsError result")
}
if !strings.Contains(err.Error(), "read-only") {
t.Errorf("expected refusal text in error, got %q", err.Error())
}
}
func TestFormatToolResult_NilResult(t *testing.T) {
_, err := formatToolResult("anything", nil, false)
if err == nil {
t.Fatal("expected error for nil result")
}
}
func TestFormatToolResult_EmbeddedJSONStringInValueRoundtripsCleanly(t *testing.T) {
// describe_method returns rows whose `shape` field is itself a JSON-encoded
// string. Robot scenarios feed our stdout to json.loads, so anything that
// gets the escaping wrong (eg interpolating into a single-quoted Python
// source literal) will explode. Verify formatToolResult's output is
// itself round-trippable through Go's json.Unmarshal -- the strict-mode
// equivalent of what Robot does in its Parse MCP JSON Output helper.
res := &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "ignored markdown"}},
StructuredContent: map[string]any{
"rows": []any{
map[string]any{
"name": "routingConfig",
"shape": `{"description":"A routing config.","type":"object"}`,
},
},
},
}
out, err := formatToolResult("describe_method", res, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var decoded map[string]any
if err := json.Unmarshal([]byte(out), &decoded); err != nil {
t.Fatalf("client output is not valid JSON: %v\noutput: %s", err, out)
}
rows, ok := decoded["rows"].([]any)
if !ok || len(rows) != 1 {
t.Fatalf("expected one row, got %#v", decoded["rows"])
}
first, ok := rows[0].(map[string]any)
if !ok {
t.Fatalf("row is not a map: %#v", rows[0])
}
if first["shape"] != `{"description":"A routing config.","type":"object"}` {
t.Errorf("shape did not survive round-trip: %#v", first["shape"])
}
}
func TestFormatToolResult_StructuredContentAsArray(t *testing.T) {
// SDK may surface structured content as either a typed value (server side)
// or a generic decoded JSON value (client side). Verify both shape families
// round-trip cleanly.
res := &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: "ignored"}},
StructuredContent: map[string]any{
"rows": []any{
map[string]any{"name": "google"},
map[string]any{"name": "aws"},
},
},
}
out, err := formatToolResult("list_providers", res, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for _, want := range []string{`"rows":`, `"name":"google"`, `"name":"aws"`} {
if !strings.Contains(out, want) {
t.Errorf("expected %q in output, got %q", want, out)
}
}
}