From 9b0790fce73b14e56b1d4b7a50d898d8b9298a76 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sun, 30 Mar 2025 01:47:19 -0400 Subject: [PATCH] Move use_mcp_tool and access_mcp_resource to tool files (#2101) --- src/core/Cline.ts | 175 ++---------------------- src/core/tools/accessMcpResourceTool.ts | 77 +++++++++++ src/core/tools/useMcpToolTool.ts | 100 ++++++++++++++ 3 files changed, 190 insertions(+), 162 deletions(-) create mode 100644 src/core/tools/accessMcpResourceTool.ts create mode 100644 src/core/tools/useMcpToolTool.ts diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 3b4c683a01..e7b2694c7d 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -93,6 +93,8 @@ import { listCodeDefinitionNamesTool } from "./tools/listCodeDefinitionNamesTool import { searchFilesTool } from "./tools/searchFilesTool" import { browserActionTool } from "./tools/browserActionTool" import { executeCommandTool } from "./tools/executeCommandTool" +import { useMcpToolTool } from "./tools/useMcpToolTool" +import { accessMcpResourceTool } from "./tools/accessMcpResourceTool" export type ToolResponse = string | Array type UserContent = Array @@ -1630,170 +1632,19 @@ export class Cline extends EventEmitter { break } case "use_mcp_tool": { - 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 { - 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 this.ask("use_mcp_server", partialMessage, block.partial).catch(() => {}) - break - } else { - if (!server_name) { - this.consecutiveMistakeCount++ - pushToolResult( - await this.sayAndCreateMissingParamError("use_mcp_tool", "server_name"), - ) - break - } - if (!tool_name) { - this.consecutiveMistakeCount++ - pushToolResult( - await this.sayAndCreateMissingParamError("use_mcp_tool", "tool_name"), - ) - break - } - // arguments are optional, but if they are provided they must be valid JSON - // if (!mcp_arguments) { - // this.consecutiveMistakeCount++ - // pushToolResult(await this.sayAndCreateMissingParamError("use_mcp_tool", "arguments")) - // break - // } - let parsedArguments: Record | undefined - if (mcp_arguments) { - try { - parsedArguments = JSON.parse(mcp_arguments) - } catch (error) { - this.consecutiveMistakeCount++ - await this.say( - "error", - `Roo tried to use ${tool_name} with an invalid JSON argument. Retrying...`, - ) - pushToolResult( - formatResponse.toolError( - formatResponse.invalidMcpToolArgumentError(server_name, tool_name), - ), - ) - break - } - } - this.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) { - break - } - // now execute the tool - await this.say("mcp_server_request_started") // same as browser_action_result - const toolResult = await this.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 this.say("mcp_server_response", toolResultPretty) - pushToolResult(formatResponse.toolResult(toolResultPretty)) - break - } - } catch (error) { - await handleError("executing MCP tool", error) - break - } + await useMcpToolTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag) + break } case "access_mcp_resource": { - const server_name: string | undefined = block.params.server_name - const uri: string | undefined = block.params.uri - try { - if (block.partial) { - const partialMessage = JSON.stringify({ - type: "access_mcp_resource", - serverName: removeClosingTag("server_name", server_name), - uri: removeClosingTag("uri", uri), - } satisfies ClineAskUseMcpServer) - await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => {}) - break - } else { - if (!server_name) { - this.consecutiveMistakeCount++ - pushToolResult( - await this.sayAndCreateMissingParamError("access_mcp_resource", "server_name"), - ) - break - } - if (!uri) { - this.consecutiveMistakeCount++ - pushToolResult( - await this.sayAndCreateMissingParamError("access_mcp_resource", "uri"), - ) - break - } - this.consecutiveMistakeCount = 0 - const completeMessage = JSON.stringify({ - type: "access_mcp_resource", - serverName: server_name, - uri, - } satisfies ClineAskUseMcpServer) - const didApprove = await askApproval("use_mcp_server", completeMessage) - if (!didApprove) { - break - } - // now execute the tool - await this.say("mcp_server_request_started") - const resourceResult = await this.providerRef - .deref() - ?.getMcpHub() - ?.readResource(server_name, uri) - const resourceResultPretty = - resourceResult?.contents - .map((item) => { - if (item.text) { - return item.text - } - return "" - }) - .filter(Boolean) - .join("\n\n") || "(Empty response)" - - // handle images (image must contain mimetype and blob) - let images: string[] = [] - resourceResult?.contents.forEach((item) => { - if (item.mimeType?.startsWith("image") && item.blob) { - images.push(item.blob) - } - }) - await this.say("mcp_server_response", resourceResultPretty, images) - pushToolResult(formatResponse.toolResult(resourceResultPretty, images)) - break - } - } catch (error) { - await handleError("accessing MCP resource", error) - break - } + await accessMcpResourceTool( + this, + block, + askApproval, + handleError, + pushToolResult, + removeClosingTag, + ) + break } case "ask_followup_question": { const question: string | undefined = block.params.question diff --git a/src/core/tools/accessMcpResourceTool.ts b/src/core/tools/accessMcpResourceTool.ts new file mode 100644 index 0000000000..94bea9062c --- /dev/null +++ b/src/core/tools/accessMcpResourceTool.ts @@ -0,0 +1,77 @@ +import { ClineAskUseMcpServer } from "../../shared/ExtensionMessage" +import { RemoveClosingTag } from "./types" +import { ToolUse } from "../assistant-message" +import { AskApproval, HandleError, PushToolResult } from "./types" +import { Cline } from "../Cline" +import { formatResponse } from "../prompts/responses" + +export async function accessMcpResourceTool( + cline: Cline, + block: ToolUse, + askApproval: AskApproval, + handleError: HandleError, + pushToolResult: PushToolResult, + removeClosingTag: RemoveClosingTag, +) { + const server_name: string | undefined = block.params.server_name + const uri: string | undefined = block.params.uri + try { + if (block.partial) { + const partialMessage = JSON.stringify({ + type: "access_mcp_resource", + serverName: removeClosingTag("server_name", server_name), + uri: removeClosingTag("uri", uri), + } satisfies ClineAskUseMcpServer) + await cline.ask("use_mcp_server", partialMessage, block.partial).catch(() => {}) + return + } else { + if (!server_name) { + cline.consecutiveMistakeCount++ + pushToolResult(await cline.sayAndCreateMissingParamError("access_mcp_resource", "server_name")) + return + } + if (!uri) { + cline.consecutiveMistakeCount++ + pushToolResult(await cline.sayAndCreateMissingParamError("access_mcp_resource", "uri")) + return + } + cline.consecutiveMistakeCount = 0 + const completeMessage = JSON.stringify({ + type: "access_mcp_resource", + serverName: server_name, + uri, + } satisfies ClineAskUseMcpServer) + const didApprove = await askApproval("use_mcp_server", completeMessage) + if (!didApprove) { + return + } + // now execute the tool + await cline.say("mcp_server_request_started") + const resourceResult = await cline.providerRef.deref()?.getMcpHub()?.readResource(server_name, uri) + const resourceResultPretty = + resourceResult?.contents + .map((item) => { + if (item.text) { + return item.text + } + return "" + }) + .filter(Boolean) + .join("\n\n") || "(Empty response)" + + // handle images (image must contain mimetype and blob) + let images: string[] = [] + resourceResult?.contents.forEach((item) => { + if (item.mimeType?.startsWith("image") && item.blob) { + images.push(item.blob) + } + }) + await cline.say("mcp_server_response", resourceResultPretty, images) + pushToolResult(formatResponse.toolResult(resourceResultPretty, images)) + return + } + } catch (error) { + await handleError("accessing MCP resource", error) + return + } +} diff --git a/src/core/tools/useMcpToolTool.ts b/src/core/tools/useMcpToolTool.ts new file mode 100644 index 0000000000..699f693a13 --- /dev/null +++ b/src/core/tools/useMcpToolTool.ts @@ -0,0 +1,100 @@ +import { Cline } from "../Cline" +import { ToolUse } from "../assistant-message" +import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "./types" +import { formatResponse } from "../prompts/responses" +import { ClineAskUseMcpServer } from "../../shared/ExtensionMessage" + +export async function useMcpToolTool( + cline: Cline, + block: ToolUse, + askApproval: AskApproval, + handleError: HandleError, + 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 { + 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++ + pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "server_name")) + return + } + if (!tool_name) { + cline.consecutiveMistakeCount++ + pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "tool_name")) + return + } + // arguments are optional, but if they are provided they must be valid JSON + // if (!mcp_arguments) { + // cline.consecutiveMistakeCount++ + // pushToolResult(await cline.sayAndCreateMissingParamError("use_mcp_tool", "arguments")) + // return + // } + let parsedArguments: Record | undefined + if (mcp_arguments) { + try { + parsedArguments = JSON.parse(mcp_arguments) + } catch (error) { + cline.consecutiveMistakeCount++ + 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)) + return + } + } catch (error) { + await handleError("executing MCP tool", error) + return + } +}