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