mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Incorporate MCP changes (#93)
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
This commit is contained in:
@@ -7,11 +7,13 @@ import SettingsView from "./components/settings/SettingsView"
|
||||
import WelcomeView from "./components/welcome/WelcomeView"
|
||||
import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
|
||||
import { vscode } from "./utils/vscode"
|
||||
import McpView from "./components/mcp/McpView"
|
||||
|
||||
const AppContent = () => {
|
||||
const { didHydrateState, showWelcome, shouldShowAnnouncement } = useExtensionState()
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
const [showMcp, setShowMcp] = useState(false)
|
||||
const [showAnnouncement, setShowAnnouncement] = useState(false)
|
||||
|
||||
const handleMessage = useCallback((e: MessageEvent) => {
|
||||
@@ -22,14 +24,22 @@ const AppContent = () => {
|
||||
case "settingsButtonClicked":
|
||||
setShowSettings(true)
|
||||
setShowHistory(false)
|
||||
setShowMcp(false)
|
||||
break
|
||||
case "historyButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(true)
|
||||
setShowMcp(false)
|
||||
break
|
||||
case "mcpButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
setShowMcp(true)
|
||||
break
|
||||
case "chatButtonClicked":
|
||||
setShowSettings(false)
|
||||
setShowHistory(false)
|
||||
setShowMcp(false)
|
||||
break
|
||||
}
|
||||
break
|
||||
@@ -57,13 +67,15 @@ const AppContent = () => {
|
||||
<>
|
||||
{showSettings && <SettingsView onDone={() => setShowSettings(false)} />}
|
||||
{showHistory && <HistoryView onDone={() => setShowHistory(false)} />}
|
||||
{showMcp && <McpView onDone={() => setShowMcp(false)} />}
|
||||
{/* Do not conditionally load ChatView, it's expensive and there's state we don't want to lose (user input, disableInput, askResponse promise, etc.) */}
|
||||
<ChatView
|
||||
showHistoryView={() => {
|
||||
setShowSettings(false)
|
||||
setShowMcp(false)
|
||||
setShowHistory(true)
|
||||
}}
|
||||
isHidden={showSettings || showHistory}
|
||||
isHidden={showSettings || showHistory || showMcp}
|
||||
showAnnouncement={showAnnouncement}
|
||||
hideAnnouncement={() => {
|
||||
setShowAnnouncement(false)
|
||||
|
||||
@@ -30,20 +30,36 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
<h3 style={{ margin: "0 0 8px" }}>
|
||||
🎉{" "}New in v{minorVersion}
|
||||
🎉{" "}New in Cline v{minorVersion}
|
||||
</h3>
|
||||
<p style={{ margin: "5px 0px", fontWeight: "bold" }}>Add custom tools to Cline using MCP!</p>
|
||||
<p style={{ margin: "5px 0px" }}>
|
||||
Cline now uses Anthropic's new{" "}
|
||||
<VSCodeLink
|
||||
href="https://www.anthropic.com/news/3-5-models-and-computer-use"
|
||||
style={{ display: "inline" }}>
|
||||
"Computer Use"
|
||||
</VSCodeLink>{" "}
|
||||
feature to launch a browser, click, type, and scroll. This gives him more autonomy in runtime debugging,
|
||||
end-to-end testing, and even general web use. Try asking "Look up the weather in Colorado" to see it in
|
||||
action, or{" "}
|
||||
<VSCodeLink href="https://x.com/sdrzn/status/1850880547825823989" style={{ display: "inline" }}>
|
||||
see a full demo here.
|
||||
The Model Context Protocol allows agents like Cline to plug and play custom tools,{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
|
||||
e.g. a web-search tool or GitHub tool.
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
<p style={{ margin: "5px 0px" }}>
|
||||
You can add and configure MCP servers by clicking the new{" "}
|
||||
<span className="codicon codicon-server" style={{ fontSize: "10px" }}></span> icon in the menu bar.
|
||||
</p>
|
||||
<p style={{ margin: "5px 0px" }}>
|
||||
To take things a step further, Cline also has the ability to create custom tools for himself. Just say
|
||||
"add a tool that..." and watch as he builds and installs new capabilities specific to{" "}
|
||||
<i>your workflow</i>. For example:
|
||||
<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
|
||||
<li>"...fetches Jira tickets": Get ticket ACs and put Cline to work</li>
|
||||
<li>"...manages AWS EC2s": Check server metrics and scale up or down</li>
|
||||
<li>"...pulls PagerDuty incidents": Pulls details to help Cline fix bugs</li>
|
||||
</ul>
|
||||
Cline handles everything from creating the MCP server to installing it in the extension, ready to use in
|
||||
future tasks. The servers are saved to <code>~/Documents/Cline/MCP</code> so you can easily share them
|
||||
with others too.{" "}
|
||||
</p>
|
||||
<p style={{ margin: "5px 0px" }}>
|
||||
Try it yourself by asking Cline to "add a tool that gets the latest npm docs", or
|
||||
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
|
||||
see a demo of MCP in action here.
|
||||
</VSCodeLink>
|
||||
</p>
|
||||
{/*<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
|
||||
@@ -93,6 +109,14 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
|
||||
environments)
|
||||
</li>
|
||||
</ul>*/}
|
||||
<div
|
||||
style={{
|
||||
height: "1px",
|
||||
background: "var(--vscode-foreground)",
|
||||
opacity: 0.1,
|
||||
margin: "8px 0",
|
||||
}}
|
||||
/>
|
||||
<p style={{ margin: "0" }}>
|
||||
Join
|
||||
<VSCodeLink style={{ display: "inline" }} href="https://discord.gg/cline">
|
||||
|
||||
@@ -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,21 @@ 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 {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on
|
||||
the <code>{mcpServerUse.serverName}</code> MCP server:
|
||||
</span>,
|
||||
]
|
||||
case "completion_result":
|
||||
return [
|
||||
<span
|
||||
@@ -181,7 +209,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",
|
||||
@@ -618,6 +654,28 @@ 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>
|
||||
<CodeAccordian
|
||||
code={message.text}
|
||||
language="json"
|
||||
isExpanded={true}
|
||||
onToggleExpand={onToggleExpand}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
@@ -717,6 +775,76 @@ 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={{
|
||||
// Use the matched resource/template details, with fallbacks
|
||||
...(findMatchingResourceOrTemplate(
|
||||
useMcpServer.uri || "",
|
||||
server?.resources,
|
||||
server?.resourceTemplates,
|
||||
) || {
|
||||
name: "",
|
||||
mimeType: "",
|
||||
description: "",
|
||||
}),
|
||||
// Always use the actual URI from the request
|
||||
uri: useMcpServer.uri || "",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{useMcpServer.type === "use_mcp_tool" && (
|
||||
<>
|
||||
<McpToolRow
|
||||
tool={{
|
||||
name: useMcpServer.toolName || "",
|
||||
description:
|
||||
server?.tools?.find((tool) => tool.name === useMcpServer.toolName)
|
||||
?.description || "",
|
||||
}}
|
||||
/>
|
||||
{useMcpServer.arguments && useMcpServer.arguments !== "{}" && (
|
||||
<div style={{ marginTop: "8px" }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "4px",
|
||||
opacity: 0.8,
|
||||
fontSize: "12px",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
Arguments
|
||||
</div>
|
||||
<CodeAccordian
|
||||
code={useMcpServer.arguments}
|
||||
language="json"
|
||||
isExpanded={true}
|
||||
onToggleExpand={onToggleExpand}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
case "completion_result":
|
||||
if (message.text) {
|
||||
return (
|
||||
|
||||
@@ -156,6 +156,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
|
||||
playSoundOnMessage("celebration")
|
||||
@@ -205,6 +212,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
|
||||
@@ -273,6 +282,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":
|
||||
@@ -314,6 +324,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" })
|
||||
@@ -348,6 +359,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
|
||||
@@ -463,6 +475,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
return false
|
||||
}
|
||||
break
|
||||
case "mcp_server_request_started":
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
@@ -824,7 +838,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
|
||||
I can handle complex software development tasks step-by-step. With tools that let me create
|
||||
& edit files, explore complex projects, use the browser, and execute terminal commands
|
||||
(after you grant permission), I can assist you in ways that go beyond code completion or
|
||||
tech support.
|
||||
tech support. I can even use MCP to create new tools and extend my own capabilities.
|
||||
</p>
|
||||
</div>
|
||||
{taskHistory.length > 0 && <HistoryPreview showHistoryView={showHistoryView} />}
|
||||
|
||||
@@ -10,6 +10,46 @@ interface MarkdownBlockProps {
|
||||
markdown?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom remark plugin that converts plain URLs in text into clickable links
|
||||
*
|
||||
* The original bug: We were converting text nodes into paragraph nodes,
|
||||
* which broke the markdown structure because text nodes should remain as text nodes
|
||||
* within their parent elements (like paragraphs, list items, etc.).
|
||||
* This caused the entire content to disappear because the structure became invalid.
|
||||
*/
|
||||
const remarkUrlToLink = () => {
|
||||
return (tree: any) => {
|
||||
// Visit all "text" nodes in the markdown AST (Abstract Syntax Tree)
|
||||
visit(tree, "text", (node: any, index, parent) => {
|
||||
const urlRegex = /https?:\/\/[^\s<>)"]+/g
|
||||
const matches = node.value.match(urlRegex)
|
||||
if (!matches) return
|
||||
|
||||
const parts = node.value.split(urlRegex)
|
||||
const children: any[] = []
|
||||
|
||||
parts.forEach((part: string, i: number) => {
|
||||
if (part) children.push({ type: "text", value: part })
|
||||
if (matches[i]) {
|
||||
children.push({
|
||||
type: "link",
|
||||
url: matches[i],
|
||||
children: [{ type: "text", value: matches[i] }],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Fix: Instead of converting the node to a paragraph (which broke things),
|
||||
// we replace the original text node with our new nodes in the parent's children array.
|
||||
// This preserves the document structure while adding our links.
|
||||
if (parent) {
|
||||
parent.children.splice(index, 1, ...children)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const StyledMarkdown = styled.div`
|
||||
pre {
|
||||
background-color: ${CODE_BLOCK_BG_COLOR};
|
||||
@@ -88,6 +128,15 @@ const StyledMarkdown = styled.div`
|
||||
p {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
a {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const StyledPre = styled.pre<{ theme: any }>`
|
||||
@@ -111,6 +160,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
|
||||
const { theme } = useExtensionState()
|
||||
const [reactContent, setMarkdown] = useRemark({
|
||||
remarkPlugins: [
|
||||
remarkUrlToLink,
|
||||
() => {
|
||||
return (tree) => {
|
||||
visit(tree, "code", (node: any) => {
|
||||
|
||||
59
webview-ui/src/components/mcp/McpResourceRow.tsx
Normal file
59
webview-ui/src/components/mcp/McpResourceRow.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { McpResource, McpResourceTemplate } from "../../../../src/shared/mcp"
|
||||
|
||||
type McpResourceRowProps = {
|
||||
item: McpResource | McpResourceTemplate
|
||||
}
|
||||
|
||||
const McpResourceRow = ({ item }: McpResourceRowProps) => {
|
||||
const hasUri = "uri" in item
|
||||
const uri = hasUri ? item.uri : item.uriTemplate
|
||||
|
||||
return (
|
||||
<div
|
||||
key={uri}
|
||||
style={{
|
||||
padding: "3px 0",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: "4px",
|
||||
}}>
|
||||
<span className={`codicon codicon-symbol-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
|
||||
88
webview-ui/src/components/mcp/McpToolRow.tsx
Normal file
88
webview-ui/src/components/mcp/McpToolRow.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { McpTool } from "../../../../src/shared/mcp"
|
||||
|
||||
type McpToolRowProps = {
|
||||
tool: McpTool
|
||||
}
|
||||
|
||||
const McpToolRow = ({ tool }: McpToolRowProps) => {
|
||||
return (
|
||||
<div
|
||||
key={tool.name}
|
||||
style={{
|
||||
padding: "3px 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 &&
|
||||
Object.keys(tool.inputSchema.properties as Record<string, any>).length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: "8px",
|
||||
fontSize: "12px",
|
||||
border: "1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 30%, transparent)",
|
||||
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,
|
||||
overflowWrap: "break-word",
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
{schema.description || "No description"}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpToolRow
|
||||
305
webview-ui/src/components/mcp/McpView.tsx
Normal file
305
webview-ui/src/components/mcp/McpView.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
import {
|
||||
VSCodeButton,
|
||||
VSCodeLink,
|
||||
VSCodePanels,
|
||||
VSCodePanelTab,
|
||||
VSCodePanelView,
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
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
|
||||
}
|
||||
|
||||
const McpView = ({ onDone }: McpViewProps) => {
|
||||
const { mcpServers: servers } = useExtensionState()
|
||||
// const [servers, setServers] = useState<McpServer[]>([
|
||||
// // Add some mock servers for testing
|
||||
// {
|
||||
// name: "local-tools",
|
||||
// config: JSON.stringify({
|
||||
// mcpServers: {
|
||||
// "local-tools": {
|
||||
// command: "npx",
|
||||
// args: ["-y", "@modelcontextprotocol/server-tools"],
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// status: "connected",
|
||||
// tools: [
|
||||
// {
|
||||
// name: "execute_command",
|
||||
// description: "Run a shell command on the local system",
|
||||
// },
|
||||
// {
|
||||
// name: "read_file",
|
||||
// description: "Read contents of a file from the filesystem",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// name: "postgres-db",
|
||||
// config: JSON.stringify({
|
||||
// mcpServers: {
|
||||
// "postgres-db": {
|
||||
// command: "npx",
|
||||
// args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"],
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// status: "disconnected",
|
||||
// error: "Failed to connect to database: Connection refused",
|
||||
// },
|
||||
// {
|
||||
// name: "github-tools",
|
||||
// config: JSON.stringify({
|
||||
// mcpServers: {
|
||||
// "github-tools": {
|
||||
// command: "npx",
|
||||
// args: ["-y", "@modelcontextprotocol/server-github"],
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
// status: "connecting",
|
||||
// resources: [
|
||||
// {
|
||||
// uri: "github://repo/issues",
|
||||
// name: "Repository Issues",
|
||||
// },
|
||||
// {
|
||||
// uri: "github://repo/pulls",
|
||||
// name: "Pull Requests",
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "10px 17px 10px 20px",
|
||||
}}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>MCP Servers</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
fontSize: "13px",
|
||||
marginBottom: "20px",
|
||||
marginTop: "5px",
|
||||
}}>
|
||||
The{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol" style={{ display: "inline" }}>
|
||||
Model Context Protocol
|
||||
</VSCodeLink>{" "}
|
||||
enables communication with locally running MCP servers that provide additional tools and resources
|
||||
to extend Cline's capabilities. You can use{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
|
||||
community-made servers
|
||||
</VSCodeLink>{" "}
|
||||
or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest
|
||||
npm docs").
|
||||
</div>
|
||||
|
||||
{/* Server List */}
|
||||
{servers.length > 0 && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||
{servers.map((server) => (
|
||||
<ServerRow key={server.name} server={server} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Settings Button */}
|
||||
<div style={{ marginTop: "10px", width: "100%" }}>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
style={{ width: "100%" }}
|
||||
onClick={() => {
|
||||
vscode.postMessage({ type: "openMcpSettings" })
|
||||
}}>
|
||||
<span className="codicon codicon-edit" style={{ marginRight: "6px" }}></span>
|
||||
Edit MCP Settings
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
{/* Bottom padding */}
|
||||
<div style={{ height: "20px" }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Server Row Component
|
||||
const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
const getStatusColor = () => {
|
||||
switch (server.status) {
|
||||
case "connected":
|
||||
return "var(--vscode-testing-iconPassed)"
|
||||
case "connecting":
|
||||
return "var(--vscode-charts-yellow)"
|
||||
case "disconnected":
|
||||
return "var(--vscode-testing-iconFailed)"
|
||||
}
|
||||
}
|
||||
|
||||
const handleRowClick = () => {
|
||||
if (!server.error) {
|
||||
setIsExpanded(!isExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRestart = () => {
|
||||
vscode.postMessage({
|
||||
type: "restartMcpServer",
|
||||
text: server.name,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: "10px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "8px",
|
||||
background: "var(--vscode-textCodeBlock-background)",
|
||||
cursor: server.error ? "default" : "pointer",
|
||||
borderRadius: isExpanded || server.error ? "4px 4px 0 0" : "4px",
|
||||
}}
|
||||
onClick={handleRowClick}>
|
||||
{!server.error && (
|
||||
<span
|
||||
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
|
||||
style={{ marginRight: "8px" }}
|
||||
/>
|
||||
)}
|
||||
<span style={{ flex: 1 }}>{server.name}</span>
|
||||
<div
|
||||
style={{
|
||||
width: "8px",
|
||||
height: "8px",
|
||||
borderRadius: "50%",
|
||||
background: getStatusColor(),
|
||||
marginLeft: "8px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{server.error ? (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
background: "var(--vscode-textCodeBlock-background)",
|
||||
borderRadius: "0 0 4px 4px",
|
||||
width: "100%",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-testing-iconFailed)",
|
||||
marginBottom: "8px",
|
||||
padding: "0 10px",
|
||||
overflowWrap: "break-word",
|
||||
wordBreak: "break-word",
|
||||
}}>
|
||||
{server.error}
|
||||
</div>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={handleRestart}
|
||||
disabled={server.status === "connecting"}
|
||||
style={{ width: "calc(100% - 20px)", margin: "0 10px 10px 10px" }}>
|
||||
{server.status === "connecting" ? "Retrying..." : "Retry Connection"}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
) : (
|
||||
isExpanded && (
|
||||
<div
|
||||
style={{
|
||||
background: "var(--vscode-textCodeBlock-background)",
|
||||
padding: "0 10px 10px 10px",
|
||||
fontSize: "13px",
|
||||
borderRadius: "0 0 4px 4px",
|
||||
}}>
|
||||
<VSCodePanels>
|
||||
<VSCodePanelTab id="tools">Tools ({server.tools?.length || 0})</VSCodePanelTab>
|
||||
<VSCodePanelTab id="resources">
|
||||
Resources (
|
||||
{[...(server.resourceTemplates || []), ...(server.resources || [])].length || 0})
|
||||
</VSCodePanelTab>
|
||||
|
||||
<VSCodePanelView id="tools-view">
|
||||
{server.tools && server.tools.length > 0 ? (
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: "8px", width: "100%" }}>
|
||||
{server.tools.map((tool) => (
|
||||
<McpToolRow key={tool.name} tool={tool} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: "10px 0", color: "var(--vscode-descriptionForeground)" }}>
|
||||
No tools found
|
||||
</div>
|
||||
)}
|
||||
</VSCodePanelView>
|
||||
|
||||
<VSCodePanelView id="resources-view">
|
||||
{(server.resources && server.resources.length > 0) ||
|
||||
(server.resourceTemplates && server.resourceTemplates.length > 0) ? (
|
||||
<div
|
||||
style={{ display: "flex", flexDirection: "column", gap: "8px", 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)" }}>
|
||||
No resources found
|
||||
</div>
|
||||
)}
|
||||
</VSCodePanelView>
|
||||
</VSCodePanels>
|
||||
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={handleRestart}
|
||||
disabled={server.status === "connecting"}
|
||||
style={{ width: "calc(100% - 14px)", margin: "0 7px 3px 7px" }}>
|
||||
{server.status === "connecting" ? "Restarting..." : "Restart Server"}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpView
|
||||
@@ -31,7 +31,8 @@ const WelcomeView = () => {
|
||||
Claude 3.5 Sonnet's agentic coding capabilities
|
||||
</VSCodeLink>{" "}
|
||||
and access to tools that let me create & edit files, explore complex projects, use the browser, and
|
||||
execute terminal commands (with your permission, of course).
|
||||
execute terminal commands (with your permission, of course). I can even use MCP to create new tools and
|
||||
extend my own capabilities.
|
||||
</p>
|
||||
|
||||
<b>To get started, this extension needs an API provider for Claude 3.5 Sonnet.</b>
|
||||
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
import { vscode } from "../utils/vscode"
|
||||
import { convertTextMateToHljs } from "../utils/textMateToHljs"
|
||||
import { findLastIndex } from "../../../src/shared/array"
|
||||
import { McpServer } from "../../../src/shared/mcp"
|
||||
|
||||
export interface ExtensionStateContextType extends ExtensionState {
|
||||
didHydrateState: boolean
|
||||
showWelcome: boolean
|
||||
theme: any
|
||||
openRouterModels: Record<string, ModelInfo>
|
||||
mcpServers: McpServer[]
|
||||
filePaths: string[]
|
||||
setApiConfiguration: (config: ApiConfiguration) => void
|
||||
setCustomInstructions: (value?: string) => void
|
||||
@@ -48,6 +50,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
|
||||
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
|
||||
})
|
||||
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
|
||||
|
||||
const handleMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
@@ -104,6 +107,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
})
|
||||
break
|
||||
}
|
||||
case "mcpServers": {
|
||||
setMcpServers(message.mcpServers ?? [])
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -119,6 +126,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
||||
showWelcome,
|
||||
theme,
|
||||
openRouterModels,
|
||||
mcpServers,
|
||||
filePaths,
|
||||
setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
|
||||
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
|
||||
|
||||
45
webview-ui/src/utils/mcp.ts
Normal file
45
webview-ui/src/utils/mcp.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
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 = String(template.uriTemplate)
|
||||
// First escape special regex characters
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
// Then replace {param} with ([^/]+) to match any non-slash characters
|
||||
// We need to use \{ and \} because we just escaped them
|
||||
.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