Skip to content

Commit 1a1fc31

Browse files
committed
fix: improve MCP tool error handling with retry logic and better messages
- Add retry logic with exponential backoff (up to 3 retries) for transient failures - Implement comprehensive error handling with user-friendly messages - Add parameter validation before MCP tool execution - Enhance error messages to provide actionable guidance - Check server connection status before attempting tool calls - Add localized error messages for better user experience Fixes #6189
1 parent 2170c61 commit 1a1fc31

File tree

5 files changed

+171
-54
lines changed

5 files changed

+171
-54
lines changed

src/core/tools/__tests__/useMcpToolTool.spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,13 @@ describe("useMcpToolTool", () => {
136136

137137
expect(mockTask.consecutiveMistakeCount).toBe(1)
138138
expect(mockTask.recordToolError).toHaveBeenCalledWith("use_mcp_tool")
139-
expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("invalid JSON argument"))
140-
expect(mockPushToolResult).toHaveBeenCalledWith("Tool error: Invalid args for test_server:test_tool")
139+
expect(mockTask.say).toHaveBeenCalledWith(
140+
"error",
141+
expect.stringContaining("Invalid JSON arguments for tool 'test_tool'"),
142+
)
143+
expect(mockPushToolResult).toHaveBeenCalledWith(
144+
expect.stringContaining("Invalid JSON arguments for test_server.test_tool"),
145+
)
141146
})
142147
})
143148

src/core/tools/useMcpToolTool.ts

Lines changed: 92 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -54,29 +54,54 @@ async function validateParams(
5454
return { isValid: false }
5555
}
5656

57+
// Validate server name format
58+
const serverName = params.server_name.trim()
59+
if (!serverName) {
60+
cline.consecutiveMistakeCount++
61+
cline.recordToolError("use_mcp_tool")
62+
await cline.say("error", "Server name cannot be empty or contain only whitespace")
63+
pushToolResult(formatResponse.toolError("Invalid server name: cannot be empty"))
64+
return { isValid: false }
65+
}
66+
67+
// Validate tool name format
68+
const toolName = params.tool_name.trim()
69+
if (!toolName) {
70+
cline.consecutiveMistakeCount++
71+
cline.recordToolError("use_mcp_tool")
72+
await cline.say("error", "Tool name cannot be empty or contain only whitespace")
73+
pushToolResult(formatResponse.toolError("Invalid tool name: cannot be empty"))
74+
return { isValid: false }
75+
}
76+
5777
let parsedArguments: Record<string, unknown> | undefined
5878

5979
if (params.arguments) {
6080
try {
6181
parsedArguments = JSON.parse(params.arguments)
82+
83+
// Validate that arguments is an object (not array or primitive)
84+
if ((parsedArguments !== null && typeof parsedArguments !== "object") || Array.isArray(parsedArguments)) {
85+
throw new Error("Arguments must be a JSON object, not an array or primitive value")
86+
}
6287
} catch (error) {
6388
cline.consecutiveMistakeCount++
6489
cline.recordToolError("use_mcp_tool")
65-
await cline.say("error", t("mcp:errors.invalidJsonArgument", { toolName: params.tool_name }))
90+
91+
const errorMessage = error instanceof Error ? error.message : "Invalid JSON"
92+
await cline.say("error", `Invalid JSON arguments for tool '${toolName}': ${errorMessage}`)
6693

6794
pushToolResult(
68-
formatResponse.toolError(
69-
formatResponse.invalidMcpToolArgumentError(params.server_name, params.tool_name),
70-
),
95+
formatResponse.toolError(`Invalid JSON arguments for ${serverName}.${toolName}: ${errorMessage}`),
7196
)
7297
return { isValid: false }
7398
}
7499
}
75100

76101
return {
77102
isValid: true,
78-
serverName: params.server_name,
79-
toolName: params.tool_name,
103+
serverName,
104+
toolName,
80105
parsedArguments,
81106
}
82107
}
@@ -127,41 +152,74 @@ async function executeToolAndProcessResult(
127152
toolName,
128153
})
129154

130-
const toolResult = await cline.providerRef.deref()?.getMcpHub()?.callTool(serverName, toolName, parsedArguments)
155+
let retryCount = 0
156+
const maxRetries = 3
157+
let lastError: Error | null = null
131158

132-
let toolResultPretty = "(No response)"
159+
while (retryCount <= maxRetries) {
160+
try {
161+
const mcpHub = cline.providerRef.deref()?.getMcpHub()
162+
if (!mcpHub) {
163+
throw new Error("MCP Hub is not available. Please ensure MCP servers are properly configured.")
164+
}
133165

134-
if (toolResult) {
135-
const outputText = processToolContent(toolResult)
166+
const toolResult = await mcpHub.callTool(serverName, toolName, parsedArguments)
136167

137-
if (outputText) {
138-
await sendExecutionStatus(cline, {
139-
executionId,
140-
status: "output",
141-
response: outputText,
142-
})
168+
if (toolResult) {
169+
const outputText = processToolContent(toolResult)
143170

144-
toolResultPretty = (toolResult.isError ? "Error:\n" : "") + outputText
145-
}
171+
if (outputText) {
172+
await sendExecutionStatus(cline, {
173+
executionId,
174+
status: "output",
175+
response: outputText,
176+
})
146177

147-
// Send completion status
148-
await sendExecutionStatus(cline, {
149-
executionId,
150-
status: toolResult.isError ? "error" : "completed",
151-
response: toolResultPretty,
152-
error: toolResult.isError ? "Error executing MCP tool" : undefined,
153-
})
154-
} else {
155-
// Send error status if no result
156-
await sendExecutionStatus(cline, {
157-
executionId,
158-
status: "error",
159-
error: "No response from MCP server",
160-
})
178+
const toolResultPretty = (toolResult.isError ? "Error:\n" : "") + outputText
179+
180+
// Send completion status
181+
await sendExecutionStatus(cline, {
182+
executionId,
183+
status: toolResult.isError ? "error" : "completed",
184+
response: toolResultPretty,
185+
error: toolResult.isError ? outputText : undefined,
186+
})
187+
188+
await cline.say("mcp_server_response", toolResultPretty)
189+
pushToolResult(formatResponse.toolResult(toolResultPretty))
190+
return
191+
}
192+
}
193+
194+
// If we get here, toolResult was null/undefined
195+
throw new Error(`No response received from MCP server '${serverName}' for tool '${toolName}'`)
196+
} catch (error) {
197+
lastError = error instanceof Error ? error : new Error(String(error))
198+
retryCount++
199+
200+
if (retryCount <= maxRetries) {
201+
const delay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000) // Exponential backoff with max 5s
202+
await cline.say(
203+
"error",
204+
`MCP tool execution failed (attempt ${retryCount}/${maxRetries}). Retrying in ${delay / 1000}s...`,
205+
)
206+
await new Promise((resolve) => setTimeout(resolve, delay))
207+
}
208+
}
161209
}
162210

163-
await cline.say("mcp_server_response", toolResultPretty)
164-
pushToolResult(formatResponse.toolResult(toolResultPretty))
211+
// All retries failed
212+
const errorMessage = lastError?.message || "Unknown error occurred"
213+
const userFriendlyError = `Failed to execute MCP tool '${toolName}' on server '${serverName}' after ${maxRetries} attempts. ${errorMessage}`
214+
215+
await sendExecutionStatus(cline, {
216+
executionId,
217+
status: "error",
218+
error: userFriendlyError,
219+
})
220+
221+
await cline.say("mcp_server_response", `Error: ${userFriendlyError}`)
222+
pushToolResult(formatResponse.toolError(userFriendlyError))
165223
}
166224

167225
export async function useMcpToolTool(

src/i18n/locales/en/mcp.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"invalid_settings_validation": "Invalid MCP settings format: {{errorMessages}}",
66
"create_json": "Failed to create or open .roo/mcp.json: {{error}}",
77
"failed_update_project": "Failed to update project MCP servers",
8-
"invalidJsonArgument": "Roo tried to use {{toolName}} with an invalid JSON argument. Retrying..."
8+
"invalidJsonArgument": "Roo tried to use {{toolName}} with an invalid JSON argument. Retrying...",
9+
"connection_failed": "Failed to connect to MCP server '{{serverName}}'. Please check your server configuration and ensure the server is running.",
10+
"tool_execution_failed": "Failed to execute MCP tool '{{toolName}}' on server '{{serverName}}': {{error}}"
911
},
1012
"info": {
1113
"server_restarting": "Restarting {{serverName}} MCP server...",

src/services/mcp/McpHub.ts

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1464,12 +1464,39 @@ export class McpHub {
14641464
): Promise<McpToolCallResponse> {
14651465
const connection = this.findConnection(serverName, source)
14661466
if (!connection) {
1467+
// Provide helpful suggestions
1468+
const availableServers = this.getAllServers()
1469+
.filter((s) => !s.disabled)
1470+
.map((s) => s.name)
1471+
.join(", ")
1472+
1473+
const errorMsg =
1474+
availableServers.length > 0
1475+
? `MCP server '${serverName}' not found. Available servers: ${availableServers}`
1476+
: `No MCP servers are currently connected. Please configure MCP servers in your settings.`
1477+
1478+
throw new Error(errorMsg)
1479+
}
1480+
1481+
if (connection.server.disabled) {
14671482
throw new Error(
1468-
`No connection found for server: ${serverName}${source ? ` with source ${source}` : ""}. Please make sure to use MCP servers available under 'Connected MCP Servers'.`,
1483+
`MCP server '${serverName}' is currently disabled. Please enable it in the MCP settings to use it.`,
14691484
)
14701485
}
1471-
if (connection.server.disabled) {
1472-
throw new Error(`Server "${serverName}" is disabled and cannot be used`)
1486+
1487+
// Check if server is connected
1488+
if (connection.server.status !== "connected") {
1489+
throw new Error(
1490+
`MCP server '${serverName}' is not connected (status: ${connection.server.status}). Please check the server configuration and logs.`,
1491+
)
1492+
}
1493+
1494+
// Validate tool exists
1495+
const availableTools = connection.server.tools || []
1496+
const toolExists = availableTools.some((t) => t.name === toolName)
1497+
if (!toolExists && availableTools.length > 0) {
1498+
const toolNames = availableTools.map((t) => t.name).join(", ")
1499+
throw new Error(`Tool '${toolName}' not found on server '${serverName}'. Available tools: ${toolNames}`)
14731500
}
14741501

14751502
let timeout: number
@@ -1482,19 +1509,44 @@ export class McpHub {
14821509
timeout = 60 * 1000
14831510
}
14841511

1485-
return await connection.client.request(
1486-
{
1487-
method: "tools/call",
1488-
params: {
1489-
name: toolName,
1490-
arguments: toolArguments,
1512+
try {
1513+
const result = await connection.client.request(
1514+
{
1515+
method: "tools/call",
1516+
params: {
1517+
name: toolName,
1518+
arguments: toolArguments,
1519+
},
14911520
},
1492-
},
1493-
CallToolResultSchema,
1494-
{
1495-
timeout,
1496-
},
1497-
)
1521+
CallToolResultSchema,
1522+
{
1523+
timeout,
1524+
},
1525+
)
1526+
1527+
return result
1528+
} catch (error) {
1529+
// Enhance error messages for common issues
1530+
if (error instanceof Error) {
1531+
if (error.message.includes("timeout")) {
1532+
throw new Error(
1533+
`MCP tool '${toolName}' on server '${serverName}' timed out after ${timeout / 1000} seconds. The tool may be taking too long to respond or the server may be unresponsive.`,
1534+
)
1535+
}
1536+
if (error.message.includes("ECONNREFUSED")) {
1537+
throw new Error(
1538+
`Cannot connect to MCP server '${serverName}'. The server may not be running or may be configured incorrectly.`,
1539+
)
1540+
}
1541+
if (error.message.includes("EPIPE") || error.message.includes("ENOTCONN")) {
1542+
throw new Error(
1543+
`Lost connection to MCP server '${serverName}'. The server may have crashed or been terminated.`,
1544+
)
1545+
}
1546+
}
1547+
// Re-throw with original error if not a known issue
1548+
throw error
1549+
}
14981550
}
14991551

15001552
/**

src/services/mcp/__tests__/McpHub.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,7 @@ describe("McpHub", () => {
545545
mcpHub.connections = [mockConnection]
546546

547547
await expect(mcpHub.callTool("disabled-server", "some-tool", {})).rejects.toThrow(
548-
'Server "disabled-server" is disabled and cannot be used',
548+
"MCP server 'disabled-server' is currently disabled. Please enable it in the MCP settings to use it.",
549549
)
550550
})
551551

@@ -610,7 +610,7 @@ describe("McpHub", () => {
610610

611611
it("should throw error if server not found", async () => {
612612
await expect(mcpHub.callTool("non-existent-server", "some-tool", {})).rejects.toThrow(
613-
"No connection found for server: non-existent-server",
613+
"No MCP servers are currently connected. Please configure MCP servers in your settings.",
614614
)
615615
})
616616

0 commit comments

Comments
 (0)