Skip to content

Commit a5799c4

Browse files
author
Suresh Sivasankaran
committed
Improved MCP server request and response handling with enhanced message formatting
1 parent 16c40ef commit a5799c4

File tree

7 files changed

+926
-55
lines changed

7 files changed

+926
-55
lines changed

cli/src/ui/messages/extension/SayMessageRouter.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
SayUserEditTodosMessage,
2424
SayImageMessage,
2525
SayMcpServerRequestStartedMessage,
26+
SayMcpServerResponseMessage,
2627
SayApiReqFinishedMessage,
2728
SayApiReqRetryDelayedMessage,
2829
SayCommandOutputMessage,
@@ -108,6 +109,9 @@ export const SayMessageRouter: React.FC<MessageComponentProps> = ({ message }) =
108109
case "mcp_server_request_started":
109110
return <SayMcpServerRequestStartedMessage message={message} />
110111

112+
case "mcp_server_response":
113+
return <SayMcpServerResponseMessage message={message} />
114+
111115
case "api_req_finished":
112116
return <SayApiReqFinishedMessage message={message} />
113117

Lines changed: 372 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
import { describe, it, expect } from "vitest"
2+
import {
3+
formatJson,
4+
formatContentWithMetadata,
5+
buildMetadataString,
6+
formatByteSize,
7+
isMcpServerData,
8+
parseMcpServerData,
9+
} from "../utils.js"
10+
import type { ExtensionChatMessage } from "../../../../types/messages.js"
11+
12+
describe("formatJson", () => {
13+
it("should format valid JSON with indentation", () => {
14+
const input = '{"key":"value","nested":{"data":123}}'
15+
const expected = JSON.stringify(JSON.parse(input), null, 2)
16+
expect(formatJson(input)).toBe(expected)
17+
})
18+
19+
it("should use custom indentation", () => {
20+
const input = '{"key":"value"}'
21+
const expected = JSON.stringify(JSON.parse(input), null, 4)
22+
expect(formatJson(input, 4)).toBe(expected)
23+
})
24+
25+
it("should return null for invalid JSON", () => {
26+
expect(formatJson("plain text")).toBe(null)
27+
expect(formatJson("{invalid}")).toBe(null)
28+
expect(formatJson("")).toBe(null)
29+
})
30+
31+
it("should handle arrays", () => {
32+
const input = '["item1","item2"]'
33+
const expected = JSON.stringify(JSON.parse(input), null, 2)
34+
expect(formatJson(input)).toBe(expected)
35+
})
36+
})
37+
38+
describe("formatContentWithMetadata", () => {
39+
it("should detect and format JSON content", () => {
40+
const json = '{"key":"value"}'
41+
const result = formatContentWithMetadata(json, 20, 5)
42+
43+
expect(result.isJson).toBe(true)
44+
expect(result.content).toContain("key")
45+
expect(result.content).toContain("value")
46+
expect(result.isPreview).toBe(false)
47+
})
48+
49+
it("should handle plain text content", () => {
50+
const text = "Plain text content"
51+
const result = formatContentWithMetadata(text, 20, 5)
52+
53+
expect(result.isJson).toBe(false)
54+
expect(result.content).toBe(text)
55+
expect(result.lineCount).toBe(1)
56+
expect(result.isPreview).toBe(false)
57+
})
58+
59+
it("should create preview for long content", () => {
60+
const lines = Array(30)
61+
.fill(null)
62+
.map((_, i) => `Line ${i + 1}`)
63+
.join("\n")
64+
const result = formatContentWithMetadata(lines, 20, 5)
65+
66+
expect(result.isPreview).toBe(true)
67+
expect(result.hiddenLines).toBe(25) // 30 - 5 = 25
68+
expect(result.lineCount).toBe(30)
69+
expect(result.content).toContain("Line 1")
70+
expect(result.content).not.toContain("Line 30")
71+
})
72+
73+
it("should calculate metadata correctly", () => {
74+
const text = "Hello\nWorld"
75+
const result = formatContentWithMetadata(text, 20, 5)
76+
77+
expect(result.lineCount).toBe(2)
78+
expect(result.charCount).toBe(11)
79+
expect(result.byteSize).toBeGreaterThan(0)
80+
})
81+
82+
it("should handle empty string", () => {
83+
const result = formatContentWithMetadata("", 20, 5)
84+
85+
expect(result.content).toBe("")
86+
expect(result.lineCount).toBe(0)
87+
expect(result.charCount).toBe(0)
88+
expect(result.byteSize).toBe(0)
89+
expect(result.isPreview).toBe(false)
90+
})
91+
92+
it("should handle content at threshold boundary", () => {
93+
const lines = Array(20)
94+
.fill(null)
95+
.map((_, i) => `Line ${i + 1}`)
96+
.join("\n")
97+
const result = formatContentWithMetadata(lines, 20, 5)
98+
99+
expect(result.isPreview).toBe(false)
100+
expect(result.lineCount).toBe(20)
101+
})
102+
103+
it("should handle content just over threshold", () => {
104+
const lines = Array(21)
105+
.fill(null)
106+
.map((_, i) => `Line ${i + 1}`)
107+
.join("\n")
108+
const result = formatContentWithMetadata(lines, 20, 5)
109+
110+
expect(result.isPreview).toBe(true)
111+
expect(result.hiddenLines).toBe(16) // 21 - 5 = 16
112+
})
113+
})
114+
115+
describe("formatByteSize", () => {
116+
it("should format bytes", () => {
117+
expect(formatByteSize(100)).toBe("100 B")
118+
expect(formatByteSize(1023)).toBe("1023 B")
119+
})
120+
121+
it("should format kilobytes", () => {
122+
expect(formatByteSize(1024)).toBe("1.0 KB")
123+
expect(formatByteSize(2048)).toBe("2.0 KB")
124+
expect(formatByteSize(1536)).toBe("1.5 KB")
125+
})
126+
127+
it("should format megabytes", () => {
128+
expect(formatByteSize(1024 * 1024)).toBe("1.0 MB")
129+
expect(formatByteSize(2.5 * 1024 * 1024)).toBe("2.5 MB")
130+
expect(formatByteSize(10 * 1024 * 1024)).toBe("10.0 MB")
131+
})
132+
133+
it("should round to one decimal place", () => {
134+
expect(formatByteSize(1536)).toBe("1.5 KB")
135+
expect(formatByteSize(1587)).toBe("1.5 KB") // Should round down
136+
expect(formatByteSize(1638)).toBe("1.6 KB") // Should round up
137+
})
138+
})
139+
140+
describe("buildMetadataString", () => {
141+
it("should build metadata for JSON content", () => {
142+
const metadata = {
143+
isJson: true,
144+
content: "{}",
145+
lineCount: 5,
146+
charCount: 100,
147+
byteSize: 100,
148+
isPreview: false,
149+
hiddenLines: 0,
150+
}
151+
152+
const result = buildMetadataString(metadata)
153+
expect(result).toContain("JSON")
154+
expect(result).toContain("5 lines")
155+
expect(result).not.toContain("B") // Under 1KB
156+
})
157+
158+
it("should build metadata for text content", () => {
159+
const metadata = {
160+
isJson: false,
161+
content: "text",
162+
lineCount: 1,
163+
charCount: 50,
164+
byteSize: 50,
165+
isPreview: false,
166+
hiddenLines: 0,
167+
}
168+
169+
const result = buildMetadataString(metadata)
170+
expect(result).toContain("Text")
171+
expect(result).toContain("1 line")
172+
expect(result).not.toContain("lines") // Singular
173+
})
174+
175+
it("should include byte size for large content", () => {
176+
const metadata = {
177+
isJson: false,
178+
content: "text",
179+
lineCount: 10,
180+
charCount: 5000,
181+
byteSize: 5000,
182+
isPreview: false,
183+
hiddenLines: 0,
184+
}
185+
186+
const result = buildMetadataString(metadata)
187+
expect(result).toContain("10 lines")
188+
expect(result).toContain("KB")
189+
})
190+
191+
it("should handle plural lines correctly", () => {
192+
const singleLine = {
193+
isJson: false,
194+
content: "text",
195+
lineCount: 1,
196+
charCount: 10,
197+
byteSize: 10,
198+
isPreview: false,
199+
hiddenLines: 0,
200+
}
201+
202+
const multiLine = {
203+
isJson: false,
204+
content: "text",
205+
lineCount: 2,
206+
charCount: 20,
207+
byteSize: 20,
208+
isPreview: false,
209+
hiddenLines: 0,
210+
}
211+
212+
expect(buildMetadataString(singleLine)).toContain("1 line")
213+
expect(buildMetadataString(multiLine)).toContain("2 lines")
214+
})
215+
216+
it("should format complete metadata string correctly", () => {
217+
const metadata = {
218+
isJson: true,
219+
content: "{}",
220+
lineCount: 26,
221+
charCount: 11469,
222+
byteSize: 11469,
223+
isPreview: false,
224+
hiddenLines: 0,
225+
}
226+
227+
const result = buildMetadataString(metadata)
228+
expect(result).toBe("JSON, 26 lines, 11.2 KB")
229+
})
230+
})
231+
232+
describe("isMcpServerData", () => {
233+
it("should validate valid MCP tool use data", () => {
234+
const valid = {
235+
type: "use_mcp_tool",
236+
serverName: "filesystem",
237+
toolName: "read_file",
238+
arguments: '{"path": "/test"}',
239+
}
240+
expect(isMcpServerData(valid)).toBe(true)
241+
})
242+
243+
it("should validate valid MCP resource access data", () => {
244+
const valid = {
245+
type: "access_mcp_resource",
246+
serverName: "database",
247+
uri: "db://users",
248+
}
249+
expect(isMcpServerData(valid)).toBe(true)
250+
})
251+
252+
it("should reject invalid type", () => {
253+
const invalid = {
254+
type: "invalid_type",
255+
serverName: "test",
256+
}
257+
expect(isMcpServerData(invalid)).toBe(false)
258+
})
259+
260+
it("should reject missing serverName", () => {
261+
const invalid = {
262+
type: "use_mcp_tool",
263+
toolName: "test",
264+
}
265+
expect(isMcpServerData(invalid)).toBe(false)
266+
})
267+
268+
it("should reject non-string serverName", () => {
269+
const invalid = {
270+
type: "use_mcp_tool",
271+
serverName: 123,
272+
}
273+
expect(isMcpServerData(invalid)).toBe(false)
274+
})
275+
276+
it("should reject non-string toolName", () => {
277+
const invalid = {
278+
type: "use_mcp_tool",
279+
serverName: "test",
280+
toolName: 123,
281+
}
282+
expect(isMcpServerData(invalid)).toBe(false)
283+
})
284+
285+
it("should reject non-string arguments", () => {
286+
const invalid = {
287+
type: "use_mcp_tool",
288+
serverName: "test",
289+
arguments: { key: "value" },
290+
}
291+
expect(isMcpServerData(invalid)).toBe(false)
292+
})
293+
294+
it("should accept minimal valid data", () => {
295+
const valid = {
296+
type: "use_mcp_tool",
297+
serverName: "test",
298+
}
299+
expect(isMcpServerData(valid)).toBe(true)
300+
})
301+
302+
it("should reject null or undefined", () => {
303+
expect(isMcpServerData(null)).toBe(false)
304+
expect(isMcpServerData(undefined)).toBe(false)
305+
})
306+
307+
it("should reject non-object values", () => {
308+
expect(isMcpServerData("string")).toBe(false)
309+
expect(isMcpServerData(123)).toBe(false)
310+
expect(isMcpServerData(true)).toBe(false)
311+
})
312+
})
313+
314+
describe("parseMcpServerData", () => {
315+
it("should parse valid MCP server data", () => {
316+
const message: ExtensionChatMessage = {
317+
type: "ask",
318+
ask: "use_mcp_server",
319+
text: JSON.stringify({
320+
type: "use_mcp_tool",
321+
serverName: "filesystem",
322+
toolName: "read_file",
323+
}),
324+
ts: Date.now(),
325+
}
326+
327+
const result = parseMcpServerData(message)
328+
expect(result).toBeTruthy()
329+
expect(result?.type).toBe("use_mcp_tool")
330+
expect(result?.serverName).toBe("filesystem")
331+
expect(result?.toolName).toBe("read_file")
332+
})
333+
334+
it("should return null for invalid data", () => {
335+
const message: ExtensionChatMessage = {
336+
type: "ask",
337+
ask: "use_mcp_server",
338+
text: JSON.stringify({
339+
type: "invalid",
340+
name: "test",
341+
}),
342+
ts: Date.now(),
343+
}
344+
345+
const result = parseMcpServerData(message)
346+
expect(result).toBe(null)
347+
})
348+
349+
it("should return null for non-JSON text", () => {
350+
const message: ExtensionChatMessage = {
351+
type: "ask",
352+
ask: "use_mcp_server",
353+
text: "plain text",
354+
ts: Date.now(),
355+
}
356+
357+
const result = parseMcpServerData(message)
358+
expect(result).toBe(null)
359+
})
360+
361+
it("should return null for empty text", () => {
362+
const message: ExtensionChatMessage = {
363+
type: "ask",
364+
ask: "use_mcp_server",
365+
text: "",
366+
ts: Date.now(),
367+
}
368+
369+
const result = parseMcpServerData(message)
370+
expect(result).toBe(null)
371+
})
372+
})

0 commit comments

Comments
 (0)