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 <ricciodaniel98@gmail.com>
This commit is contained in:
Sam Hoang Van 2025-06-12 11:46:07 +07:00 committed by GitHub
parent 1b1e5a20f1
commit a9c1dad2ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 1249 additions and 275 deletions

View file

@ -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
packages/types/src/mcp.ts Normal file
View file

@ -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>

View file

@ -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)
})
})
})

View file

@ -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(() => {})
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
const completeMessage = JSON.stringify({
type: "use_mcp_tool",
serverName: server_name,
toolName: tool_name,
arguments: mcp_arguments,
} satisfies ClineAskUseMcpServer)
const didApprove = await askApproval("use_mcp_server", completeMessage)
if (!didApprove) {
return
}
// 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))
await handlePartialRequest(cline, params, removeClosingTag)
return
}
// Validate parameters
const validation = await validateParams(cline, params, pushToolResult)
if (!validation.isValid) {
return
}
const { serverName, toolName, parsedArguments } = validation
// Reset mistake count on successful validation
cline.consecutiveMistakeCount = 0
// Get user approval
const completeMessage = JSON.stringify({
type: "use_mcp_tool",
serverName,
toolName,
arguments: params.arguments,
} satisfies ClineAskUseMcpServer)
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
}
}

View file

@ -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}}...",

View file

@ -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...",

View file

@ -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...",

View file

@ -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}}...",

View file

@ -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}}...",

View file

@ -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 सर्वर पुनः प्रारंभ हो रहा है...",

View file

@ -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}}...",

View file

@ -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}}を再起動中...",

View file

@ -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 서버를 재시작하는 중...",

View file

@ -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...",

View file

@ -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}}...",

View file

@ -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}}...",

View file

@ -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}}...",

View file

@ -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...",

View file

@ -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}}...",

View file

@ -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服务器...",

View file

@ -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 伺服器...",

View file

@ -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 {

View file

@ -1,47 +1,183 @@
// npx jest src/shared/__tests__/combineCommandSequences.test.ts
import type { ClineMessage } from "@roo-code/types"
import { combineCommandSequences } from "../combineCommandSequences"
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",
},
]
import { ClineMessage } from "@roo-code/types"
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("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 },
]
const result = combineCommandSequences(messages)
expect(result).toHaveLength(1)
expect(result[0]).toEqual({
type: "ask",
ask: "command",
text: "ls\nOutput:file1.txt\nfile2.txt",
ts: 1625097600000,
})
})
})
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,
})
})
})
})

View file

@ -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
}
return msg
})
// 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 result
}

View file

@ -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: [

View file

@ -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 }

View file

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

View file

@ -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>
</>

View file

@ -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)

View file

@ -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)

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

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

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

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

View file

@ -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"
}
}

View file

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

View file

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

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

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

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

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

View file

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