Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions src/core/tools/__tests__/useMcpToolTool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,119 @@ describe("useMcpToolTool", () => {
expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("invalid JSON argument"))
expect(mockPushToolResult).toHaveBeenCalledWith("Tool error: Invalid args for test_server:test_tool")
})

it("should warn about large but valid JSON arguments", async () => {
// Create a large JSON string (over 3800 characters)
const largeData = { data: "x".repeat(3850) }
const largeJson = JSON.stringify(largeData)

const block: ToolUse = {
type: "tool_use",
name: "use_mcp_tool",
params: {
server_name: "test_server",
tool_name: "test_tool",
arguments: largeJson,
},
partial: false,
}

mockAskApproval.mockResolvedValue(true)
const mockToolResult = {
content: [{ type: "text", text: "Tool executed successfully" }],
isError: false,
}
mockProviderRef.deref.mockReturnValue({
getMcpHub: () => ({
callTool: vi.fn().mockResolvedValue(mockToolResult),
}),
postMessageToWebview: vi.fn(),
})

await useMcpToolTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)

// Should warn about large arguments
expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("Warning:"))
expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("very large"))
expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("4000 characters"))

// But should still execute successfully
expect(mockTask.consecutiveMistakeCount).toBe(0)
expect(mockAskApproval).toHaveBeenCalled()
expect(mockPushToolResult).toHaveBeenCalledWith("Tool result: Tool executed successfully")
})

it("should detect truncated JSON arguments", async () => {
// Create a truncated JSON string that looks like it was cut off
const truncatedJson = '{"data": "' + "x".repeat(3950) // No closing quote or brace

const block: ToolUse = {
type: "tool_use",
name: "use_mcp_tool",
params: {
server_name: "test_server",
tool_name: "test_tool",
arguments: truncatedJson,
},
partial: false,
}

await useMcpToolTool(
mockTask as Task,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)

expect(mockTask.consecutiveMistakeCount).toBe(1)
expect(mockTask.recordToolError).toHaveBeenCalledWith("use_mcp_tool")

// Should show truncation-specific error message
expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("truncated"))
expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("4000 characters"))
expect(mockTask.say).toHaveBeenCalledWith("error", expect.stringContaining("smaller chunks"))

expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("truncated by the language model"))
})

it("should handle JSON ending with incomplete array", async () => {
// Create a JSON string that ends with an incomplete array
const truncatedJson = '{"nodes": [{"id": 1}, {"id": 2}, {"id"' + "x".repeat(3920)

const block: ToolUse = {
type: "tool_use",
name: "use_mcp_tool",
params: {
server_name: "test_server",
tool_name: "test_tool",
arguments: truncatedJson,
},
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("truncated"))
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("truncated by the language model"))
})
})

describe("partial requests", () => {
Expand Down
77 changes: 64 additions & 13 deletions src/core/tools/useMcpToolTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,70 @@ async function validateParams(
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 }
// Check if arguments appear to be truncated (common around 4000 characters)
const argLength = params.arguments.length
const TRUNCATION_WARNING_THRESHOLD = 3800 // Warn when close to 4000 chars
const LIKELY_TRUNCATED_THRESHOLD = 3900 // Very likely truncated if over this

// Check for signs of truncation
const lastChar = params.arguments[params.arguments.length - 1]
const endsWithIncompleteJSON =
argLength > LIKELY_TRUNCATED_THRESHOLD && lastChar !== "}" && lastChar !== "]" && lastChar !== '"'

if (endsWithIncompleteJSON || argLength > TRUNCATION_WARNING_THRESHOLD) {
// Try to parse anyway to see if it's valid JSON
try {
parsedArguments = JSON.parse(params.arguments)

// Valid JSON but very large - warn the user
if (argLength > TRUNCATION_WARNING_THRESHOLD) {
await cline.say(
"error",
`⚠️ Warning: The MCP tool arguments are very large (${argLength} characters). ` +
`Some language models may truncate tool calls around 4000 characters. ` +
`Consider breaking this into smaller operations if the tool fails.`,
)
}
} catch (error) {
// Invalid JSON and likely truncated
cline.consecutiveMistakeCount++
cline.recordToolError("use_mcp_tool")

const errorMessage =
argLength > LIKELY_TRUNCATED_THRESHOLD
? `The MCP tool arguments appear to be truncated (${argLength} characters). ` +
`The JSON is incomplete and cannot be parsed. ` +
`This is a known limitation where some language models truncate tool calls around 4000 characters. ` +
`Please try breaking this operation into smaller chunks or reducing the data size.`
: t("mcp:errors.invalidJsonArgument", { toolName: params.tool_name })

await cline.say("error", errorMessage)

pushToolResult(
formatResponse.toolError(
argLength > LIKELY_TRUNCATED_THRESHOLD
? `Tool arguments were truncated by the language model. The JSON is incomplete (${argLength} characters). Please use smaller data chunks.`
: formatResponse.invalidMcpToolArgumentError(params.server_name, params.tool_name),
),
)
return { isValid: false }
}
} else {
// Normal JSON parsing for smaller 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 }
}
}
}

Expand Down