Skip to content

Commit 98b091b

Browse files
committed
feat(mcp): improve mcp with McpExecution, make UX like terminal execute
Implements a new UI component and status tracking system for MCP tool executions, providing users with feedback on tool execution progress and results. - Add McpExecutionStatus schema with started/output/completed/error states - Enhance useMcpToolTool to send status updates at different execution stages - Create new McpExecution component with status indicators and expandable sections - Refactor ChatRow to use the new component for better visualization - Update CodeAccordian to support custom headers for arguments/response sections This change improves the user experience by providing clear visual feedback during MCP tool execution, similar to the existing command execution experience.
1 parent 8447670 commit 98b091b

File tree

8 files changed

+639
-121
lines changed

8 files changed

+639
-121
lines changed

src/core/tools/useMcpToolTool.ts

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Task } from "../task/Task"
22
import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
33
import { formatResponse } from "../prompts/responses"
44
import { ClineAskUseMcpServer } from "../../shared/ExtensionMessage"
5+
import { McpExecutionStatus } from "../../schemas"
56

67
export async function useMcpToolTool(
78
cline: Task,
@@ -67,6 +68,7 @@ export async function useMcpToolTool(
6768
arguments: mcp_arguments,
6869
} satisfies ClineAskUseMcpServer)
6970

71+
const executionId = cline.lastMessageTs?.toString() ?? Date.now().toString()
7072
const didApprove = await askApproval("use_mcp_server", completeMessage)
7173

7274
if (!didApprove) {
@@ -76,15 +78,31 @@ export async function useMcpToolTool(
7678
// Now execute the tool
7779
await cline.say("mcp_server_request_started") // same as browser_action_result
7880

81+
// Send started status
82+
const clineProvider = await cline.providerRef.deref()
83+
const startedStatus: McpExecutionStatus = {
84+
executionId,
85+
status: "started",
86+
serverName: server_name,
87+
toolName: tool_name,
88+
}
89+
clineProvider?.postMessageToWebview({
90+
type: "mcpExecutionStatus",
91+
text: JSON.stringify(startedStatus),
92+
})
93+
7994
const toolResult = await cline.providerRef
8095
.deref()
8196
?.getMcpHub()
8297
?.callTool(server_name, tool_name, parsedArguments)
8398

84-
// TODO: add progress indicator and ability to parse images and non-text responses
85-
const toolResultPretty =
86-
(toolResult?.isError ? "Error:\n" : "") +
87-
toolResult?.content
99+
// Process the result
100+
let toolResultPretty = "(No response)"
101+
102+
if (toolResult) {
103+
// Send output status if there's content
104+
if (toolResult.content && toolResult.content.length > 0) {
105+
const outputText = toolResult.content
88106
.map((item) => {
89107
if (item.type === "text") {
90108
return item.text
@@ -96,7 +114,48 @@ export async function useMcpToolTool(
96114
return ""
97115
})
98116
.filter(Boolean)
99-
.join("\n\n") || "(No response)"
117+
.join("\n\n")
118+
119+
if (outputText) {
120+
const outputStatus: McpExecutionStatus = {
121+
executionId,
122+
status: "output",
123+
response: outputText,
124+
}
125+
clineProvider?.postMessageToWebview({
126+
type: "mcpExecutionStatus",
127+
text: JSON.stringify(outputStatus),
128+
})
129+
130+
toolResultPretty = (toolResult.isError ? "Error:\n" : "") + outputText
131+
}
132+
}
133+
134+
// Send completed or error status
135+
const completedStatus: McpExecutionStatus = {
136+
executionId,
137+
status: toolResult.isError ? "error" : "completed",
138+
response: toolResultPretty,
139+
error: toolResult.isError ? "Error executing MCP tool" : undefined,
140+
}
141+
clineProvider?.postMessageToWebview({
142+
type: "mcpExecutionStatus",
143+
text: JSON.stringify(completedStatus),
144+
})
145+
} else {
146+
// Send error status if no result
147+
const errorStatus: McpExecutionStatus = {
148+
executionId,
149+
status: "error",
150+
error: "No response from MCP server",
151+
}
152+
clineProvider?.postMessageToWebview({
153+
type: "mcpExecutionStatus",
154+
text: JSON.stringify(errorStatus),
155+
})
156+
157+
toolResultPretty = "(No response)"
158+
}
100159

101160
await cline.say("mcp_server_response", toolResultPretty)
102161
pushToolResult(formatResponse.toolResult(toolResultPretty))

src/schemas/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,36 @@ export const commandExecutionStatusSchema = z.discriminatedUnion("status", [
308308

309309
export type CommandExecutionStatus = z.infer<typeof commandExecutionStatusSchema>
310310

311+
/**
312+
* McpExecutionStatus
313+
*/
314+
315+
export const mcpExecutionStatusSchema = z.discriminatedUnion("status", [
316+
z.object({
317+
executionId: z.string(),
318+
status: z.literal("started"),
319+
serverName: z.string(),
320+
toolName: z.string(),
321+
}),
322+
z.object({
323+
executionId: z.string(),
324+
status: z.literal("output"),
325+
response: z.string(),
326+
}),
327+
z.object({
328+
executionId: z.string(),
329+
status: z.literal("completed"),
330+
response: z.string().optional(),
331+
}),
332+
z.object({
333+
executionId: z.string(),
334+
status: z.literal("error"),
335+
error: z.string().optional(),
336+
}),
337+
])
338+
339+
export type McpExecutionStatus = z.infer<typeof mcpExecutionStatusSchema>
340+
311341
/**
312342
* ExperimentId
313343
*/

src/shared/ExtensionMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export interface ExtensionMessage {
6868
| "acceptInput"
6969
| "setHistoryPreviewCollapsed"
7070
| "commandExecutionStatus"
71+
| "mcpExecutionStatus"
7172
| "vsCodeSetting"
7273
text?: string
7374
action?:
@@ -275,6 +276,7 @@ export interface ClineAskUseMcpServer {
275276
toolName?: string
276277
arguments?: string
277278
uri?: string
279+
response?: string
278280
}
279281

280282
export interface ClineApiReqInfo {
Lines changed: 177 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,183 @@
1-
// npx jest src/shared/__tests__/combineCommandSequences.test.ts
2-
1+
import { combineCommandSequences } from "../combineCommandSequences"
32
import { ClineMessage } from "../ExtensionMessage"
43

5-
import { combineCommandSequences } from "../combineCommandSequences"
4+
describe("combineCommandSequences", () => {
5+
describe("command sequences", () => {
6+
it("should combine command and command_output messages", () => {
7+
const messages: ClineMessage[] = [
8+
{ type: "ask", ask: "command", text: "ls", ts: 1625097600000 },
9+
{ type: "ask", ask: "command_output", text: "file1.txt", ts: 1625097601000 },
10+
{ type: "ask", ask: "command_output", text: "file2.txt", ts: 1625097602000 },
11+
]
612

7-
const messages: ClineMessage[] = [
8-
{
9-
ts: 1745710928469,
10-
type: "say",
11-
say: "api_req_started",
12-
text: '{"request":"<task>\\nRun the command \\"ping w…tes":12117,"cacheReads":0,"cost":0.020380125}',
13-
images: undefined,
14-
},
15-
{
16-
ts: 1745710930332,
17-
type: "say",
18-
say: "text",
19-
text: "Okay, I can run that command for you. The `pin…'s reachable and measure the round-trip time.",
20-
images: undefined,
21-
},
22-
{ ts: 1745710930748, type: "ask", ask: "command", text: "ping www.google.com", partial: false },
23-
{ ts: 1745710930894, type: "say", say: "command_output", text: "", images: undefined },
24-
{ ts: 1745710930894, type: "ask", ask: "command_output", text: "" },
25-
{
26-
ts: 1745710930954,
27-
type: "say",
28-
say: "command_output",
29-
text: "PING www.google.com (142.251.46.228): 56 data bytes\n",
30-
images: undefined,
31-
},
32-
{
33-
ts: 1745710930954,
34-
type: "ask",
35-
ask: "command_output",
36-
text: "PING www.google.com (142.251.46.228): 56 data bytes\n",
37-
},
38-
]
13+
const result = combineCommandSequences(messages)
3914

40-
describe("combineCommandSequences", () => {
41-
it("should combine command sequences", () => {
42-
const message = combineCommandSequences(messages).at(-1)
43-
expect(message!.text).toEqual(
44-
"ping www.google.com\nOutput:PING www.google.com (142.251.46.228): 56 data bytes\n",
45-
)
15+
expect(result).toHaveLength(1)
16+
expect(result[0]).toEqual({
17+
type: "ask",
18+
ask: "command",
19+
text: "ls\nOutput:file1.txt\nfile2.txt",
20+
ts: 1625097600000,
21+
})
22+
})
23+
})
24+
25+
describe("MCP server responses", () => {
26+
it("should combine use_mcp_server and mcp_server_response messages", () => {
27+
const messages: ClineMessage[] = [
28+
{
29+
type: "ask",
30+
ask: "use_mcp_server",
31+
text: JSON.stringify({
32+
serverName: "test-server",
33+
toolName: "test-tool",
34+
arguments: { param: "value" },
35+
}),
36+
ts: 1625097600000,
37+
},
38+
{ type: "say", say: "mcp_server_response", text: "Response data", ts: 1625097601000 },
39+
]
40+
41+
const result = combineCommandSequences(messages)
42+
43+
expect(result).toHaveLength(1)
44+
expect(result[0]).toEqual({
45+
type: "ask",
46+
ask: "use_mcp_server",
47+
text: JSON.stringify({
48+
serverName: "test-server",
49+
toolName: "test-tool",
50+
arguments: { param: "value" },
51+
response: "Response data",
52+
}),
53+
ts: 1625097600000,
54+
})
55+
})
56+
57+
it("should handle multiple mcp_server_response messages", () => {
58+
const messages: ClineMessage[] = [
59+
{
60+
type: "ask",
61+
ask: "use_mcp_server",
62+
text: JSON.stringify({
63+
serverName: "test-server",
64+
toolName: "test-tool",
65+
arguments: { param: "value" },
66+
}),
67+
ts: 1625097600000,
68+
},
69+
{ type: "say", say: "mcp_server_response", text: "First response", ts: 1625097601000 },
70+
{ type: "say", say: "mcp_server_response", text: "Second response", ts: 1625097602000 },
71+
]
72+
73+
const result = combineCommandSequences(messages)
74+
75+
expect(result).toHaveLength(1)
76+
expect(result[0]).toEqual({
77+
type: "ask",
78+
ask: "use_mcp_server",
79+
text: JSON.stringify({
80+
serverName: "test-server",
81+
toolName: "test-tool",
82+
arguments: { param: "value" },
83+
response: "First response\nSecond response",
84+
}),
85+
ts: 1625097600000,
86+
})
87+
})
88+
89+
it("should handle multiple MCP server requests", () => {
90+
const messages: ClineMessage[] = [
91+
{
92+
type: "ask",
93+
ask: "use_mcp_server",
94+
text: JSON.stringify({
95+
serverName: "test-server-1",
96+
toolName: "test-tool-1",
97+
arguments: { param: "value1" },
98+
}),
99+
ts: 1625097600000,
100+
},
101+
{ type: "say", say: "mcp_server_response", text: "Response 1", ts: 1625097601000 },
102+
{
103+
type: "ask",
104+
ask: "use_mcp_server",
105+
text: JSON.stringify({
106+
serverName: "test-server-2",
107+
toolName: "test-tool-2",
108+
arguments: { param: "value2" },
109+
}),
110+
ts: 1625097602000,
111+
},
112+
{ type: "say", say: "mcp_server_response", text: "Response 2", ts: 1625097603000 },
113+
]
114+
115+
const result = combineCommandSequences(messages)
116+
117+
expect(result).toHaveLength(2)
118+
expect(result[0]).toEqual({
119+
type: "ask",
120+
ask: "use_mcp_server",
121+
text: JSON.stringify({
122+
serverName: "test-server-1",
123+
toolName: "test-tool-1",
124+
arguments: { param: "value1" },
125+
response: "Response 1",
126+
}),
127+
ts: 1625097600000,
128+
})
129+
expect(result[1]).toEqual({
130+
type: "ask",
131+
ask: "use_mcp_server",
132+
text: JSON.stringify({
133+
serverName: "test-server-2",
134+
toolName: "test-tool-2",
135+
arguments: { param: "value2" },
136+
response: "Response 2",
137+
}),
138+
ts: 1625097602000,
139+
})
140+
})
141+
})
142+
143+
describe("mixed sequences", () => {
144+
it("should handle both command and MCP server sequences", () => {
145+
const messages: ClineMessage[] = [
146+
{ type: "ask", ask: "command", text: "ls", ts: 1625097600000 },
147+
{ type: "ask", ask: "command_output", text: "file1.txt", ts: 1625097601000 },
148+
{
149+
type: "ask",
150+
ask: "use_mcp_server",
151+
text: JSON.stringify({
152+
serverName: "test-server",
153+
toolName: "test-tool",
154+
arguments: { param: "value" },
155+
}),
156+
ts: 1625097602000,
157+
},
158+
{ type: "say", say: "mcp_server_response", text: "MCP response", ts: 1625097603000 },
159+
]
160+
161+
const result = combineCommandSequences(messages)
162+
163+
expect(result).toHaveLength(2)
164+
expect(result[0]).toEqual({
165+
type: "ask",
166+
ask: "command",
167+
text: "ls\nOutput:file1.txt",
168+
ts: 1625097600000,
169+
})
170+
expect(result[1]).toEqual({
171+
type: "ask",
172+
ask: "use_mcp_server",
173+
text: JSON.stringify({
174+
serverName: "test-server",
175+
toolName: "test-tool",
176+
arguments: { param: "value" },
177+
response: "MCP response",
178+
}),
179+
ts: 1625097602000,
180+
})
181+
})
46182
})
47183
})

0 commit comments

Comments
 (0)