Selaa lähdekoodia

feat(mcp): improve mcp with McpExecution, make UX like terminal execute (#3485)

* feat: add MCP viewer and enhanced UI

- Add McpExecutionStatus type with discriminated union for execution states
- Implement McpExecution component with live status updates and collapsible output
- Replace inline MCP UI in ChatRow with dedicated McpExecution component
- Add comprehensive test coverage for useMcpToolTool
- Enhance CodeAccordian to support custom headers
- Improve combineCommandSequences to handle MCP execution flows

* refactor: merge loops in combineCommandSequences for better efficiency

- Reduced from 3 separate loops to 1 main processing loop
- Improved time complexity from O(n²) to O(n)
- Reduced total passes through array from 4 to 2
- Better memory access patterns and cache utilization

* feat: enhance JSON parsing and rendering in McpExecution component

* feat: add mock implementations for react-markdown and remark-gfm

* fix: remove unreachable return statement in ChatRowContent component

* feat(i18n): add execution status messages and error handling for invalid JSON arguments

* test: add parameter validation tests for missing server_name and tool_name

* Update McpExecution component to show server name in header and tool name in approval section

---------

Co-authored-by: Daniel Riccio <[email protected]>
Sam Hoang Van 6 kuukautta sitten
vanhempi
sitoutus
a9c1dad2ce
47 muutettua tiedostoa jossa 1241 lisäystä ja 267 poistoa
  1. 1 0
      packages/types/src/index.ts
  2. 31 0
      packages/types/src/mcp.ts
  3. 269 0
      src/core/tools/__tests__/useMcpToolTool.test.ts
  4. 191 82
      src/core/tools/useMcpToolTool.ts
  5. 2 1
      src/i18n/locales/ca/mcp.json
  6. 2 1
      src/i18n/locales/de/mcp.json
  7. 2 1
      src/i18n/locales/en/mcp.json
  8. 2 1
      src/i18n/locales/es/mcp.json
  9. 2 2
      src/i18n/locales/fr/mcp.json
  10. 2 2
      src/i18n/locales/hi/mcp.json
  11. 2 2
      src/i18n/locales/it/mcp.json
  12. 2 2
      src/i18n/locales/ja/mcp.json
  13. 2 2
      src/i18n/locales/ko/mcp.json
  14. 2 2
      src/i18n/locales/nl/mcp.json
  15. 2 2
      src/i18n/locales/pl/mcp.json
  16. 2 2
      src/i18n/locales/pt-BR/mcp.json
  17. 2 2
      src/i18n/locales/ru/mcp.json
  18. 2 2
      src/i18n/locales/tr/mcp.json
  19. 2 2
      src/i18n/locales/vi/mcp.json
  20. 2 2
      src/i18n/locales/zh-CN/mcp.json
  21. 2 2
      src/i18n/locales/zh-TW/mcp.json
  22. 2 0
      src/shared/ExtensionMessage.ts
  23. 177 41
      src/shared/__tests__/combineCommandSequences.test.ts
  24. 87 21
      src/shared/combineCommandSequences.ts
  25. 2 0
      webview-ui/jest.config.cjs
  26. 19 0
      webview-ui/src/__mocks__/react-markdown.tsx
  27. 3 0
      webview-ui/src/__mocks__/remark-gfm.ts
  28. 23 70
      webview-ui/src/components/chat/ChatRow.tsx
  29. 291 0
      webview-ui/src/components/chat/McpExecution.tsx
  30. 10 7
      webview-ui/src/components/common/CodeAccordian.tsx
  31. 6 1
      webview-ui/src/i18n/locales/ca/mcp.json
  32. 6 1
      webview-ui/src/i18n/locales/de/mcp.json
  33. 5 0
      webview-ui/src/i18n/locales/en/mcp.json
  34. 6 1
      webview-ui/src/i18n/locales/es/mcp.json
  35. 6 1
      webview-ui/src/i18n/locales/fr/mcp.json
  36. 6 1
      webview-ui/src/i18n/locales/hi/mcp.json
  37. 6 1
      webview-ui/src/i18n/locales/it/mcp.json
  38. 6 1
      webview-ui/src/i18n/locales/ja/mcp.json
  39. 6 1
      webview-ui/src/i18n/locales/ko/mcp.json
  40. 6 1
      webview-ui/src/i18n/locales/nl/mcp.json
  41. 6 1
      webview-ui/src/i18n/locales/pl/mcp.json
  42. 6 1
      webview-ui/src/i18n/locales/pt-BR/mcp.json
  43. 6 1
      webview-ui/src/i18n/locales/ru/mcp.json
  44. 6 1
      webview-ui/src/i18n/locales/tr/mcp.json
  45. 6 1
      webview-ui/src/i18n/locales/vi/mcp.json
  46. 6 1
      webview-ui/src/i18n/locales/zh-CN/mcp.json
  47. 6 1
      webview-ui/src/i18n/locales/zh-TW/mcp.json

+ 1 - 0
packages/types/src/index.ts

@@ -7,6 +7,7 @@ export * from "./experiment.js"
 export * from "./global-settings.js"
 export * from "./history.js"
 export * from "./ipc.js"
+export * from "./mcp.js"
 export * from "./message.js"
 export * from "./mode.js"
 export * from "./model.js"

+ 31 - 0
packages/types/src/mcp.ts

@@ -0,0 +1,31 @@
+import { z } from "zod"
+
+/**
+ * McpExecutionStatus
+ */
+
+export const mcpExecutionStatusSchema = z.discriminatedUnion("status", [
+	z.object({
+		executionId: z.string(),
+		status: z.literal("started"),
+		serverName: z.string(),
+		toolName: z.string(),
+	}),
+	z.object({
+		executionId: z.string(),
+		status: z.literal("output"),
+		response: z.string(),
+	}),
+	z.object({
+		executionId: z.string(),
+		status: z.literal("completed"),
+		response: z.string().optional(),
+	}),
+	z.object({
+		executionId: z.string(),
+		status: z.literal("error"),
+		error: z.string().optional(),
+	}),
+])
+
+export type McpExecutionStatus = z.infer<typeof mcpExecutionStatusSchema>

+ 269 - 0
src/core/tools/__tests__/useMcpToolTool.test.ts

@@ -0,0 +1,269 @@
+import { useMcpToolTool } from "../useMcpToolTool"
+import { Task } from "../../task/Task"
+import { ToolUse } from "../../../shared/tools"
+import { formatResponse } from "../../prompts/responses"
+
+// Mock dependencies
+jest.mock("../../prompts/responses", () => ({
+	formatResponse: {
+		toolResult: jest.fn((result: string) => `Tool result: ${result}`),
+		toolError: jest.fn((error: string) => `Tool error: ${error}`),
+		invalidMcpToolArgumentError: jest.fn((server: string, tool: string) => `Invalid args for ${server}:${tool}`),
+	},
+}))
+
+jest.mock("../../../i18n", () => ({
+	t: jest.fn((key: string, params?: any) => {
+		if (key === "mcp:errors.invalidJsonArgument" && params?.toolName) {
+			return `Roo tried to use ${params.toolName} with an invalid JSON argument. Retrying...`
+		}
+		return key
+	}),
+}))
+
+describe("useMcpToolTool", () => {
+	let mockTask: Partial<Task>
+	let mockAskApproval: jest.Mock
+	let mockHandleError: jest.Mock
+	let mockPushToolResult: jest.Mock
+	let mockRemoveClosingTag: jest.Mock
+	let mockProviderRef: any
+
+	beforeEach(() => {
+		mockAskApproval = jest.fn()
+		mockHandleError = jest.fn()
+		mockPushToolResult = jest.fn()
+		mockRemoveClosingTag = jest.fn((tag: string, value?: string) => value || "")
+
+		mockProviderRef = {
+			deref: jest.fn().mockReturnValue({
+				getMcpHub: jest.fn().mockReturnValue({
+					callTool: jest.fn(),
+				}),
+				postMessageToWebview: jest.fn(),
+			}),
+		}
+
+		mockTask = {
+			consecutiveMistakeCount: 0,
+			recordToolError: jest.fn(),
+			sayAndCreateMissingParamError: jest.fn(),
+			say: jest.fn(),
+			ask: jest.fn(),
+			lastMessageTs: 123456789,
+			providerRef: mockProviderRef,
+		}
+	})
+
+	describe("parameter validation", () => {
+		it("should handle missing server_name", async () => {
+			const block: ToolUse = {
+				type: "tool_use",
+				name: "use_mcp_tool",
+				params: {
+					tool_name: "test_tool",
+					arguments: "{}",
+				},
+				partial: false,
+			}
+
+			mockTask.sayAndCreateMissingParamError = jest.fn().mockResolvedValue("Missing server_name error")
+
+			await useMcpToolTool(
+				mockTask as Task,
+				block,
+				mockAskApproval,
+				mockHandleError,
+				mockPushToolResult,
+				mockRemoveClosingTag,
+			)
+
+			expect(mockTask.consecutiveMistakeCount).toBe(1)
+			expect(mockTask.recordToolError).toHaveBeenCalledWith("use_mcp_tool")
+			expect(mockTask.sayAndCreateMissingParamError).toHaveBeenCalledWith("use_mcp_tool", "server_name")
+			expect(mockPushToolResult).toHaveBeenCalledWith("Missing server_name error")
+		})
+
+		it("should handle missing tool_name", async () => {
+			const block: ToolUse = {
+				type: "tool_use",
+				name: "use_mcp_tool",
+				params: {
+					server_name: "test_server",
+					arguments: "{}",
+				},
+				partial: false,
+			}
+
+			mockTask.sayAndCreateMissingParamError = jest.fn().mockResolvedValue("Missing tool_name error")
+
+			await useMcpToolTool(
+				mockTask as Task,
+				block,
+				mockAskApproval,
+				mockHandleError,
+				mockPushToolResult,
+				mockRemoveClosingTag,
+			)
+
+			expect(mockTask.consecutiveMistakeCount).toBe(1)
+			expect(mockTask.recordToolError).toHaveBeenCalledWith("use_mcp_tool")
+			expect(mockTask.sayAndCreateMissingParamError).toHaveBeenCalledWith("use_mcp_tool", "tool_name")
+			expect(mockPushToolResult).toHaveBeenCalledWith("Missing tool_name error")
+		})
+
+		it("should handle invalid JSON arguments", async () => {
+			const block: ToolUse = {
+				type: "tool_use",
+				name: "use_mcp_tool",
+				params: {
+					server_name: "test_server",
+					tool_name: "test_tool",
+					arguments: "invalid json",
+				},
+				partial: false,
+			}
+
+			await useMcpToolTool(
+				mockTask as Task,
+				block,
+				mockAskApproval,
+				mockHandleError,
+				mockPushToolResult,
+				mockRemoveClosingTag,
+			)
+
+			expect(mockTask.consecutiveMistakeCount).toBe(1)
+			expect(mockTask.recordToolError).toHaveBeenCalledWith("use_mcp_tool")
+			expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("invalid JSON argument"))
+			expect(mockPushToolResult).toHaveBeenCalledWith("Tool error: Invalid args for test_server:test_tool")
+		})
+	})
+
+	describe("partial requests", () => {
+		it("should handle partial requests", async () => {
+			const block: ToolUse = {
+				type: "tool_use",
+				name: "use_mcp_tool",
+				params: {
+					server_name: "test_server",
+					tool_name: "test_tool",
+					arguments: "{}",
+				},
+				partial: true,
+			}
+
+			mockTask.ask = jest.fn().mockResolvedValue(true)
+
+			await useMcpToolTool(
+				mockTask as Task,
+				block,
+				mockAskApproval,
+				mockHandleError,
+				mockPushToolResult,
+				mockRemoveClosingTag,
+			)
+
+			expect(mockTask.ask).toHaveBeenCalledWith("use_mcp_server", expect.stringContaining("use_mcp_tool"), true)
+		})
+	})
+
+	describe("successful execution", () => {
+		it("should execute tool successfully with valid parameters", async () => {
+			const block: ToolUse = {
+				type: "tool_use",
+				name: "use_mcp_tool",
+				params: {
+					server_name: "test_server",
+					tool_name: "test_tool",
+					arguments: '{"param": "value"}',
+				},
+				partial: false,
+			}
+
+			mockAskApproval.mockResolvedValue(true)
+
+			const mockToolResult = {
+				content: [{ type: "text", text: "Tool executed successfully" }],
+				isError: false,
+			}
+
+			mockProviderRef.deref.mockReturnValue({
+				getMcpHub: () => ({
+					callTool: jest.fn().mockResolvedValue(mockToolResult),
+				}),
+				postMessageToWebview: jest.fn(),
+			})
+
+			await useMcpToolTool(
+				mockTask as Task,
+				block,
+				mockAskApproval,
+				mockHandleError,
+				mockPushToolResult,
+				mockRemoveClosingTag,
+			)
+
+			expect(mockTask.consecutiveMistakeCount).toBe(0)
+			expect(mockAskApproval).toHaveBeenCalled()
+			expect(mockTask.say).toHaveBeenCalledWith("mcp_server_request_started")
+			expect(mockTask.say).toHaveBeenCalledWith("mcp_server_response", "Tool executed successfully")
+			expect(mockPushToolResult).toHaveBeenCalledWith("Tool result: Tool executed successfully")
+		})
+
+		it("should handle user rejection", async () => {
+			const block: ToolUse = {
+				type: "tool_use",
+				name: "use_mcp_tool",
+				params: {
+					server_name: "test_server",
+					tool_name: "test_tool",
+					arguments: "{}",
+				},
+				partial: false,
+			}
+
+			mockAskApproval.mockResolvedValue(false)
+
+			await useMcpToolTool(
+				mockTask as Task,
+				block,
+				mockAskApproval,
+				mockHandleError,
+				mockPushToolResult,
+				mockRemoveClosingTag,
+			)
+
+			expect(mockTask.say).not.toHaveBeenCalledWith("mcp_server_request_started")
+			expect(mockPushToolResult).not.toHaveBeenCalled()
+		})
+	})
+
+	describe("error handling", () => {
+		it("should handle unexpected errors", async () => {
+			const block: ToolUse = {
+				type: "tool_use",
+				name: "use_mcp_tool",
+				params: {
+					server_name: "test_server",
+					tool_name: "test_tool",
+				},
+				partial: false,
+			}
+
+			const error = new Error("Unexpected error")
+			mockAskApproval.mockRejectedValue(error)
+
+			await useMcpToolTool(
+				mockTask as Task,
+				block,
+				mockAskApproval,
+				mockHandleError,
+				mockPushToolResult,
+				mockRemoveClosingTag,
+			)
+
+			expect(mockHandleError).toHaveBeenCalledWith("executing MCP tool", error)
+		})
+	})
+})

+ 191 - 82
src/core/tools/useMcpToolTool.ts

@@ -2,6 +2,167 @@ import { Task } from "../task/Task"
 import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
 import { formatResponse } from "../prompts/responses"
 import { ClineAskUseMcpServer } from "../../shared/ExtensionMessage"
+import { McpExecutionStatus } from "@roo-code/types"
+import { t } from "../../i18n"
+
+interface McpToolParams {
+	server_name?: string
+	tool_name?: string
+	arguments?: string
+}
+
+type ValidationResult =
+	| { isValid: false }
+	| {
+			isValid: true
+			serverName: string
+			toolName: string
+			parsedArguments?: Record<string, unknown>
+	  }
+
+async function handlePartialRequest(
+	cline: Task,
+	params: McpToolParams,
+	removeClosingTag: RemoveClosingTag,
+): Promise<void> {
+	const partialMessage = JSON.stringify({
+		type: "use_mcp_tool",
+		serverName: removeClosingTag("server_name", params.server_name),
+		toolName: removeClosingTag("tool_name", params.tool_name),
+		arguments: removeClosingTag("arguments", params.arguments),
+	} satisfies ClineAskUseMcpServer)
+
+	await cline.ask("use_mcp_server", partialMessage, true).catch(() => {})
+}
+
+async function validateParams(
+	cline: Task,
+	params: McpToolParams,
+	pushToolResult: PushToolResult,
+): Promise<ValidationResult> {
+	if (!params.server_name) {
+		cline.consecutiveMistakeCount++
+		cline.recordToolError("use_mcp_tool")
+		pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "server_name"))
+		return { isValid: false }
+	}
+
+	if (!params.tool_name) {
+		cline.consecutiveMistakeCount++
+		cline.recordToolError("use_mcp_tool")
+		pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"))
+		return { isValid: false }
+	}
+
+	let parsedArguments: Record<string, unknown> | undefined
+
+	if (params.arguments) {
+		try {
+			parsedArguments = JSON.parse(params.arguments)
+		} catch (error) {
+			cline.consecutiveMistakeCount++
+			cline.recordToolError("use_mcp_tool")
+			await cline.say("error", t("mcp:errors.invalidJsonArgument", { toolName: params.tool_name }))
+
+			pushToolResult(
+				formatResponse.toolError(
+					formatResponse.invalidMcpToolArgumentError(params.server_name, params.tool_name),
+				),
+			)
+			return { isValid: false }
+		}
+	}
+
+	return {
+		isValid: true,
+		serverName: params.server_name,
+		toolName: params.tool_name,
+		parsedArguments,
+	}
+}
+
+async function sendExecutionStatus(cline: Task, status: McpExecutionStatus): Promise<void> {
+	const clineProvider = await cline.providerRef.deref()
+	clineProvider?.postMessageToWebview({
+		type: "mcpExecutionStatus",
+		text: JSON.stringify(status),
+	})
+}
+
+function processToolContent(toolResult: any): string {
+	if (!toolResult?.content || toolResult.content.length === 0) {
+		return ""
+	}
+
+	return toolResult.content
+		.map((item: any) => {
+			if (item.type === "text") {
+				return item.text
+			}
+			if (item.type === "resource") {
+				const { blob: _, ...rest } = item.resource
+				return JSON.stringify(rest, null, 2)
+			}
+			return ""
+		})
+		.filter(Boolean)
+		.join("\n\n")
+}
+
+async function executeToolAndProcessResult(
+	cline: Task,
+	serverName: string,
+	toolName: string,
+	parsedArguments: Record<string, unknown> | undefined,
+	executionId: string,
+	pushToolResult: PushToolResult,
+): Promise<void> {
+	await cline.say("mcp_server_request_started")
+
+	// Send started status
+	await sendExecutionStatus(cline, {
+		executionId,
+		status: "started",
+		serverName,
+		toolName,
+	})
+
+	const toolResult = await cline.providerRef.deref()?.getMcpHub()?.callTool(serverName, toolName, parsedArguments)
+
+	let toolResultPretty = "(No response)"
+
+	if (toolResult) {
+		const outputText = processToolContent(toolResult)
+
+		if (outputText) {
+			await sendExecutionStatus(cline, {
+				executionId,
+				status: "output",
+				response: outputText,
+			})
+
+			toolResultPretty = (toolResult.isError ? "Error:\n" : "") + outputText
+		}
+
+		// Send completion status
+		await sendExecutionStatus(cline, {
+			executionId,
+			status: toolResult.isError ? "error" : "completed",
+			response: toolResultPretty,
+			error: toolResult.isError ? "Error executing MCP tool" : undefined,
+		})
+	} else {
+		// Send error status if no result
+		await sendExecutionStatus(cline, {
+			executionId,
+			status: "error",
+			error: "No response from MCP server",
+		})
+	}
+
+	await cline.say("mcp_server_response", toolResultPretty)
+	pushToolResult(formatResponse.toolResult(toolResultPretty))
+}
 
 export async function useMcpToolTool(
 	cline: Task,
@@ -11,100 +172,48 @@ export async function useMcpToolTool(
 	pushToolResult: PushToolResult,
 	removeClosingTag: RemoveClosingTag,
 ) {
-	const server_name: string | undefined = block.params.server_name
-	const tool_name: string | undefined = block.params.tool_name
-	const mcp_arguments: string | undefined = block.params.arguments
 	try {
+		const params: McpToolParams = {
+			server_name: block.params.server_name,
+			tool_name: block.params.tool_name,
+			arguments: block.params.arguments,
+		}
+
+		// Handle partial requests
 		if (block.partial) {
-			const partialMessage = JSON.stringify({
-				type: "use_mcp_tool",
-				serverName: removeClosingTag("server_name", server_name),
-				toolName: removeClosingTag("tool_name", tool_name),
-				arguments: removeClosingTag("arguments", mcp_arguments),
-			} satisfies ClineAskUseMcpServer)
-
-			await cline.ask("use_mcp_server", partialMessage, block.partial).catch(() => {})
+			await handlePartialRequest(cline, params, removeClosingTag)
 			return
-		} else {
-			if (!server_name) {
-				cline.consecutiveMistakeCount++
-				cline.recordToolError("use_mcp_tool")
-				pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "server_name"))
-				return
-			}
-
-			if (!tool_name) {
-				cline.consecutiveMistakeCount++
-				cline.recordToolError("use_mcp_tool")
-				pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"))
-				return
-			}
-
-			let parsedArguments: Record<string, unknown> | undefined
-
-			if (mcp_arguments) {
-				try {
-					parsedArguments = JSON.parse(mcp_arguments)
-				} catch (error) {
-					cline.consecutiveMistakeCount++
-					cline.recordToolError("use_mcp_tool")
-					await cline.say("error", `Roo tried to use ${tool_name} with an invalid JSON argument. Retrying...`)
-
-					pushToolResult(
-						formatResponse.toolError(formatResponse.invalidMcpToolArgumentError(server_name, tool_name)),
-					)
-
-					return
-				}
-			}
+		}
 
-			cline.consecutiveMistakeCount = 0
+		// Validate parameters
+		const validation = await validateParams(cline, params, pushToolResult)
+		if (!validation.isValid) {
+			return
+		}
 
-			const completeMessage = JSON.stringify({
-				type: "use_mcp_tool",
-				serverName: server_name,
-				toolName: tool_name,
-				arguments: mcp_arguments,
-			} satisfies ClineAskUseMcpServer)
+		const { serverName, toolName, parsedArguments } = validation
 
-			const didApprove = await askApproval("use_mcp_server", completeMessage)
+		// Reset mistake count on successful validation
+		cline.consecutiveMistakeCount = 0
 
-			if (!didApprove) {
-				return
-			}
+		// Get user approval
+		const completeMessage = JSON.stringify({
+			type: "use_mcp_tool",
+			serverName,
+			toolName,
+			arguments: params.arguments,
+		} satisfies ClineAskUseMcpServer)
 
-			// Now execute the tool
-			await cline.say("mcp_server_request_started") // same as browser_action_result
-
-			const toolResult = await cline.providerRef
-				.deref()
-				?.getMcpHub()
-				?.callTool(server_name, tool_name, parsedArguments)
-
-			// TODO: add progress indicator and ability to parse images and non-text responses
-			const toolResultPretty =
-				(toolResult?.isError ? "Error:\n" : "") +
-					toolResult?.content
-						.map((item) => {
-							if (item.type === "text") {
-								return item.text
-							}
-							if (item.type === "resource") {
-								const { blob: _, ...rest } = item.resource
-								return JSON.stringify(rest, null, 2)
-							}
-							return ""
-						})
-						.filter(Boolean)
-						.join("\n\n") || "(No response)"
-
-			await cline.say("mcp_server_response", toolResultPretty)
-			pushToolResult(formatResponse.toolResult(toolResultPretty))
+		const executionId = cline.lastMessageTs?.toString() ?? Date.now().toString()
+		const didApprove = await askApproval("use_mcp_server", completeMessage)
 
+		if (!didApprove) {
 			return
 		}
+
+		// Execute the tool and process results
+		await executeToolAndProcessResult(cline, serverName!, toolName!, parsedArguments, executionId, pushToolResult)
 	} catch (error) {
 		await handleError("executing MCP tool", error)
-		return
 	}
 }

+ 2 - 1
src/i18n/locales/ca/mcp.json

@@ -4,7 +4,8 @@
 		"invalid_settings_syntax": "Format JSON de configuració MCP no vàlid. Si us plau, comprova si hi ha errors de sintaxi al teu fitxer de configuració.",
 		"invalid_settings_validation": "Format de configuració MCP no vàlid: {{errorMessages}}",
 		"create_json": "Ha fallat la creació o obertura de .roo/mcp.json: {{error}}",
-		"failed_update_project": "Ha fallat l'actualització dels servidors MCP del projecte"
+		"failed_update_project": "Ha fallat l'actualització dels servidors MCP del projecte",
+		"invalidJsonArgument": "Roo ha intentat utilitzar {{toolName}} amb un argument JSON no vàlid. Tornant a intentar..."
 	},
 	"info": {
 		"server_restarting": "Reiniciant el servidor MCP {{serverName}}...",

+ 2 - 1
src/i18n/locales/de/mcp.json

@@ -4,7 +4,8 @@
 		"invalid_settings_syntax": "Ungültiges MCP-Einstellungen-JSON-Format. Bitte überprüfe deine Einstellungsdatei auf Syntaxfehler.",
 		"invalid_settings_validation": "Ungültiges MCP-Einstellungen-Format: {{errorMessages}}",
 		"create_json": "Fehler beim Erstellen oder Öffnen von .roo/mcp.json: {{error}}",
-		"failed_update_project": "Fehler beim Aktualisieren der Projekt-MCP-Server"
+		"failed_update_project": "Fehler beim Aktualisieren der Projekt-MCP-Server",
+		"invalidJsonArgument": "Roo hat versucht, {{toolName}} mit einem ungültigen JSON-Argument zu verwenden. Wiederhole..."
 	},
 	"info": {
 		"server_restarting": "MCP-Server {{serverName}} wird neu gestartet...",

+ 2 - 1
src/i18n/locales/en/mcp.json

@@ -4,7 +4,8 @@
 		"invalid_settings_syntax": "Invalid MCP settings JSON format. Please check your settings file for syntax errors.",
 		"invalid_settings_validation": "Invalid MCP settings format: {{errorMessages}}",
 		"create_json": "Failed to create or open .roo/mcp.json: {{error}}",
-		"failed_update_project": "Failed to update project MCP servers"
+		"failed_update_project": "Failed to update project MCP servers",
+		"invalidJsonArgument": "Roo tried to use {{toolName}} with an invalid JSON argument. Retrying..."
 	},
 	"info": {
 		"server_restarting": "Restarting {{serverName}} MCP server...",

+ 2 - 1
src/i18n/locales/es/mcp.json

@@ -4,7 +4,8 @@
 		"invalid_settings_syntax": "Formato JSON de la configuración MCP no válido. Verifica si hay errores de sintaxis en tu archivo de configuración.",
 		"invalid_settings_validation": "Formato de configuración MCP no válido: {{errorMessages}}",
 		"create_json": "Error al crear o abrir .roo/mcp.json: {{error}}",
-		"failed_update_project": "Error al actualizar los servidores MCP del proyecto"
+		"failed_update_project": "Error al actualizar los servidores MCP del proyecto",
+		"invalidJsonArgument": "Roo intentó usar {{toolName}} con un argumento JSON no válido. Reintentando..."
 	},
 	"info": {
 		"server_restarting": "Reiniciando el servidor MCP {{serverName}}...",

+ 2 - 2
src/i18n/locales/fr/mcp.json

@@ -3,9 +3,9 @@
 		"invalid_settings_format": "Format JSON des paramètres MCP invalide. Veuillez vous assurer que vos paramètres suivent le format JSON correct.",
 		"invalid_settings_syntax": "Format JSON des paramètres MCP invalide. Veuillez vérifier le syntaxe de votre fichier de paramètres.",
 		"invalid_settings_validation": "Format de paramètres MCP invalide : {{errorMessages}}",
-
 		"create_json": "Échec de la création ou de l'ouverture de .roo/mcp.json : {{error}}",
-		"failed_update_project": "Échec de la mise à jour des serveurs MCP du projet"
+		"failed_update_project": "Échec de la mise à jour des serveurs MCP du projet",
+		"invalidJsonArgument": "Roo a essayé d'utiliser {{toolName}} avec un argument JSON invalide. Nouvelle tentative..."
 	},
 	"info": {
 		"server_restarting": "Redémarrage du serveur MCP {{serverName}}...",

+ 2 - 2
src/i18n/locales/hi/mcp.json

@@ -3,9 +3,9 @@
 		"invalid_settings_format": "अमान्य MCP सेटिंग्स JSON फॉर्मेट। कृपया सुनिश्चित करें कि आपकी सेटिंग्स सही JSON फॉर्मेट का पालन करती हैं।",
 		"invalid_settings_syntax": "अमान्य MCP सेटिंग्स JSON फॉर्मेट। कृपया अपनी सेटिंग्स फ़ाइल में सिंटैक्स त्रुटियों की जांच करें।",
 		"invalid_settings_validation": "अमान्य MCP सेटिंग्स फॉर्मेट: {{errorMessages}}",
-
 		"create_json": ".roo/mcp.json बनाने या खोलने में विफल: {{error}}",
-		"failed_update_project": "प्रोजेक्ट MCP सर्वर अपडेट करने में विफल"
+		"failed_update_project": "प्रोजेक्ट MCP सर्वर अपडेट करने में विफल",
+		"invalidJsonArgument": "Roo ने {{toolName}} को अमान्य JSON आर्गुमेंट के साथ उपयोग करने की कोशिश की। फिर से कोशिश कर रहा है..."
 	},
 	"info": {
 		"server_restarting": "{{serverName}} MCP सर्वर पुनः प्रारंभ हो रहा है...",

+ 2 - 2
src/i18n/locales/it/mcp.json

@@ -3,9 +3,9 @@
 		"invalid_settings_format": "Formato JSON delle impostazioni MCP non valido. Assicurati che le tue impostazioni seguano il formato JSON corretto.",
 		"invalid_settings_syntax": "Formato JSON delle impostazioni MCP non valido. Verifica gli errori di sintassi nel tuo file delle impostazioni.",
 		"invalid_settings_validation": "Formato delle impostazioni MCP non valido: {{errorMessages}}",
-
 		"create_json": "Impossibile creare o aprire .roo/mcp.json: {{error}}",
-		"failed_update_project": "Errore durante l'aggiornamento dei server MCP del progetto"
+		"failed_update_project": "Errore durante l'aggiornamento dei server MCP del progetto",
+		"invalidJsonArgument": "Roo ha tentato di usare {{toolName}} con un argomento JSON non valido. Riprovo..."
 	},
 	"info": {
 		"server_restarting": "Riavvio del server MCP {{serverName}}...",

+ 2 - 2
src/i18n/locales/ja/mcp.json

@@ -3,9 +3,9 @@
 		"invalid_settings_format": "MCP設定のJSONフォーマットが無効です。設定が正しいJSONフォーマットに従っていることを確認してください。",
 		"invalid_settings_syntax": "MCP設定のJSONフォーマットが無効です。設定ファイルの構文エラーを確認してください。",
 		"invalid_settings_validation": "MCP設定フォーマットが無効です:{{errorMessages}}",
-
 		"create_json": ".roo/mcp.jsonの作成または開くことに失敗しました:{{error}}",
-		"failed_update_project": "プロジェクトMCPサーバーの更新に失敗しました"
+		"failed_update_project": "プロジェクトMCPサーバーの更新に失敗しました",
+		"invalidJsonArgument": "Rooが無効なJSON引数で{{toolName}}を使用しようとしました。再試行中..."
 	},
 	"info": {
 		"server_restarting": "MCPサーバー{{serverName}}を再起動中...",

+ 2 - 2
src/i18n/locales/ko/mcp.json

@@ -3,9 +3,9 @@
 		"invalid_settings_format": "잘못된 MCP 설정 JSON 형식입니다. 설정이 올바른 JSON 형식을 따르는지 확인하세요.",
 		"invalid_settings_syntax": "잘못된 MCP 설정 JSON 형식입니다. 설정 파일의 구문 오류를 확인하세요.",
 		"invalid_settings_validation": "잘못된 MCP 설정 형식: {{errorMessages}}",
-
 		"create_json": ".roo/mcp.json 생성 또는 열기 실패: {{error}}",
-		"failed_update_project": "프로젝트 MCP 서버 업데이트에 실패했습니다"
+		"failed_update_project": "프로젝트 MCP 서버 업데이트에 실패했습니다",
+		"invalidJsonArgument": "Roo가 유효하지 않은 JSON 인자로 {{toolName}}을(를) 사용하려고 했습니다. 다시 시도 중..."
 	},
 	"info": {
 		"server_restarting": "{{serverName}} MCP 서버를 재시작하는 중...",

+ 2 - 2
src/i18n/locales/nl/mcp.json

@@ -3,9 +3,9 @@
 		"invalid_settings_format": "Ongeldig MCP-instellingen JSON-formaat. Zorg ervoor dat je instellingen het juiste JSON-formaat volgen.",
 		"invalid_settings_syntax": "Ongeldig MCP-instellingen JSON-formaat. Controleer je instellingenbestand op syntaxfouten.",
 		"invalid_settings_validation": "Ongeldig MCP-instellingenformaat: {{errorMessages}}",
-
 		"create_json": "Aanmaken of openen van .roo/mcp.json mislukt: {{error}}",
-		"failed_update_project": "Bijwerken van project MCP-servers mislukt"
+		"failed_update_project": "Bijwerken van project MCP-servers mislukt",
+		"invalidJsonArgument": "Roo probeerde {{toolName}} te gebruiken met een ongeldig JSON-argument. Opnieuw proberen..."
 	},
 	"info": {
 		"server_restarting": "{{serverName}} MCP-server wordt opnieuw gestart...",

+ 2 - 2
src/i18n/locales/pl/mcp.json

@@ -3,9 +3,9 @@
 		"invalid_settings_format": "Nieprawidłowy format JSON ustawień MCP. Upewnij się, że Twoje ustawienia są zgodne z poprawnym formatem JSON.",
 		"invalid_settings_syntax": "Nieprawidłowy format JSON ustawień MCP. Sprawdź, czy w pliku ustawień nie ma błędów składniowych.",
 		"invalid_settings_validation": "Nieprawidłowy format ustawień MCP: {{errorMessages}}",
-
 		"create_json": "Nie udało się utworzyć lub otworzyć .roo/mcp.json: {{error}}",
-		"failed_update_project": "Nie udało się zaktualizować serwerów MCP projektu"
+		"failed_update_project": "Nie udało się zaktualizować serwerów MCP projektu",
+		"invalidJsonArgument": "Roo próbował użyć {{toolName}} z nieprawidłowym argumentem JSON. Ponawianie..."
 	},
 	"info": {
 		"server_restarting": "Ponowne uruchamianie serwera MCP {{serverName}}...",

+ 2 - 2
src/i18n/locales/pt-BR/mcp.json

@@ -3,9 +3,9 @@
 		"invalid_settings_format": "Formato JSON das configurações MCP inválido. Por favor, verifique se suas configurações seguem o formato JSON correto.",
 		"invalid_settings_syntax": "Formato JSON das configurações MCP inválido. Por favor, verifique se há erros de sintaxe no seu arquivo de configurações.",
 		"invalid_settings_validation": "Formato de configurações MCP inválido: {{errorMessages}}",
-
 		"create_json": "Falha ao criar ou abrir .roo/mcp.json: {{error}}",
-		"failed_update_project": "Falha ao atualizar os servidores MCP do projeto"
+		"failed_update_project": "Falha ao atualizar os servidores MCP do projeto",
+		"invalidJsonArgument": "Roo tentou usar {{toolName}} com um argumento JSON inválido. Tentando novamente..."
 	},
 	"info": {
 		"server_restarting": "Reiniciando o servidor MCP {{serverName}}...",

+ 2 - 2
src/i18n/locales/ru/mcp.json

@@ -3,9 +3,9 @@
 		"invalid_settings_format": "Неверный формат JSON настроек MCP. Пожалуйста, убедитесь, что ваши настройки соответствуют правильному формату JSON.",
 		"invalid_settings_syntax": "Неверный формат JSON настроек MCP. Пожалуйста, проверьте ваш файл настроек на наличие синтаксических ошибок.",
 		"invalid_settings_validation": "Неверный формат настроек MCP: {{errorMessages}}",
-
 		"create_json": "Не удалось создать или открыть .roo/mcp.json: {{error}}",
-		"failed_update_project": "Не удалось обновить серверы проекта MCP"
+		"failed_update_project": "Не удалось обновить серверы проекта MCP",
+		"invalidJsonArgument": "Roo попытался использовать {{toolName}} с недопустимым JSON-аргументом. Повторная попытка..."
 	},
 	"info": {
 		"server_restarting": "Перезапуск сервера MCP {{serverName}}...",

+ 2 - 2
src/i18n/locales/tr/mcp.json

@@ -3,9 +3,9 @@
 		"invalid_settings_format": "Geçersiz MCP ayarları JSON formatı. Lütfen ayarlarınızın doğru JSON formatını takip ettiğinden emin olun.",
 		"invalid_settings_syntax": "Geçersiz MCP ayarları JSON formatı. Lütfen ayarlar dosyanızda sözdizimi hatalarını kontrol edin.",
 		"invalid_settings_validation": "Geçersiz MCP ayarları formatı: {{errorMessages}}",
-
 		"create_json": ".roo/mcp.json oluşturulamadı veya açılamadı: {{error}}",
-		"failed_update_project": "Proje MCP sunucuları güncellenemedi"
+		"failed_update_project": "Proje MCP sunucuları güncellenemedi",
+		"invalidJsonArgument": "Roo, {{toolName}} aracını geçersiz bir JSON argümanıyla kullanmaya çalıştı. Tekrar deneniyor..."
 	},
 	"info": {
 		"server_restarting": "{{serverName}} MCP sunucusu yeniden başlatılıyor...",

+ 2 - 2
src/i18n/locales/vi/mcp.json

@@ -3,9 +3,9 @@
 		"invalid_settings_format": "Định dạng JSON của cài đặt MCP không hợp lệ. Vui lòng đảm bảo cài đặt của bạn tuân theo định dạng JSON chính xác.",
 		"invalid_settings_syntax": "Định dạng JSON của cài đặt MCP không hợp lệ. Vui lòng kiểm tra lỗi cú pháp trong tệp cài đặt của bạn.",
 		"invalid_settings_validation": "Định dạng cài đặt MCP không hợp lệ: {{errorMessages}}",
-
 		"create_json": "Không thể tạo hoặc mở .roo/mcp.json: {{error}}",
-		"failed_update_project": "Không thể cập nhật máy chủ MCP của dự án"
+		"failed_update_project": "Không thể cập nhật máy chủ MCP của dự án",
+		"invalidJsonArgument": "Roo đã cố gắng sử dụng {{toolName}} với tham số JSON không hợp lệ. Đang thử lại..."
 	},
 	"info": {
 		"server_restarting": "Đang khởi động lại máy chủ MCP {{serverName}}...",

+ 2 - 2
src/i18n/locales/zh-CN/mcp.json

@@ -3,9 +3,9 @@
 		"invalid_settings_format": "MCP设置JSON格式无效。请确保您的设置遵循正确的JSON格式。",
 		"invalid_settings_syntax": "MCP设置JSON格式无效。请检查您的设置文件是否有语法错误。",
 		"invalid_settings_validation": "MCP设置格式无效:{{errorMessages}}",
-
 		"create_json": "创建或打开 .roo/mcp.json 失败:{{error}}",
-		"failed_update_project": "更新项目MCP服务器失败"
+		"failed_update_project": "更新项目MCP服务器失败",
+		"invalidJsonArgument": "Roo 尝试使用无效的 JSON 参数调用 {{toolName}}。正在重试..."
 	},
 	"info": {
 		"server_restarting": "正在重启{{serverName}}MCP服务器...",

+ 2 - 2
src/i18n/locales/zh-TW/mcp.json

@@ -3,9 +3,9 @@
 		"invalid_settings_format": "MCP 設定 JSON 格式無效。請確保您的設定遵循正確的 JSON 格式。",
 		"invalid_settings_syntax": "MCP 設定 JSON 格式無效。請檢查您的設定檔案是否有語法錯誤。",
 		"invalid_settings_validation": "MCP 設定格式無效:{{errorMessages}}",
-
 		"create_json": "建立或開啟 .roo/mcp.json 失敗:{{error}}",
-		"failed_update_project": "更新專案 MCP 伺服器失敗"
+		"failed_update_project": "更新專案 MCP 伺服器失敗",
+		"invalidJsonArgument": "Roo 嘗試使用無效的 JSON 參數呼叫 {{toolName}}。正在重試..."
 	},
 	"info": {
 		"server_restarting": "正在重啟{{serverName}}MCP 伺服器...",

+ 2 - 0
src/shared/ExtensionMessage.ts

@@ -67,6 +67,7 @@ export interface ExtensionMessage {
 		| "acceptInput"
 		| "setHistoryPreviewCollapsed"
 		| "commandExecutionStatus"
+		| "mcpExecutionStatus"
 		| "vsCodeSetting"
 		| "authenticatedUser"
 		| "condenseTaskContextResponse"
@@ -313,6 +314,7 @@ export interface ClineAskUseMcpServer {
 	toolName?: string
 	arguments?: string
 	uri?: string
+	response?: string
 }
 
 export interface ClineApiReqInfo {

+ 177 - 41
src/shared/__tests__/combineCommandSequences.test.ts

@@ -1,47 +1,183 @@
-// npx jest src/shared/__tests__/combineCommandSequences.test.ts
+import { combineCommandSequences } from "../combineCommandSequences"
+import { ClineMessage } from "@roo-code/types"
 
-import type { ClineMessage } from "@roo-code/types"
+describe("combineCommandSequences", () => {
+	describe("command sequences", () => {
+		it("should combine command and command_output messages", () => {
+			const messages: ClineMessage[] = [
+				{ type: "ask", ask: "command", text: "ls", ts: 1625097600000 },
+				{ type: "ask", ask: "command_output", text: "file1.txt", ts: 1625097601000 },
+				{ type: "ask", ask: "command_output", text: "file2.txt", ts: 1625097602000 },
+			]
 
-import { combineCommandSequences } from "../combineCommandSequences"
+			const result = combineCommandSequences(messages)
 
-const messages: ClineMessage[] = [
-	{
-		ts: 1745710928469,
-		type: "say",
-		say: "api_req_started",
-		text: '{"request":"<task>\\nRun the command \\"ping w…tes":12117,"cacheReads":0,"cost":0.020380125}',
-		images: undefined,
-	},
-	{
-		ts: 1745710930332,
-		type: "say",
-		say: "text",
-		text: "Okay, I can run that command for you. The `pin…'s reachable and measure the round-trip time.",
-		images: undefined,
-	},
-	{ ts: 1745710930748, type: "ask", ask: "command", text: "ping www.google.com", partial: false },
-	{ ts: 1745710930894, type: "say", say: "command_output", text: "", images: undefined },
-	{ ts: 1745710930894, type: "ask", ask: "command_output", text: "" },
-	{
-		ts: 1745710930954,
-		type: "say",
-		say: "command_output",
-		text: "PING www.google.com (142.251.46.228): 56 data bytes\n",
-		images: undefined,
-	},
-	{
-		ts: 1745710930954,
-		type: "ask",
-		ask: "command_output",
-		text: "PING www.google.com (142.251.46.228): 56 data bytes\n",
-	},
-]
+			expect(result).toHaveLength(1)
+			expect(result[0]).toEqual({
+				type: "ask",
+				ask: "command",
+				text: "ls\nOutput:file1.txt\nfile2.txt",
+				ts: 1625097600000,
+			})
+		})
+	})
 
-describe("combineCommandSequences", () => {
-	it("should combine command sequences", () => {
-		const message = combineCommandSequences(messages).at(-1)
-		expect(message!.text).toEqual(
-			"ping www.google.com\nOutput:PING www.google.com (142.251.46.228): 56 data bytes\n",
-		)
+	describe("MCP server responses", () => {
+		it("should combine use_mcp_server and mcp_server_response messages", () => {
+			const messages: ClineMessage[] = [
+				{
+					type: "ask",
+					ask: "use_mcp_server",
+					text: JSON.stringify({
+						serverName: "test-server",
+						toolName: "test-tool",
+						arguments: { param: "value" },
+					}),
+					ts: 1625097600000,
+				},
+				{ type: "say", say: "mcp_server_response", text: "Response data", ts: 1625097601000 },
+			]
+
+			const result = combineCommandSequences(messages)
+
+			expect(result).toHaveLength(1)
+			expect(result[0]).toEqual({
+				type: "ask",
+				ask: "use_mcp_server",
+				text: JSON.stringify({
+					serverName: "test-server",
+					toolName: "test-tool",
+					arguments: { param: "value" },
+					response: "Response data",
+				}),
+				ts: 1625097600000,
+			})
+		})
+
+		it("should handle multiple mcp_server_response messages", () => {
+			const messages: ClineMessage[] = [
+				{
+					type: "ask",
+					ask: "use_mcp_server",
+					text: JSON.stringify({
+						serverName: "test-server",
+						toolName: "test-tool",
+						arguments: { param: "value" },
+					}),
+					ts: 1625097600000,
+				},
+				{ type: "say", say: "mcp_server_response", text: "First response", ts: 1625097601000 },
+				{ type: "say", say: "mcp_server_response", text: "Second response", ts: 1625097602000 },
+			]
+
+			const result = combineCommandSequences(messages)
+
+			expect(result).toHaveLength(1)
+			expect(result[0]).toEqual({
+				type: "ask",
+				ask: "use_mcp_server",
+				text: JSON.stringify({
+					serverName: "test-server",
+					toolName: "test-tool",
+					arguments: { param: "value" },
+					response: "First response\nSecond response",
+				}),
+				ts: 1625097600000,
+			})
+		})
+
+		it("should handle multiple MCP server requests", () => {
+			const messages: ClineMessage[] = [
+				{
+					type: "ask",
+					ask: "use_mcp_server",
+					text: JSON.stringify({
+						serverName: "test-server-1",
+						toolName: "test-tool-1",
+						arguments: { param: "value1" },
+					}),
+					ts: 1625097600000,
+				},
+				{ type: "say", say: "mcp_server_response", text: "Response 1", ts: 1625097601000 },
+				{
+					type: "ask",
+					ask: "use_mcp_server",
+					text: JSON.stringify({
+						serverName: "test-server-2",
+						toolName: "test-tool-2",
+						arguments: { param: "value2" },
+					}),
+					ts: 1625097602000,
+				},
+				{ type: "say", say: "mcp_server_response", text: "Response 2", ts: 1625097603000 },
+			]
+
+			const result = combineCommandSequences(messages)
+
+			expect(result).toHaveLength(2)
+			expect(result[0]).toEqual({
+				type: "ask",
+				ask: "use_mcp_server",
+				text: JSON.stringify({
+					serverName: "test-server-1",
+					toolName: "test-tool-1",
+					arguments: { param: "value1" },
+					response: "Response 1",
+				}),
+				ts: 1625097600000,
+			})
+			expect(result[1]).toEqual({
+				type: "ask",
+				ask: "use_mcp_server",
+				text: JSON.stringify({
+					serverName: "test-server-2",
+					toolName: "test-tool-2",
+					arguments: { param: "value2" },
+					response: "Response 2",
+				}),
+				ts: 1625097602000,
+			})
+		})
+	})
+
+	describe("mixed sequences", () => {
+		it("should handle both command and MCP server sequences", () => {
+			const messages: ClineMessage[] = [
+				{ type: "ask", ask: "command", text: "ls", ts: 1625097600000 },
+				{ type: "ask", ask: "command_output", text: "file1.txt", ts: 1625097601000 },
+				{
+					type: "ask",
+					ask: "use_mcp_server",
+					text: JSON.stringify({
+						serverName: "test-server",
+						toolName: "test-tool",
+						arguments: { param: "value" },
+					}),
+					ts: 1625097602000,
+				},
+				{ type: "say", say: "mcp_server_response", text: "MCP response", ts: 1625097603000 },
+			]
+
+			const result = combineCommandSequences(messages)
+
+			expect(result).toHaveLength(2)
+			expect(result[0]).toEqual({
+				type: "ask",
+				ask: "command",
+				text: "ls\nOutput:file1.txt",
+				ts: 1625097600000,
+			})
+			expect(result[1]).toEqual({
+				type: "ask",
+				ask: "use_mcp_server",
+				text: JSON.stringify({
+					serverName: "test-server",
+					toolName: "test-tool",
+					arguments: { param: "value" },
+					response: "MCP response",
+				}),
+				ts: 1625097602000,
+			})
+		})
 	})
 })

+ 87 - 21
src/shared/combineCommandSequences.ts

@@ -1,17 +1,20 @@
-import type { ClineMessage } from "@roo-code/types"
+import { ClineMessage } from "@roo-code/types"
+import { safeJsonParse } from "./safeJsonParse"
 
 export const COMMAND_OUTPUT_STRING = "Output:"
 
 /**
  * Combines sequences of command and command_output messages in an array of ClineMessages.
+ * Also combines sequences of use_mcp_server and mcp_server_response messages.
  *
  * This function processes an array of ClineMessages objects, looking for sequences
- * where a 'command' message is followed by one or more 'command_output' messages.
+ * where a 'command' message is followed by one or more 'command_output' messages,
+ * or where a 'use_mcp_server' message is followed by one or more 'mcp_server_response' messages.
  * When such a sequence is found, it combines them into a single message, merging
  * their text contents.
  *
  * @param messages - An array of ClineMessage objects to process.
- * @returns A new array of ClineMessage objects with command sequences combined.
+ * @returns A new array of ClineMessage objects with command and MCP sequences combined.
  *
  * @example
  * const messages: ClineMessage[] = [
@@ -23,14 +26,54 @@ export const COMMAND_OUTPUT_STRING = "Output:"
  * // Result: [{ type: 'ask', ask: 'command', text: 'ls\nfile1.txt\nfile2.txt', ts: 1625097600000 }]
  */
 export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[] {
-	const combinedCommands: ClineMessage[] = []
+	const combinedMessages = new Map<number, ClineMessage>()
+	const processedIndices = new Set<number>()
 
-	// First pass: combine commands with their outputs.
+	// Single pass through all messages
 	for (let i = 0; i < messages.length; i++) {
-		if (messages[i].type === "ask" && messages[i].ask === "command") {
-			let combinedText = messages[i].text || ""
+		const msg = messages[i]
+
+		// Handle MCP server requests
+		if (msg.type === "ask" && msg.ask === "use_mcp_server") {
+			// Look ahead for MCP responses
+			let responses: string[] = []
+			let j = i + 1
+
+			while (j < messages.length) {
+				if (messages[j].say === "mcp_server_response") {
+					responses.push(messages[j].text || "")
+					processedIndices.add(j)
+					j++
+				} else if (messages[j].type === "ask" && messages[j].ask === "use_mcp_server") {
+					// Stop if we encounter another MCP request
+					break
+				} else {
+					j++
+				}
+			}
+
+			if (responses.length > 0) {
+				// Parse the JSON from the message text
+				const jsonObj = safeJsonParse<any>(msg.text || "{}", {})
+
+				// Add the response to the JSON object
+				jsonObj.response = responses.join("\n")
+
+				// Stringify the updated JSON object
+				const combinedText = JSON.stringify(jsonObj)
+
+				combinedMessages.set(msg.ts, { ...msg, text: combinedText })
+			} else {
+				// If there's no response, just keep the original message
+				combinedMessages.set(msg.ts, { ...msg })
+			}
+		}
+		// Handle command sequences
+		else if (msg.type === "ask" && msg.ask === "command") {
+			let combinedText = msg.text || ""
 			let j = i + 1
 			let previous: { type: "ask" | "say"; text: string } | undefined
+			let lastProcessedIndex = i
 
 			while (j < messages.length) {
 				const { type, ask, say, text = "" } = messages[j]
@@ -47,33 +90,56 @@ export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[
 					const isDuplicate = previous && previous.type !== type && previous.text === text
 
 					if (text.length > 0 && !isDuplicate) {
+						// Add a newline before adding the text if there's already content
+						if (
+							previous &&
+							combinedText.length >
+								combinedText.indexOf(COMMAND_OUTPUT_STRING) + COMMAND_OUTPUT_STRING.length
+						) {
+							combinedText += "\n"
+						}
 						combinedText += text
 					}
 
 					previous = { type, text }
+					processedIndices.add(j)
+					lastProcessedIndex = j
 				}
 
 				j++
 			}
 
-			combinedCommands.push({ ...messages[i], text: combinedText })
+			combinedMessages.set(msg.ts, { ...msg, text: combinedText })
 
-			// Move to the index just before the next command or end of array.
-			i = j - 1
+			// Only skip ahead if we actually processed command outputs
+			if (lastProcessedIndex > i) {
+				i = lastProcessedIndex
+			}
 		}
 	}
 
-	// console.log(`[combineCommandSequences] combinedCommands ->`, messages, combinedCommands)
+	// Build final result: filter out processed messages and use combined versions
+	const result: ClineMessage[] = []
+	for (let i = 0; i < messages.length; i++) {
+		const msg = messages[i]
 
-	// Second pass: remove command_outputs and replace original commands with
-	// combined ones.
-	return messages
-		.filter((msg) => !(msg.ask === "command_output" || msg.say === "command_output"))
-		.map((msg) => {
-			if (msg.type === "ask" && msg.ask === "command") {
-				return combinedCommands.find((cmd) => cmd.ts === msg.ts) || msg
-			}
+		// Skip messages that were processed as outputs/responses
+		if (processedIndices.has(i)) {
+			continue
+		}
+
+		// Skip command_output and mcp_server_response messages
+		if (msg.ask === "command_output" || msg.say === "command_output" || msg.say === "mcp_server_response") {
+			continue
+		}
+
+		// Use combined version if available
+		if (combinedMessages.has(msg.ts)) {
+			result.push(combinedMessages.get(msg.ts)!)
+		} else {
+			result.push(msg)
+		}
+	}
 
-			return msg
-		})
+	return result
 }

+ 2 - 0
webview-ui/jest.config.cjs

@@ -22,6 +22,8 @@ module.exports = {
 		"^\\./TranslationContext$": "<rootDir>/src/__mocks__/i18n/TranslationContext.tsx",
 		"^@src/utils/highlighter$": "<rootDir>/src/__mocks__/utils/highlighter.ts",
 		"^shiki$": "<rootDir>/src/__mocks__/shiki.ts",
+		"^react-markdown$": "<rootDir>/src/__mocks__/react-markdown.tsx",
+		"^remark-gfm$": "<rootDir>/src/__mocks__/remark-gfm.ts",
 	},
 	reporters: [["jest-simple-dot-reporter", {}]],
 	transformIgnorePatterns: [

+ 19 - 0
webview-ui/src/__mocks__/react-markdown.tsx

@@ -0,0 +1,19 @@
+import React from "react"
+
+interface ReactMarkdownProps {
+	children?: React.ReactNode
+	className?: string
+	remarkPlugins?: any[]
+	components?: any
+}
+
+const ReactMarkdown: React.FC<ReactMarkdownProps> = ({ children, className }) => {
+	return (
+		<div className={className} data-testid="mock-react-markdown">
+			{children}
+		</div>
+	)
+}
+
+export default ReactMarkdown
+export type { ReactMarkdownProps as Options }

+ 3 - 0
webview-ui/src/__mocks__/remark-gfm.ts

@@ -0,0 +1,3 @@
+const remarkGfm = () => {}
+
+export default remarkGfm

+ 23 - 70
webview-ui/src/components/chat/ChatRow.tsx

@@ -1,4 +1,5 @@
 import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
+import { McpExecution } from "./McpExecution"
 import { useSize } from "react-use"
 import { useTranslation, Trans } from "react-i18next"
 import deepEqual from "fast-deep-equal"
@@ -25,7 +26,6 @@ import MarkdownBlock from "../common/MarkdownBlock"
 import { ReasoningBlock } from "./ReasoningBlock"
 import Thumbnails from "../common/Thumbnails"
 import McpResourceRow from "../mcp/McpResourceRow"
-import McpToolRow from "../mcp/McpToolRow"
 
 import { Mention } from "./Mention"
 import { CheckpointSaved } from "./checkpoints/CheckpointSaved"
@@ -965,28 +965,6 @@ export const ChatRowContent = ({
 					)
 				case "shell_integration_warning":
 					return <CommandExecutionError />
-				case "mcp_server_response":
-					return (
-						<>
-							<div style={{ paddingTop: 0 }}>
-								<div
-									style={{
-										marginBottom: "4px",
-										opacity: 0.8,
-										fontSize: "12px",
-										textTransform: "uppercase",
-									}}>
-									{t("chat:response")}
-								</div>
-								<CodeAccordian
-									code={message.text}
-									language="json"
-									isExpanded={true}
-									onToggleExpand={handleToggleExpand}
-								/>
-							</div>
-						</>
-					)
 				case "checkpoint_saved":
 					return (
 						<CheckpointSaved
@@ -1070,7 +1048,17 @@ export const ChatRowContent = ({
 						/>
 					)
 				case "use_mcp_server":
-					const useMcpServer = safeJsonParse<ClineAskUseMcpServer>(message.text)
+					// Parse the message text to get the MCP server request
+					const messageJson = safeJsonParse<any>(message.text, {})
+
+					// Extract the response field if it exists
+					const { response, ...mcpServerRequest } = messageJson
+
+					// Create the useMcpServer object with the response field
+					const useMcpServer: ClineAskUseMcpServer = {
+						...mcpServerRequest,
+						response,
+					}
 
 					if (!useMcpServer) {
 						return null
@@ -1084,13 +1072,7 @@ export const ChatRowContent = ({
 								{icon}
 								{title}
 							</div>
-							<div
-								style={{
-									background: "var(--vscode-textCodeBlock-background)",
-									borderRadius: "3px",
-									padding: "8px 10px",
-									marginTop: "8px",
-								}}>
+							<div className="w-full bg-vscode-editor-background border border-vscode-border rounded-xs p-2 mt-2">
 								{useMcpServer.type === "access_mcp_resource" && (
 									<McpResourceRow
 										item={{
@@ -1110,45 +1092,16 @@ export const ChatRowContent = ({
 									/>
 								)}
 								{useMcpServer.type === "use_mcp_tool" && (
-									<>
-										<div onClick={(e) => e.stopPropagation()}>
-											<McpToolRow
-												tool={{
-													name: useMcpServer.toolName || "",
-													description:
-														server?.tools?.find(
-															(tool) => tool.name === useMcpServer.toolName,
-														)?.description || "",
-													alwaysAllow:
-														server?.tools?.find(
-															(tool) => tool.name === useMcpServer.toolName,
-														)?.alwaysAllow || false,
-												}}
-												serverName={useMcpServer.serverName}
-												serverSource={server?.source}
-												alwaysAllowMcp={alwaysAllowMcp}
-											/>
-										</div>
-										{useMcpServer.arguments && useMcpServer.arguments !== "{}" && (
-											<div style={{ marginTop: "8px" }}>
-												<div
-													style={{
-														marginBottom: "4px",
-														opacity: 0.8,
-														fontSize: "12px",
-														textTransform: "uppercase",
-													}}>
-													{t("chat:arguments")}
-												</div>
-												<CodeAccordian
-													code={useMcpServer.arguments}
-													language="json"
-													isExpanded={true}
-													onToggleExpand={handleToggleExpand}
-												/>
-											</div>
-										)}
-									</>
+									<McpExecution
+										executionId={message.ts.toString()}
+										text={useMcpServer.arguments !== "{}" ? useMcpServer.arguments : undefined}
+										serverName={useMcpServer.serverName}
+										toolName={useMcpServer.toolName}
+										isArguments={true}
+										server={server}
+										useMcpServer={useMcpServer}
+										alwaysAllowMcp={alwaysAllowMcp}
+									/>
 								)}
 							</div>
 						</>

+ 291 - 0
webview-ui/src/components/chat/McpExecution.tsx

@@ -0,0 +1,291 @@
+import { useCallback, useEffect, useMemo, useState, memo } from "react"
+import { Server, ChevronDown } from "lucide-react"
+import { useEvent } from "react-use"
+import { useTranslation } from "react-i18next"
+
+import { McpExecutionStatus, mcpExecutionStatusSchema } from "@roo-code/types"
+import { ExtensionMessage, ClineAskUseMcpServer } from "../../../../src/shared/ExtensionMessage"
+import { safeJsonParse } from "../../../../src/shared/safeJsonParse"
+import { cn } from "@src/lib/utils"
+import { Button } from "@src/components/ui"
+import CodeBlock from "../common/CodeBlock"
+import McpToolRow from "../mcp/McpToolRow"
+import { Markdown } from "./Markdown"
+
+interface McpExecutionProps {
+	executionId: string
+	text?: string
+	serverName?: string
+	toolName?: string
+	isArguments?: boolean
+	server?: {
+		tools?: Array<{
+			name: string
+			description?: string
+			alwaysAllow?: boolean
+		}>
+		source?: "global" | "project"
+	}
+	useMcpServer?: ClineAskUseMcpServer
+	alwaysAllowMcp?: boolean
+}
+
+export const McpExecution = ({
+	executionId,
+	text,
+	serverName: initialServerName,
+	toolName: initialToolName,
+	isArguments = false,
+	server,
+	useMcpServer,
+	alwaysAllowMcp = false,
+}: McpExecutionProps) => {
+	const { t } = useTranslation("mcp")
+
+	// State for tracking MCP response status
+	const [status, setStatus] = useState<McpExecutionStatus | null>(null)
+	const [responseText, setResponseText] = useState(text || "")
+	const [argumentsText, setArgumentsText] = useState(text || "")
+	const [serverName, setServerName] = useState(initialServerName)
+	const [toolName, setToolName] = useState(initialToolName)
+
+	// Only need expanded state for response section (like command output)
+	const [isResponseExpanded, setIsResponseExpanded] = useState(false)
+
+	// Try to parse JSON and return both the result and formatted text
+	const tryParseJson = (text: string): { isJson: boolean; formatted: string } => {
+		if (!text) return { isJson: false, formatted: "" }
+
+		try {
+			const parsed = JSON.parse(text)
+			return {
+				isJson: true,
+				formatted: JSON.stringify(parsed, null, 2),
+			}
+		} catch {
+			return {
+				isJson: false,
+				formatted: text,
+			}
+		}
+	}
+
+	const responseData = useMemo(() => tryParseJson(responseText), [responseText])
+	const argumentsData = useMemo(() => tryParseJson(argumentsText), [argumentsText])
+
+	const formattedResponseText = responseData.formatted
+	const formattedArgumentsText = argumentsData.formatted
+	const responseIsJson = responseData.isJson
+
+	const onToggleResponseExpand = useCallback(() => {
+		setIsResponseExpanded(!isResponseExpanded)
+	}, [isResponseExpanded])
+
+	// Listen for MCP execution status messages
+	const onMessage = useCallback(
+		(event: MessageEvent) => {
+			const message: ExtensionMessage = event.data
+
+			if (message.type === "mcpExecutionStatus") {
+				try {
+					const result = mcpExecutionStatusSchema.safeParse(safeJsonParse(message.text || "{}", {}))
+
+					if (result.success) {
+						const data = result.data
+
+						// Only update if this message is for our response
+						if (data.executionId === executionId) {
+							setStatus(data)
+
+							if (data.status === "output" && data.response) {
+								setResponseText((prev) => prev + data.response)
+								// Keep the arguments when we get output
+								if (isArguments && argumentsText === responseText) {
+									setArgumentsText(responseText)
+								}
+							} else if (data.status === "completed" && data.response) {
+								setResponseText(data.response)
+								// Keep the arguments when we get completed response
+								if (isArguments && argumentsText === responseText) {
+									setArgumentsText(responseText)
+								}
+							}
+						}
+					}
+				} catch (e) {
+					console.error("Failed to parse MCP execution status", e)
+				}
+			}
+		},
+		[argumentsText, executionId, isArguments, responseText],
+	)
+
+	useEvent("message", onMessage)
+
+	// Initialize with text if provided and parse command/response sections
+	useEffect(() => {
+		// Handle arguments text
+		if (text) {
+			try {
+				// Try to parse the text as JSON for arguments
+				const jsonObj = safeJsonParse<any>(text, null)
+
+				if (jsonObj && typeof jsonObj === "object") {
+					// Format the JSON for display
+					setArgumentsText(JSON.stringify(jsonObj, null, 2))
+				} else {
+					// If not valid JSON, use as is
+					setArgumentsText(text)
+				}
+			} catch (_e) {
+				// If parsing fails, use text as is
+				setArgumentsText(text)
+			}
+		}
+
+		// Handle response text
+		if (useMcpServer?.response) {
+			setResponseText(useMcpServer.response)
+		}
+
+		if (initialServerName && initialServerName !== serverName) {
+			setServerName(initialServerName)
+		}
+
+		if (initialToolName && initialToolName !== toolName) {
+			setToolName(initialToolName)
+		}
+	}, [text, useMcpServer, initialServerName, initialToolName, serverName, toolName, isArguments])
+
+	return (
+		<>
+			<div className="flex flex-row items-center justify-between gap-2 mb-1">
+				<div className="flex flex-row items-center gap-1 flex-wrap">
+					<Server size={16} className="text-vscode-descriptionForeground" />
+					<div className="flex items-center gap-1 flex-wrap">
+						{serverName && <span className="font-bold text-vscode-foreground">{serverName}</span>}
+					</div>
+				</div>
+				<div className="flex flex-row items-center justify-between gap-2 px-1">
+					<div className="flex flex-row items-center gap-1">
+						{status && (
+							<div className="flex flex-row items-center gap-2 font-mono text-xs">
+								<div
+									className={cn("rounded-full size-1.5", {
+										"bg-lime-400": status.status === "started" || status.status === "completed",
+										"bg-red-400": status.status === "error",
+									})}
+								/>
+								<div
+									className={cn("whitespace-nowrap", {
+										"text-vscode-foreground":
+											status.status === "started" || status.status === "completed",
+										"text-vscode-errorForeground": status.status === "error",
+									})}>
+									{status.status === "started"
+										? t("execution.running")
+										: status.status === "completed"
+											? t("execution.completed")
+											: t("execution.error")}
+								</div>
+								{status.status === "error" && "error" in status && status.error && (
+									<div className="whitespace-nowrap">({status.error})</div>
+								)}
+							</div>
+						)}
+						{responseText && responseText.length > 0 && (
+							<Button variant="ghost" size="icon" onClick={onToggleResponseExpand}>
+								<ChevronDown
+									className={cn("size-4 transition-transform duration-300", {
+										"rotate-180": isResponseExpanded,
+									})}
+								/>
+							</Button>
+						)}
+					</div>
+				</div>
+			</div>
+
+			<div className="w-full bg-vscode-editor-background rounded-xs p-2">
+				{/* Tool information section */}
+				{useMcpServer?.type === "use_mcp_tool" && (
+					<div onClick={(e) => e.stopPropagation()}>
+						<McpToolRow
+							tool={{
+								name: useMcpServer.toolName || "",
+								description:
+									server?.tools?.find((tool) => tool.name === useMcpServer.toolName)?.description ||
+									"",
+								alwaysAllow:
+									server?.tools?.find((tool) => tool.name === useMcpServer.toolName)?.alwaysAllow ||
+									false,
+							}}
+							serverName={useMcpServer.serverName}
+							serverSource={server?.source}
+							alwaysAllowMcp={alwaysAllowMcp}
+						/>
+					</div>
+				)}
+				{!useMcpServer && toolName && serverName && (
+					<div onClick={(e) => e.stopPropagation()}>
+						<McpToolRow
+							tool={{
+								name: toolName || "",
+								description: "",
+								alwaysAllow: false,
+							}}
+							serverName={serverName}
+							serverSource={undefined}
+							alwaysAllowMcp={alwaysAllowMcp}
+						/>
+					</div>
+				)}
+
+				{/* Arguments section - display like command (always visible) */}
+				{(isArguments || useMcpServer?.arguments || argumentsText) && (
+					<div
+						className={cn({
+							"mt-1 pt-1":
+								!isArguments && (useMcpServer?.type === "use_mcp_tool" || (toolName && serverName)),
+						})}>
+						<CodeBlock source={formattedArgumentsText} language="json" />
+					</div>
+				)}
+
+				{/* Response section - collapsible like command output */}
+				<ResponseContainer
+					isExpanded={isResponseExpanded}
+					response={formattedResponseText}
+					isJson={responseIsJson}
+					hasArguments={!!(isArguments || useMcpServer?.arguments || argumentsText)}
+				/>
+			</div>
+		</>
+	)
+}
+
+McpExecution.displayName = "McpExecution"
+
+const ResponseContainerInternal = ({
+	isExpanded,
+	response,
+	isJson,
+	hasArguments,
+}: {
+	isExpanded: boolean
+	response: string
+	isJson: boolean
+	hasArguments?: boolean
+}) => (
+	<div
+		className={cn("overflow-hidden", {
+			"max-h-0": !isExpanded,
+			"max-h-[100%] mt-1 pt-1 border-t border-border/25": isExpanded && hasArguments,
+			"max-h-[100%] mt-1 pt-1": isExpanded && !hasArguments,
+		})}>
+		{response.length > 0 &&
+			(isJson ? <CodeBlock source={response} language="json" /> : <Markdown markdown={response} partial={false} />)}
+	</div>
+)
+
+const ResponseContainer = memo(ResponseContainerInternal)

+ 10 - 7
webview-ui/src/components/common/CodeAccordian.tsx

@@ -1,8 +1,6 @@
 import { memo, useMemo } from "react"
 import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
-
-import type { ToolProgressStatus } from "@roo-code/types"
-
+import { type ToolProgressStatus } from "@roo-code/types"
 import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
 import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
 
@@ -18,6 +16,7 @@ interface CodeAccordianProps {
 	isExpanded: boolean
 	isFeedback?: boolean
 	onToggleExpand: () => void
+	header?: string
 }
 
 const CodeAccordian = ({
@@ -29,17 +28,23 @@ const CodeAccordian = ({
 	isExpanded,
 	isFeedback,
 	onToggleExpand,
+	header,
 }: CodeAccordianProps) => {
 	const inferredLanguage = useMemo(() => language ?? (path ? getLanguageFromPath(path) : "txt"), [path, language])
 	const source = useMemo(() => code.trim(), [code])
-	const hasHeader = Boolean(path || isFeedback)
+	const hasHeader = Boolean(path || isFeedback || header)
 
 	return (
 		<ToolUseBlock>
 			{hasHeader && (
 				<ToolUseBlockHeader onClick={onToggleExpand}>
 					{isLoading && <VSCodeProgressRing className="size-3 mr-2" />}
-					{isFeedback ? (
+					{header ? (
+						<div className="flex items-center">
+							<span className="codicon codicon-server mr-1.5"></span>
+							<span className="whitespace-nowrap overflow-hidden text-ellipsis mr-2">{header}</span>
+						</div>
+					) : isFeedback ? (
 						<div className="flex items-center">
 							<span className={`codicon codicon-${isFeedback ? "feedback" : "codicon-output"} mr-1.5`} />
 							<span className="whitespace-nowrap overflow-hidden text-ellipsis mr-2 rtl">
@@ -75,6 +80,4 @@ const CodeAccordian = ({
 	)
 }
 
-// Memo does shallow comparison of props, so if you need it to re-render when a
-// nested object changes, you need to pass a custom comparison function.
 export default memo(CodeAccordian)

+ 6 - 1
webview-ui/src/i18n/locales/ca/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "Tornant a intentar...",
 		"retryConnection": "Torna a intentar la connexió"
 	},
-	"refreshMCP": "Actualitza els servidors MCP"
+	"refreshMCP": "Actualitza els servidors MCP",
+	"execution": {
+		"running": "En execució",
+		"completed": "Completat",
+		"error": "Error"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/de/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "Wiederhole...",
 		"retryConnection": "Verbindung wiederholen"
 	},
-	"refreshMCP": "MCP-Server aktualisieren"
+	"refreshMCP": "MCP-Server aktualisieren",
+	"execution": {
+		"running": "Wird ausgeführt",
+		"completed": "Abgeschlossen",
+		"error": "Fehler"
+	}
 }

+ 5 - 0
webview-ui/src/i18n/locales/en/mcp.json

@@ -54,5 +54,10 @@
 	"serverStatus": {
 		"retrying": "Retrying...",
 		"retryConnection": "Retry Connection"
+	},
+	"execution": {
+		"running": "Running",
+		"completed": "Completed",
+		"error": "Error"
 	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/es/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "Reintentando...",
 		"retryConnection": "Reintentar conexión"
 	},
-	"refreshMCP": "Actualizar Servidores MCP"
+	"refreshMCP": "Actualizar Servidores MCP",
+	"execution": {
+		"running": "Ejecutando",
+		"completed": "Completado",
+		"error": "Error"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/fr/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "Nouvelle tentative...",
 		"retryConnection": "Réessayer la connexion"
 	},
-	"refreshMCP": "Rafraîchir les serveurs MCP"
+	"refreshMCP": "Rafraîchir les serveurs MCP",
+	"execution": {
+		"running": "En cours",
+		"completed": "Terminé",
+		"error": "Erreur"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/hi/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "फिर से कोशिश कर रहा है...",
 		"retryConnection": "कनेक्शन फिर से आज़माएँ"
 	},
-	"refreshMCP": "एमसीपी सर्वर रीफ्रेश करें"
+	"refreshMCP": "एमसीपी सर्वर रीफ्रेश करें",
+	"execution": {
+		"running": "चल रहा है",
+		"completed": "पूरा हुआ",
+		"error": "त्रुटि"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/it/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "Riprovo...",
 		"retryConnection": "Riprova connessione"
 	},
-	"refreshMCP": "Aggiorna server MCP"
+	"refreshMCP": "Aggiorna server MCP",
+	"execution": {
+		"running": "In esecuzione",
+		"completed": "Completato",
+		"error": "Errore"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/ja/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "再試行中...",
 		"retryConnection": "再接続"
 	},
-	"refreshMCP": "MCPサーバーを更新"
+	"refreshMCP": "MCPサーバーを更新",
+	"execution": {
+		"running": "実行中",
+		"completed": "完了",
+		"error": "エラー"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/ko/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "다시 시도 중...",
 		"retryConnection": "연결 다시 시도"
 	},
-	"refreshMCP": "MCP 서버 새로 고침"
+	"refreshMCP": "MCP 서버 새로 고침",
+	"execution": {
+		"running": "실행 중",
+		"completed": "완료됨",
+		"error": "오류"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/nl/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "Opnieuw proberen...",
 		"retryConnection": "Verbinding opnieuw proberen"
 	},
-	"refreshMCP": "MCP-servers vernieuwen"
+	"refreshMCP": "MCP-servers vernieuwen",
+	"execution": {
+		"running": "Wordt uitgevoerd",
+		"completed": "Voltooid",
+		"error": "Fout"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/pl/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "Ponawianie...",
 		"retryConnection": "Ponów połączenie"
 	},
-	"refreshMCP": "Odśwież serwery MCP"
+	"refreshMCP": "Odśwież serwery MCP",
+	"execution": {
+		"running": "Uruchomione",
+		"completed": "Zakończone",
+		"error": "Błąd"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/pt-BR/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "Tentando novamente...",
 		"retryConnection": "Tentar reconectar"
 	},
-	"refreshMCP": "Atualizar Servidores MCP"
+	"refreshMCP": "Atualizar Servidores MCP",
+	"execution": {
+		"running": "Em execução",
+		"completed": "Concluído",
+		"error": "Erro"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/ru/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "Повторная попытка...",
 		"retryConnection": "Повторить подключение"
 	},
-	"refreshMCP": "Обновить MCP серверы"
+	"refreshMCP": "Обновить MCP серверы",
+	"execution": {
+		"running": "Выполняется",
+		"completed": "Завершено",
+		"error": "Ошибка"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/tr/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "Tekrar deneniyor...",
 		"retryConnection": "Bağlantıyı tekrar dene"
 	},
-	"refreshMCP": "MCP Sunucularını Yenile"
+	"refreshMCP": "MCP Sunucularını Yenile",
+	"execution": {
+		"running": "Çalışıyor",
+		"completed": "Tamamlandı",
+		"error": "Hata"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/vi/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "Đang thử lại...",
 		"retryConnection": "Thử kết nối lại"
 	},
-	"refreshMCP": "Làm mới Máy chủ MCP"
+	"refreshMCP": "Làm mới Máy chủ MCP",
+	"execution": {
+		"running": "Đang chạy",
+		"completed": "Hoàn thành",
+		"error": "Lỗi"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/zh-CN/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "重试中...",
 		"retryConnection": "重试连接"
 	},
-	"refreshMCP": "刷新 MCP 服务器"
+	"refreshMCP": "刷新 MCP 服务器",
+	"execution": {
+		"running": "运行中",
+		"completed": "已完成",
+		"error": "错误"
+	}
 }

+ 6 - 1
webview-ui/src/i18n/locales/zh-TW/mcp.json

@@ -54,5 +54,10 @@
 		"retrying": "重試中...",
 		"retryConnection": "重試連線"
 	},
-	"refreshMCP": "重新整理 MCP 伺服器"
+	"refreshMCP": "重新整理 MCP 伺服器",
+	"execution": {
+		"running": "執行中",
+		"completed": "已完成",
+		"error": "錯誤"
+	}
 }