Files
Roo-Code/webview-ui/src/components/mcp/McpView.tsx
2025-01-02 22:02:24 -08:00

368 lines
9.9 KiB
TypeScript

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"
import McpEnabledToggle from "./McpEnabledToggle"
type McpViewProps = {
onDone: () => void
}
const McpView = ({ onDone }: McpViewProps) => {
const { mcpServers: servers, alwaysAllowMcp, mcpEnabled } = 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: "10px",
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>
<McpEnabledToggle />
{mcpEnabled && (
<>
{/* Server List */}
{servers.length > 0 && (
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{servers.map((server) => (
<ServerRow key={server.name} server={server} alwaysAllowMcp={alwaysAllowMcp} />
))}
</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, alwaysAllowMcp }: { server: McpServer, alwaysAllowMcp?: boolean }) => {
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",
opacity: server.disabled ? 0.6 : 1,
}}
onClick={handleRowClick}>
{!server.error && (
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}
style={{ marginRight: "8px" }}
/>
)}
<span style={{ flex: 1 }}>{server.name}</span>
<div
style={{ display: "flex", alignItems: "center", marginRight: "8px" }}
onClick={(e) => e.stopPropagation()}>
<div
role="switch"
aria-checked={!server.disabled}
tabIndex={0}
style={{
width: "20px",
height: "10px",
backgroundColor: server.disabled ?
"var(--vscode-titleBar-inactiveForeground)" :
"var(--vscode-button-background)",
borderRadius: "5px",
position: "relative",
cursor: "pointer",
transition: "background-color 0.2s",
opacity: server.disabled ? 0.4 : 0.8,
}}
onClick={() => {
vscode.postMessage({
type: "toggleMcpServer",
serverName: server.name,
disabled: !server.disabled
});
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
vscode.postMessage({
type: "toggleMcpServer",
serverName: server.name,
disabled: !server.disabled
});
}
}}
>
<div style={{
width: "6px",
height: "6px",
backgroundColor: "var(--vscode-titleBar-activeForeground)",
borderRadius: "50%",
position: "absolute",
top: "2px",
left: server.disabled ? "2px" : "12px",
transition: "left 0.2s",
}} />
</div>
</div>
<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}
serverName={server.name}
alwaysAllowMcp={alwaysAllowMcp}
/>
))}
</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