diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 50a08bd532..0b6a2c2424 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -14,7 +14,7 @@ import { selectImages } from "../../integrations/misc/process-images" import { getTheme } from "../../integrations/theme/getTheme" import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker" import { McpHub } from "../../services/mcp/McpHub" -import { McpMarketplaceCatalog, McpMarketplaceItem } from "../../shared/mcp" +import { McpDownloadResponse, McpMarketplaceCatalog, McpMarketplaceItem, McpServer } from "../../shared/mcp" import { FirebaseAuthManager, UserInfo } from "../../services/auth/FirebaseAuthManager" import { ApiProvider, ModelInfo } from "../../shared/api" import { findLast } from "../../shared/array" @@ -404,7 +404,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { items: (response.data || []).map((item: any) => ({ ...item, githubStars: item.githubStars ?? 0, - downloads: item.downloads ?? 0, + downloadCount: item.downloadCount ?? 0, tags: item.tags ?? [], })), } @@ -438,30 +438,59 @@ export class ClineProvider implements vscode.WebviewViewProvider { private async downloadMcp(mcpId: string) { try { - const response = await axios.post( + // First check if we already have this MCP server installed + const servers = this.mcpHub?.getServers() || [] + const isInstalled = servers.some((server: McpServer) => { + try { + const config = JSON.parse(server.config) + const serverConfig = config.mcpServers[server.name] + const githubUrl = serverConfig.args?.find((arg: string) => arg.includes("github.com")) + return githubUrl?.includes(mcpId) + } catch { + return false + } + }) + + if (isInstalled) { + throw new Error("This MCP server is already installed") + } + + // Fetch server details from marketplace + const response = await axios.post( "https://api.cline.bot/v1/mcp/download", + { mcpId }, { - mcpId, - }, - { - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, + timeout: 10000, }, ) if (!response.data) { - throw new Error("Invalid response from MCP download API") + throw new Error("Invalid response from MCP marketplace API") } + console.log("[downloadMcp] Response from download API", { response }) + const mcpDetails = response.data + + // Validate required fields + if (!mcpDetails.githubUrl) { + throw new Error("Missing GitHub URL in MCP download response") + } + if (!mcpDetails.readmeContent) { + throw new Error("Missing README content in MCP download response") + } + + // Send details to webview await this.postMessageToWebview({ type: "mcpDownloadDetails", mcpDownloadDetails: mcpDetails, }) - // Create a new task for Cline to set up the MCP server + // Create task with context from README const task = `Set up the MCP server from ${mcpDetails.githubUrl}. Here's some additional context from the README:\n\n${mcpDetails.readmeContent}` + + // Initialize task and show chat view await this.initClineWithTask(task) await this.postMessageToWebview({ type: "action", @@ -469,7 +498,23 @@ export class ClineProvider implements vscode.WebviewViewProvider { }) } catch (error) { console.error("Failed to download MCP:", error) - const errorMessage = error instanceof Error ? error.message : "Failed to download MCP" + let errorMessage = "Failed to download MCP" + + if (axios.isAxiosError(error)) { + if (error.code === "ECONNABORTED") { + errorMessage = "Request timed out. Please try again." + } else if (error.response?.status === 404) { + errorMessage = "MCP server not found in marketplace." + } else if (error.response?.status === 500) { + errorMessage = "Internal server error. Please try again later." + } else if (!error.response && error.request) { + errorMessage = "Network error. Please check your internet connection." + } + } else if (error instanceof Error) { + errorMessage = error.message + } + + // Show error in both notification and marketplace UI vscode.window.showErrorMessage(errorMessage) await this.postMessageToWebview({ type: "mcpDownloadDetails", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index ee2d1a4d9c..8e00540534 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -5,7 +5,7 @@ import { AutoApprovalSettings } from "./AutoApprovalSettings" import { BrowserSettings } from "./BrowserSettings" import { ChatSettings } from "./ChatSettings" import { HistoryItem } from "./HistoryItem" -import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem } from "./mcp" +import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp" // webview will hold state export interface ExtensionMessage { @@ -50,7 +50,7 @@ export interface ExtensionMessage { mcpServers?: McpServer[] mcpMarketplaceCatalog?: McpMarketplaceCatalog error?: string - mcpDownloadDetails?: McpMarketplaceItem + mcpDownloadDetails?: McpDownloadResponse } export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown" diff --git a/src/shared/mcp.ts b/src/shared/mcp.ts index d859dc7b8f..28f4692c93 100644 --- a/src/shared/mcp.ts +++ b/src/shared/mcp.ts @@ -81,7 +81,7 @@ export interface McpMarketplaceItem { readmeContent?: string isRecommended: boolean githubStars: number - downloads: number + downloadCount: number createdAt: string updatedAt: string lastGithubSync: string @@ -90,3 +90,13 @@ export interface McpMarketplaceItem { export interface McpMarketplaceCatalog { items: McpMarketplaceItem[] } + +export interface McpDownloadResponse { + mcpId: string + githubUrl: string + name: string + author: string + description: string + readmeContent: string + requiresApiKey: boolean +} diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx index 2eebaa6da5..b5471bc1e7 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -9,7 +9,17 @@ interface McpMarketplaceCardProps { } const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) => { - const isInstalled = installedServers.some((server) => server.name === item.mcpId) + const isInstalled = installedServers.some((server) => { + try { + const config = JSON.parse(server.config) + const serverConfig = config.mcpServers[server.name] + // Extract GitHub URL from args if it's an npm package + const githubUrl = serverConfig.args?.find((arg: string) => arg.includes("github.com")) + return githubUrl?.includes(item.mcpId) || githubUrl?.includes(item.githubUrl) + } catch { + return false + } + }) const [isDownloading, setIsDownloading] = useState(false) useEffect(() => { @@ -94,7 +104,7 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps)
- {item.downloads?.toLocaleString() ?? 0} + {item.downloadCount?.toLocaleString() ?? 0}
{item.requiresApiKey && (
diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx index 0c9301a151..e346db3cea 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -1,16 +1,70 @@ -import { useEffect, useState } from "react" +import { useEffect, useMemo, useState } from "react" import { VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" import { McpMarketplaceItem } from "../../../../../src/shared/mcp" import { useExtensionState } from "../../../context/ExtensionStateContext" import { vscode } from "../../../utils/vscode" import McpMarketplaceCard from "./McpMarketplaceCard" +const searchInputStyles = { + width: "100%", + padding: "4px 8px 4px 28px", + background: "var(--vscode-input-background)", + border: "1px solid var(--vscode-input-border)", + color: "var(--vscode-input-foreground)", + borderRadius: "2px", + outline: "none", + transition: "border-color 0.1s ease-in-out, opacity 0.1s ease-in-out", +} + +const selectStyles = { + padding: "4px 8px", + background: "var(--vscode-dropdown-background)", + border: "1px solid var(--vscode-dropdown-border)", + color: "var(--vscode-dropdown-foreground)", + borderRadius: "2px", + outline: "none", + transition: "border-color 0.1s ease-in-out, opacity 0.1s ease-in-out", +} + const McpMarketplaceView = () => { const { mcpServers } = useExtensionState() const [items, setItems] = useState([]) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [isRefreshing, setIsRefreshing] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [selectedCategory, setSelectedCategory] = useState(null) + const [sortBy, setSortBy] = useState<"downloadCount" | "stars" | "name">("downloadCount") + + const categories = useMemo(() => { + const uniqueCategories = new Set(items.map((item) => item.category)) + return Array.from(uniqueCategories).sort() + }, [items]) + + const filteredItems = useMemo(() => { + return items + .filter((item) => { + const matchesSearch = + searchQuery === "" || + item.name.toLowerCase().includes(searchQuery.toLowerCase()) || + item.description.toLowerCase().includes(searchQuery.toLowerCase()) || + item.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase())) + const matchesCategory = !selectedCategory || item.category === selectedCategory + return matchesSearch && matchesCategory + }) + .sort((a, b) => { + switch (sortBy) { + case "downloadCount": + return b.downloadCount - a.downloadCount + case "stars": + return b.githubStars - a.githubStars + case "name": + return a.name.localeCompare(b.name) + default: + return 0 + } + }) + }, [items, searchQuery, selectedCategory, sortBy]) useEffect(() => { const handleMessage = (event: MessageEvent) => { @@ -89,13 +143,79 @@ const McpMarketplaceView = () => { return (
-
+
+
+
+ setSearchQuery(e.target.value)} + className="mcp-search-input" + style={searchInputStyles} + /> + +
+ + +
fetchMarketplace(true)} disabled={isRefreshing}> Refresh
- {items.length === 0 ? ( + + {filteredItems.length === 0 ? (
{ padding: "20px", color: "var(--vscode-descriptionForeground)", }}> - No MCP servers found in the marketplace + {searchQuery || selectedCategory + ? "No matching MCP servers found" + : "No MCP servers found in the marketplace"}
) : ( - items.map((item) => ) + filteredItems.map((item) => ) )}
)