mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-21 04:41:16 -05:00
Add use_mcp_tool and access_mcp_resource tools
This commit is contained in:
@@ -2,13 +2,22 @@ import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/reac
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import React, { memo, useEffect, useMemo, useRef } from "react"
|
||||
import { useSize } from "react-use"
|
||||
import { ClineApiReqInfo, ClineMessage, ClineSayTool } from "../../../../src/shared/ExtensionMessage"
|
||||
import {
|
||||
ClineApiReqInfo,
|
||||
ClineAskUseMcpServer,
|
||||
ClineMessage,
|
||||
ClineSayTool,
|
||||
} from "../../../../src/shared/ExtensionMessage"
|
||||
import { COMMAND_OUTPUT_STRING } from "../../../../src/shared/combineCommandSequences"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { findMatchingResourceOrTemplate } from "../../utils/mcp"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
import MarkdownBlock from "../common/MarkdownBlock"
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
import McpResourceRow from "../mcp/McpResourceRow"
|
||||
import McpToolRow from "../mcp/McpToolRow"
|
||||
import { highlightMentions } from "./TaskHeader"
|
||||
|
||||
interface ChatRowProps {
|
||||
@@ -67,6 +76,7 @@ export const ChatRowContent = ({
|
||||
lastModifiedMessage,
|
||||
isLast,
|
||||
}: ChatRowContentProps) => {
|
||||
const { mcpServers } = useExtensionState()
|
||||
const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
|
||||
if (message.text != null && message.say === "api_req_started") {
|
||||
const info: ClineApiReqInfo = JSON.parse(message.text)
|
||||
@@ -81,6 +91,9 @@ export const ChatRowContent = ({
|
||||
: undefined
|
||||
const isCommandExecuting =
|
||||
isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
|
||||
|
||||
const isMcpServerResponding = isLast && lastModifiedMessage?.say === "mcp_server_request_started"
|
||||
|
||||
const type = message.type === "ask" ? message.ask : message.say
|
||||
|
||||
const normalColor = "var(--vscode-foreground)"
|
||||
@@ -117,6 +130,20 @@ export const ChatRowContent = ({
|
||||
Cline wants to execute this command:
|
||||
</span>,
|
||||
]
|
||||
case "use_mcp_server":
|
||||
const mcpServerUse = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
|
||||
return [
|
||||
isMcpServerResponding ? (
|
||||
<ProgressIndicator />
|
||||
) : (
|
||||
<span
|
||||
className="codicon codicon-server"
|
||||
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
|
||||
),
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>
|
||||
Cline wants to use the <code>{mcpServerUse.serverName}</code> MCP server:
|
||||
</span>,
|
||||
]
|
||||
case "completion_result":
|
||||
return [
|
||||
<span
|
||||
@@ -181,7 +208,15 @@ export const ChatRowContent = ({
|
||||
default:
|
||||
return [null, null]
|
||||
}
|
||||
}, [type, cost, apiRequestFailedMessage, isCommandExecuting, apiReqCancelReason])
|
||||
}, [
|
||||
type,
|
||||
cost,
|
||||
apiRequestFailedMessage,
|
||||
isCommandExecuting,
|
||||
apiReqCancelReason,
|
||||
isMcpServerResponding,
|
||||
message.text,
|
||||
])
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
@@ -617,6 +652,23 @@ export const ChatRowContent = ({
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
case "mcp_server_response":
|
||||
return (
|
||||
<>
|
||||
<div style={{ paddingTop: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "4px",
|
||||
opacity: 0.8,
|
||||
fontSize: "12px",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
Response
|
||||
</div>
|
||||
<CodeBlock source={`${"```"}json\n${message.text}\n${"```"}`} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
@@ -716,6 +768,73 @@ export const ChatRowContent = ({
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
case "use_mcp_server":
|
||||
const useMcpServer = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
|
||||
const server = mcpServers.find((server) => server.name === useMcpServer.serverName)
|
||||
return (
|
||||
<>
|
||||
<div style={headerStyle}>
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "var(--vscode-textCodeBlock-background)",
|
||||
borderRadius: "3px",
|
||||
padding: "8px 10px",
|
||||
marginTop: "8px",
|
||||
}}>
|
||||
{useMcpServer.type === "access_mcp_resource" && (
|
||||
<McpResourceRow
|
||||
item={{
|
||||
// Always use the actual URI from the request
|
||||
uri: useMcpServer.uri || "",
|
||||
// Use the matched resource/template details, with fallbacks
|
||||
...(findMatchingResourceOrTemplate(
|
||||
useMcpServer.uri || "",
|
||||
server?.resources,
|
||||
server?.resourceTemplates,
|
||||
) || {
|
||||
name: "",
|
||||
mimeType: "",
|
||||
description: "",
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{useMcpServer.type === "use_mcp_tool" && (
|
||||
<>
|
||||
<McpToolRow
|
||||
tool={{
|
||||
name: useMcpServer.toolName || "",
|
||||
description:
|
||||
server?.tools?.find((tool) => tool.name === useMcpServer.toolName)
|
||||
?.description || "",
|
||||
}}
|
||||
/>
|
||||
{useMcpServer.arguments && (
|
||||
<div style={{ marginTop: "6px" }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "4px",
|
||||
opacity: 0.8,
|
||||
fontSize: "11px",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
Arguments
|
||||
</div>
|
||||
<CodeBlock
|
||||
source={`${"```"}json\n${useMcpServer.arguments}\n${"```"}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
case "completion_result":
|
||||
if (message.text) {
|
||||
return (
|
||||
|
||||
@@ -133,6 +133,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
setPrimaryButtonText("Proceed While Running")
|
||||
setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "use_mcp_server":
|
||||
setTextAreaDisabled(isPartial)
|
||||
setClineAsk("use_mcp_server")
|
||||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText("Approve")
|
||||
setSecondaryButtonText("Reject")
|
||||
break
|
||||
case "completion_result":
|
||||
// extension waiting for feedback. but we can just present a new task button
|
||||
setTextAreaDisabled(isPartial)
|
||||
@@ -179,6 +186,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "browser_action":
|
||||
case "browser_action_result":
|
||||
case "command_output":
|
||||
case "mcp_server_request_started":
|
||||
case "mcp_server_response":
|
||||
case "completion_result":
|
||||
case "tool":
|
||||
break
|
||||
@@ -247,6 +256,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "browser_action_launch":
|
||||
case "command": // user can provide feedback to a tool or command use
|
||||
case "command_output": // user can send input to command stdin
|
||||
case "use_mcp_server":
|
||||
case "completion_result": // if this happens then the user has feedback for the completion result
|
||||
case "resume_task":
|
||||
case "resume_completed_task":
|
||||
@@ -288,6 +298,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "command_output":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
case "resume_task":
|
||||
case "mistake_limit_reached":
|
||||
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
|
||||
@@ -321,6 +332,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
case "command":
|
||||
case "tool":
|
||||
case "browser_action_launch":
|
||||
case "use_mcp_server":
|
||||
// responds to the API with a "This operation failed" and lets it try again
|
||||
vscode.postMessage({ type: "askResponse", askResponse: "noButtonClicked" })
|
||||
break
|
||||
@@ -436,6 +448,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
return false
|
||||
}
|
||||
break
|
||||
case "mcp_server_request_started":
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
62
webview-ui/src/components/mcp/McpResourceRow.tsx
Normal file
62
webview-ui/src/components/mcp/McpResourceRow.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { McpResource, McpResourceTemplate } from "../../../../src/shared/mcp"
|
||||
|
||||
type McpResourceRowProps = {
|
||||
item: McpResource | McpResourceTemplate
|
||||
}
|
||||
|
||||
const McpResourceRow = ({ item }: McpResourceRowProps) => {
|
||||
const isTemplate = "uriTemplate" in item
|
||||
const uri = isTemplate ? item.uriTemplate : item.uri
|
||||
|
||||
return (
|
||||
<div
|
||||
key={uri}
|
||||
style={{
|
||||
padding: "8px 0",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: "4px",
|
||||
}}>
|
||||
<span
|
||||
className={`codicon codicon-symbol-${isTemplate ? "template" : "file"}`}
|
||||
style={{ marginRight: "6px" }}
|
||||
/>
|
||||
<span style={{ fontWeight: 500, wordBreak: "break-all" }}>{uri}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
opacity: 0.8,
|
||||
margin: "4px 0",
|
||||
}}>
|
||||
{item.name && item.description
|
||||
? `${item.name}: ${item.description}`
|
||||
: !item.name && item.description
|
||||
? item.description
|
||||
: !item.description && item.name
|
||||
? item.name
|
||||
: "No description"}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
}}>
|
||||
<span style={{ opacity: 0.8 }}>Returns </span>
|
||||
<code
|
||||
style={{
|
||||
color: "var(--vscode-textPreformat-foreground)",
|
||||
background: "var(--vscode-textPreformat-background)",
|
||||
padding: "1px 4px",
|
||||
borderRadius: "3px",
|
||||
}}>
|
||||
{item.mimeType || "Unknown"}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpResourceRow
|
||||
75
webview-ui/src/components/mcp/McpToolRow.tsx
Normal file
75
webview-ui/src/components/mcp/McpToolRow.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { McpTool } from "../../../../src/shared/mcp"
|
||||
import { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||
|
||||
type McpToolRowProps = {
|
||||
tool: McpTool
|
||||
}
|
||||
|
||||
const McpToolRow = ({ tool }: McpToolRowProps) => {
|
||||
return (
|
||||
<div
|
||||
key={tool.name}
|
||||
style={{
|
||||
padding: "8px 0",
|
||||
}}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<span className="codicon codicon-symbol-method" style={{ marginRight: "6px" }}></span>
|
||||
<span style={{ fontWeight: 500 }}>{tool.name}</span>
|
||||
</div>
|
||||
{tool.description && (
|
||||
<div
|
||||
style={{
|
||||
marginLeft: "0px",
|
||||
marginTop: "4px",
|
||||
opacity: 0.8,
|
||||
fontSize: "12px",
|
||||
}}>
|
||||
{tool.description}
|
||||
</div>
|
||||
)}
|
||||
{tool.inputSchema && "properties" in tool.inputSchema && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: "8px",
|
||||
fontSize: "12px",
|
||||
background: CODE_BLOCK_BG_COLOR,
|
||||
borderRadius: "3px",
|
||||
padding: "8px",
|
||||
}}>
|
||||
<div style={{ marginBottom: "4px", opacity: 0.8, fontSize: "11px", textTransform: "uppercase" }}>
|
||||
Parameters
|
||||
</div>
|
||||
{Object.entries(tool.inputSchema.properties as Record<string, any>).map(([paramName, schema]) => {
|
||||
const isRequired =
|
||||
tool.inputSchema &&
|
||||
"required" in tool.inputSchema &&
|
||||
Array.isArray(tool.inputSchema.required) &&
|
||||
tool.inputSchema.required.includes(paramName)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={paramName}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
marginTop: "4px",
|
||||
}}>
|
||||
<code
|
||||
style={{
|
||||
color: "var(--vscode-textPreformat-foreground)",
|
||||
marginRight: "8px",
|
||||
}}>
|
||||
{paramName}
|
||||
{isRequired && <span style={{ color: "var(--vscode-errorForeground)" }}>*</span>}
|
||||
</code>
|
||||
<span style={{ opacity: 0.8 }}>{schema.description || "No description"}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpToolRow
|
||||
@@ -3,6 +3,8 @@ import { useState } from "react"
|
||||
import { vscode } from "../../utils/vscode"
|
||||
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||
import { McpServer } from "../../../../src/shared/mcp"
|
||||
import McpToolRow from "./McpToolRow"
|
||||
import McpResourceRow from "./McpResourceRow"
|
||||
|
||||
type McpViewProps = {
|
||||
onDone: () => void
|
||||
@@ -162,7 +164,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "8px",
|
||||
background: "var(--vscode-list-hoverBackground)",
|
||||
background: "var(--vscode-textCodeBlock-background)",
|
||||
cursor: server.error ? "default" : "pointer",
|
||||
borderRadius: isExpanded || server.error ? "4px 4px 0 0" : "4px",
|
||||
}}
|
||||
@@ -190,7 +192,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
style={{
|
||||
padding: "8px",
|
||||
fontSize: "13px",
|
||||
background: "var(--vscode-list-hoverBackground)",
|
||||
background: "var(--vscode-textCodeBlock-background)",
|
||||
borderRadius: "0 0 4px 4px",
|
||||
}}>
|
||||
<div style={{ color: "var(--vscode-testing-iconFailed)", marginBottom: "8px" }}>{server.error}</div>
|
||||
@@ -203,7 +205,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
isExpanded && (
|
||||
<div
|
||||
style={{
|
||||
background: "var(--vscode-list-hoverBackground)",
|
||||
background: "var(--vscode-textCodeBlock-background)",
|
||||
padding: "0 12px 0 12px",
|
||||
fontSize: "13px",
|
||||
borderRadius: "0 0 4px 4px",
|
||||
@@ -214,29 +216,10 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
|
||||
<VSCodePanelView id="tools-view">
|
||||
{server.tools && server.tools.length > 0 ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "3px" }}>
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: "3px", width: "100%" }}>
|
||||
{server.tools.map((tool) => (
|
||||
<div
|
||||
key={tool.name}
|
||||
style={{
|
||||
padding: "8px 0",
|
||||
}}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<span
|
||||
className="codicon codicon-symbol-method"
|
||||
style={{ marginRight: "6px" }}></span>
|
||||
<span style={{ fontWeight: 500 }}>{tool.name}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginLeft: "0px",
|
||||
marginTop: "4px",
|
||||
opacity: 0.8,
|
||||
fontSize: "12px",
|
||||
}}>
|
||||
{tool.description}
|
||||
</div>
|
||||
</div>
|
||||
<McpToolRow key={tool.name} tool={tool} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -246,35 +229,19 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
)}
|
||||
</VSCodePanelView>
|
||||
|
||||
{/* Resources Panel View */}
|
||||
<VSCodePanelView id="resources-view">
|
||||
{server.resources && server.resources.length > 0 ? (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "3px" }}>
|
||||
{server.resources.map((resource) => (
|
||||
<div
|
||||
key={resource.uri}
|
||||
style={{
|
||||
padding: "8px 0",
|
||||
}}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<span
|
||||
className="codicon codicon-symbol-file"
|
||||
style={{ marginRight: "6px" }}></span>
|
||||
<span style={{ fontWeight: 500 }}>{resource.name}</span>
|
||||
</div>
|
||||
<div style={{ marginTop: "6px", fontSize: "12px" }}>
|
||||
<code
|
||||
style={{
|
||||
color: "var(--vscode-textPreformat-foreground)",
|
||||
background: "var(--vscode-textPreformat-background)",
|
||||
padding: "2px 4px",
|
||||
borderRadius: "3px",
|
||||
}}>
|
||||
{resource.uri}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{(server.resources && server.resources.length > 0) ||
|
||||
(server.resourceTemplates && server.resourceTemplates.length > 0) ? (
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: "3px", width: "100%" }}>
|
||||
{[...(server.resourceTemplates || []), ...(server.resources || [])].map(
|
||||
(item) => (
|
||||
<McpResourceRow
|
||||
key={"uriTemplate" in item ? item.uriTemplate : item.uri}
|
||||
item={item}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: "10px 0", color: "var(--vscode-descriptionForeground)" }}>
|
||||
|
||||
47
webview-ui/src/utils/mcp.ts
Normal file
47
webview-ui/src/utils/mcp.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { McpResource, McpResourceTemplate } from "../../../src/shared/mcp"
|
||||
|
||||
/**
|
||||
* Matches a URI against an array of URI templates and returns the matching template
|
||||
* @param uri The URI to match
|
||||
* @param templates Array of URI templates to match against
|
||||
* @returns The matching template or undefined if no match is found
|
||||
*/
|
||||
export function findMatchingTemplate(
|
||||
uri: string,
|
||||
templates: McpResourceTemplate[] = [],
|
||||
): McpResourceTemplate | undefined {
|
||||
return templates.find((template) => {
|
||||
// Convert template to regex pattern
|
||||
const pattern = template.uriTemplate
|
||||
// Replace {param} with ([^/]+) to match any non-slash characters
|
||||
.replace(/\{([^}]+)\}/g, "([^/]+)")
|
||||
// Escape special regex characters except the ones we just added
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
// Un-escape the capturing groups we added
|
||||
.replace(/\\\(/g, "(")
|
||||
.replace(/\\\)/g, ")")
|
||||
|
||||
const regex = new RegExp(`^${pattern}$`)
|
||||
return regex.test(uri)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds either an exact resource match or a matching template for a given URI
|
||||
* @param uri The URI to find a match for
|
||||
* @param resources Array of concrete resources
|
||||
* @param templates Array of resource templates
|
||||
* @returns The matching resource, template, or undefined
|
||||
*/
|
||||
export function findMatchingResourceOrTemplate(
|
||||
uri: string,
|
||||
resources: McpResource[] = [],
|
||||
templates: McpResourceTemplate[] = [],
|
||||
): McpResource | McpResourceTemplate | undefined {
|
||||
// First try to find an exact resource match
|
||||
const exactMatch = resources.find((resource) => resource.uri === uri)
|
||||
if (exactMatch) return exactMatch
|
||||
|
||||
// If no exact match, try to find a matching template
|
||||
return findMatchingTemplate(uri, templates)
|
||||
}
|
||||
Reference in New Issue
Block a user