Add use_mcp_tool and access_mcp_resource tools

This commit is contained in:
Saoud Rizwan
2024-12-07 19:46:59 -08:00
parent 17d481d4d1
commit 1492604ee6
14 changed files with 821 additions and 66 deletions

View File

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

View File

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

View 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

View 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

View File

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

View 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)
}