From d0a46d65a733d52f160beb9395bac5c30127c6ba Mon Sep 17 00:00:00 2001 From: HadasaSchechterV <100204928+HadasaSchechterV@users.noreply.github.com> Date: Mon, 17 Mar 2025 12:06:20 +0200 Subject: [PATCH 1/7] Add MCP marketplace integration and related types --- src/core/webview/ClineProvider.ts | 172 ++++++++++++++++++++++++++++++ src/exports/roo-code.d.ts | 1 + src/shared/ExtensionMessage.ts | 7 +- src/shared/WebviewMessage.ts | 5 + src/shared/globalState.ts | 1 + src/shared/mcp.ts | 36 +++++++ 6 files changed, 221 insertions(+), 1 deletion(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 40b747f0ed..19ab6c19bb 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -61,6 +61,7 @@ import { getNonce } from "./getNonce" import { getUri } from "./getUri" import { telemetryService } from "../../services/telemetry/TelemetryService" import { TelemetrySetting } from "../../shared/TelemetrySetting" +import { McpMarketplaceCatalog, McpServer, McpDownloadResponse } from "../../shared/mcp" /** * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -2667,4 +2668,175 @@ export class ClineProvider implements vscode.WebviewViewProvider { return properties } + + // MCP Marketplace functions + private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise { + try { + const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", { + headers: { + "Content-Type": "application/json", + }, + }) + + if (!response.data) { + throw new Error("Invalid response from MCP marketplace API") + } + + const catalog: McpMarketplaceCatalog = { + items: (response.data || []).map((item: any) => ({ + ...item, + githubStars: item.githubStars ?? 0, + downloadCount: item.downloadCount ?? 0, + tags: item.tags ?? [], + })), + } + + // Store in global state + await this.updateGlobalState("mcpMarketplaceCatalog", catalog) + return catalog + } catch (error) { + console.error("Failed to fetch MCP marketplace:", error) + if (!silent) { + const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace" + await this.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + error: errorMessage, + }) + vscode.window.showErrorMessage(errorMessage) + } + return undefined + } + } + + async prefetchMcpMarketplace() { + try { + await this.fetchMcpMarketplaceFromApi(true) + } catch (error) { + console.error("Failed to prefetch MCP marketplace:", error) + } + } + + async silentlyRefreshMcpMarketplace() { + try { + const catalog = await this.fetchMcpMarketplaceFromApi(true) + if (catalog) { + await this.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + mcpMarketplaceCatalog: catalog, + }) + } + } catch (error) { + console.error("Failed to silently refresh MCP marketplace:", error) + } + } + + private async fetchMcpMarketplace(forceRefresh: boolean = false) { + try { + // Check if we have cached data + const cachedCatalog = (await this.getGlobalState("mcpMarketplaceCatalog")) as + | McpMarketplaceCatalog + | undefined + if (!forceRefresh && cachedCatalog?.items) { + await this.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + mcpMarketplaceCatalog: cachedCatalog, + }) + return + } + + const catalog = await this.fetchMcpMarketplaceFromApi(false) + if (catalog) { + await this.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + mcpMarketplaceCatalog: catalog, + }) + } + } catch (error) { + console.error("Failed to handle cached MCP marketplace:", error) + const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace" + await this.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + error: errorMessage, + }) + vscode.window.showErrorMessage(errorMessage) + } + } + + private async downloadMcp(mcpId: string) { + try { + // First check if we already have this MCP server installed + const servers = this.mcpHub?.getServers() || [] + const isInstalled = servers.some((server: McpServer) => server.name === mcpId) + + 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 }, + { + headers: { "Content-Type": "application/json" }, + timeout: 10000, + }, + ) + + if (!response.data) { + 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 task with context from README + const task = `Set up the MCP server from ${mcpDetails.githubUrl}. Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json. Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}` + + // Initialize task and show chat view + await this.initClineWithTask(task) + await this.postMessageToWebview({ + type: "action", + action: "chatButtonClicked", + }) + } catch (error) { + console.error("Failed to download MCP:", error) + 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", + error: errorMessage, + }) + } + } } diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index f9764b91a5..87b4c31d7b 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -219,6 +219,7 @@ export type GlobalStateKey = | "showRooIgnoredFiles" | "remoteBrowserEnabled" | "language" + | "mcpMarketplaceCatalog" export type ConfigurationKey = GlobalStateKey | SecretKey diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 5b018ec22e..fbe681cdaa 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -1,6 +1,6 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "./api" import { HistoryItem } from "./HistoryItem" -import { McpServer } from "./mcp" +import { McpServer, McpMarketplaceCatalog, McpDownloadResponse } from "./mcp" import { GitCommit } from "../utils/git" import { Mode, CustomModePrompts, ModeConfig } from "./modes" import { CustomSupportPrompts } from "./support-prompt" @@ -36,6 +36,8 @@ export interface ExtensionMessage { | "requestyModels" | "openAiModels" | "mcpServers" + | "mcpMarketplaceCatalog" + | "mcpDownloadDetails" | "enhancedPrompt" | "commitSearchResults" | "listApiConfig" @@ -81,6 +83,8 @@ export interface ExtensionMessage { requestyModels?: Record openAiModels?: string[] mcpServers?: McpServer[] + mcpMarketplaceCatalog?: McpMarketplaceCatalog + mcpDownloadDetails?: McpDownloadResponse commits?: GitCommit[] listApiConfig?: ApiConfigMeta[] mode?: Mode @@ -90,6 +94,7 @@ export interface ExtensionMessage { values?: Record requestId?: string promptText?: string + error?: string } export interface ApiConfigMeta { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index e802b8d8af..40385d3fc2 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -108,6 +108,10 @@ export interface WebviewMessage { | "browserConnectionResult" | "remoteBrowserEnabled" | "language" + | "fetchMcpMarketplace" + | "downloadMcp" + | "openMcpMarketplaceServerDetails" + | "silentlyRefreshMcpMarketplace" text?: string disabled?: boolean askResponse?: ClineAskResponse @@ -132,6 +136,7 @@ export interface WebviewMessage { payload?: WebViewMessagePayload source?: "global" | "project" requestId?: string + mcpId?: string } export const checkoutDiffPayloadSchema = z.object({ diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index c2b78f1ef7..3d658a8cee 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -118,6 +118,7 @@ export const GLOBAL_STATE_KEYS = [ "remoteBrowserEnabled", "language", "maxWorkspaceFiles", + "mcpMarketplaceCatalog", ] as const type CheckGlobalStateKeysExhaustiveness = diff --git a/src/shared/mcp.ts b/src/shared/mcp.ts index 2bc38a12a8..8bc2293092 100644 --- a/src/shared/mcp.ts +++ b/src/shared/mcp.ts @@ -65,3 +65,39 @@ export type McpToolCallResponse = { > isError?: boolean } + +export interface McpMarketplaceItem { + mcpId: string + githubUrl: string + name: string + author: string + description: string + codiconIcon: string + logoUrl: string + category: string + tags: string[] + requiresApiKey: boolean + readmeContent?: string + llmsInstallationContent?: string + isRecommended: boolean + githubStars: number + downloadCount: number + createdAt: string + updatedAt: string + lastGithubSync: string +} + +export interface McpMarketplaceCatalog { + items: McpMarketplaceItem[] +} + +export interface McpDownloadResponse { + mcpId: string + githubUrl: string + name: string + author: string + description: string + readmeContent: string + llmsInstallationContent: string + requiresApiKey: boolean +} From a4f3ce91b7c74e6a3226017bee21073f9fe56bee Mon Sep 17 00:00:00 2001 From: HadasaSchechterV <100204928+HadasaSchechterV@users.noreply.github.com> Date: Mon, 17 Mar 2025 12:59:46 +0200 Subject: [PATCH 2/7] Add marketplace tab and related localization for MCP view --- .../src/components/mcp/McpMarketplaceView.tsx | 208 ++++++++++++++++++ webview-ui/src/components/mcp/McpView.tsx | 172 ++++++++++----- webview-ui/src/i18n/locales/en/mcp.json | 13 +- 3 files changed, 331 insertions(+), 62 deletions(-) create mode 100644 webview-ui/src/components/mcp/McpMarketplaceView.tsx diff --git a/webview-ui/src/components/mcp/McpMarketplaceView.tsx b/webview-ui/src/components/mcp/McpMarketplaceView.tsx new file mode 100644 index 0000000000..42df34de9e --- /dev/null +++ b/webview-ui/src/components/mcp/McpMarketplaceView.tsx @@ -0,0 +1,208 @@ +import { useEffect, useState } from "react" +import { VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" +import { useAppTranslation } from "../../i18n/TranslationContext" +import { vscode } from "@/utils/vscode" +import { McpMarketplaceItem } from "../../../../src/shared/mcp" + +const McpMarketplaceView = () => { + const { t } = useAppTranslation() + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [items, setItems] = useState([]) + + useEffect(() => { + // Request marketplace data when component mounts + vscode.postMessage({ type: "fetchMcpMarketplace" }) + + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type === "mcpMarketplaceCatalog") { + setIsLoading(false) + if (message.error) { + setError(message.error) + } else if (message.mcpMarketplaceCatalog) { + setItems(message.mcpMarketplaceCatalog.items) + } + } + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, []) + + const handleInstall = (mcpId: string) => { + vscode.postMessage({ + type: "downloadMcp", + mcpId, + }) + } + + if (isLoading) { + return ( +
+ + {t("mcp:marketplace.loading")} +
+ ) + } + + if (error) { + return ( +
+ +

{error}

+ { + setIsLoading(true) + setError(null) + vscode.postMessage({ type: "fetchMcpMarketplace" }) + }}> + {t("mcp:marketplace.retry")} + +
+ ) + } + + return ( +
+ {items.length === 0 ? ( +
+ +

{t("mcp:marketplace.noServers")}

+
+ ) : ( +
+ {items.map((item) => ( +
+ {/* Header with logo */} +
+ {item.logoUrl ? ( + {item.name} + ) : ( + + )} +
+
{item.name}
+
+ {t("mcp:marketplace.by", { author: item.author })} +
+
+
+ + {/* Description */} +
+ {item.description} +
+ + {/* Stats and Install */} +
+
+
+ + {item.githubStars} +
+
+ + {item.downloadCount} +
+
+ handleInstall(item.mcpId)} style={{ minWidth: "80px" }}> + {t("mcp:marketplace.install")} + +
+
+ ))} +
+ )} +
+ ) +} + +export default McpMarketplaceView diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 97111e1332..36dd9acdb1 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -20,6 +20,7 @@ import { Tab, TabContent, TabHeader } from "../common/Tab" import McpToolRow from "./McpToolRow" import McpResourceRow from "./McpResourceRow" import McpEnabledToggle from "./McpEnabledToggle" +import McpMarketplaceView from "./McpMarketplaceView" type McpViewProps = { onDone: () => void @@ -34,6 +35,7 @@ const McpView = ({ onDone }: McpViewProps) => { setEnableMcpServerCreation, } = useExtensionState() const { t } = useAppTranslation() + const [activeTab, setActiveTab] = useState<"installed" | "marketplace">("installed") return ( @@ -43,71 +45,119 @@ const McpView = ({ onDone }: McpViewProps) => { -
- - - Model Context Protocol - - - community-made servers - - -
+
+ + setActiveTab("installed")}> + {t("mcp:tabs.installed")} + + setActiveTab("marketplace")}> + {t("mcp:tabs.marketplace")} + - + +
+ + + Model Context Protocol + + + community-made servers + + +
- {mcpEnabled && ( - <> -
- { - setEnableMcpServerCreation(e.target.checked) - vscode.postMessage({ type: "enableMcpServerCreation", bool: e.target.checked }) - }}> - {t("mcp:enableServerCreation.title")} - -

- {t("mcp:enableServerCreation.description")} -

-
+ - {/* Server List */} - {servers.length > 0 && ( -
- {servers.map((server) => ( - - ))} -
- )} + {mcpEnabled && ( + <> +
+ { + setEnableMcpServerCreation(e.target.checked) + vscode.postMessage({ + type: "enableMcpServerCreation", + bool: e.target.checked, + }) + }}> + + {t("mcp:enableServerCreation.title")} + + +

+ {t("mcp:enableServerCreation.description")} +

+
- {/* Edit Settings Button */} -
- { - vscode.postMessage({ type: "openMcpSettings" }) - }}> - - {t("mcp:editSettings")} - -
- - )} + {/* Server List */} + {servers.length > 0 ? ( +
+ {servers.map((server) => ( + + ))} +
+ ) : ( +
+ +

+ {t("mcp:emptyState.noServers")} +

+ setActiveTab("marketplace")} + appearance="secondary"> + {t("mcp:emptyState.browseMarketplace")} + +
+ )} + + {/* Edit Settings Button */} +
+ { + vscode.postMessage({ type: "openMcpSettings" }) + }}> + + {t("mcp:editSettings")} + +
+ + )} +
+ + + + +
+
) diff --git a/webview-ui/src/i18n/locales/en/mcp.json b/webview-ui/src/i18n/locales/en/mcp.json index 710b787e5d..1021901582 100644 --- a/webview-ui/src/i18n/locales/en/mcp.json +++ b/webview-ui/src/i18n/locales/en/mcp.json @@ -17,12 +17,23 @@ "noDescription": "No description" }, "tabs": { + "installed": "Installed", + "marketplace": "Marketplace", "tools": "Tools", "resources": "Resources" }, "emptyState": { "noTools": "No tools found", - "noResources": "No resources found" + "noResources": "No resources found", + "noServers": "No servers installed", + "browseMarketplace": "Browse Marketplace" + }, + "marketplace": { + "loading": "Loading marketplace...", + "noServers": "No servers available", + "retry": "Retry", + "install": "Install", + "by": "by {{author}}" }, "networkTimeout": { "label": "Network Timeout", From 8207d255aec944f25fcbb35fa11794ca2860f224 Mon Sep 17 00:00:00 2001 From: HadasaSchechterV <100204928+HadasaSchechterV@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:17:02 +0200 Subject: [PATCH 3/7] Add MCP marketplace view and submission card components --- src/core/webview/ClineProvider.ts | 58 ++++- .../src/components/mcp/McpMarketplaceView.tsx | 208 ------------------ webview-ui/src/components/mcp/McpView.tsx | 2 +- .../mcp/marketplace/McpMarketplaceCard.tsx | 203 +++++++++++++++++ .../mcp/marketplace/McpMarketplaceView.tsx | 119 ++++++++++ .../mcp/marketplace/McpSubmitCard.tsx | 51 +++++ 6 files changed, 427 insertions(+), 214 deletions(-) delete mode 100644 webview-ui/src/components/mcp/McpMarketplaceView.tsx create mode 100644 webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx create mode 100644 webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx create mode 100644 webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 19ab6c19bb..ecd38d79fc 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -62,6 +62,7 @@ import { getUri } from "./getUri" import { telemetryService } from "../../services/telemetry/TelemetryService" import { TelemetrySetting } from "../../shared/TelemetrySetting" import { McpMarketplaceCatalog, McpServer, McpDownloadResponse } from "../../shared/mcp" +import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider" /** * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -1870,6 +1871,27 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.postStateToWebview() break } + + case "fetchMcpMarketplace": { + await this.fetchMcpMarketplace(message.bool) + break + } + case "downloadMcp": { + if (message.mcpId) { + await this.downloadMcp(message.mcpId) + } + break + } + case "silentlyRefreshMcpMarketplace": { + await this.silentlyRefreshMcpMarketplace() + break + } + case "openMcpMarketplaceServerDetails": { + if (message.mcpId) { + await this.openMcpMarketplaceServerDetails(message.mcpId) + } + break + } } }, null, @@ -2786,8 +2808,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { throw new Error("Invalid response from MCP marketplace API") } - console.log("[downloadMcp] Response from download API", { response }) - const mcpDetails = response.data // Validate required fields @@ -2804,10 +2824,8 @@ export class ClineProvider implements vscode.WebviewViewProvider { mcpDownloadDetails: mcpDetails, }) - // Create task with context from README + // Initialize task with setup instructions const task = `Set up the MCP server from ${mcpDetails.githubUrl}. Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json. Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}` - - // Initialize task and show chat view await this.initClineWithTask(task) await this.postMessageToWebview({ type: "action", @@ -2839,4 +2857,34 @@ export class ClineProvider implements vscode.WebviewViewProvider { }) } } + + private async openMcpMarketplaceServerDetails(mcpId: string) { + const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${mcpId}`) + const details: McpDownloadResponse = await response.json() + + if (details.readmeContent) { + // Disable markdown preview markers + const config = vscode.workspace.getConfiguration("markdown") + await config.update("preview.markEditorSelection", false, true) + + // Create URI with base64 encoded markdown content + const uri = vscode.Uri.parse( + `${DIFF_VIEW_URI_SCHEME}:${details.name} README?${Buffer.from(details.readmeContent).toString("base64")}`, + ) + + // close existing + const tabs = vscode.window.tabGroups.all + .flatMap((tg) => tg.tabs) + .filter((tab) => tab.label && tab.label.includes("README") && tab.label.includes("Preview")) + for (const tab of tabs) { + await vscode.window.tabGroups.close(tab) + } + + // Show only the preview + await vscode.commands.executeCommand("markdown.showPreview", uri, { + sideBySide: true, + preserveFocus: true, + }) + } + } } diff --git a/webview-ui/src/components/mcp/McpMarketplaceView.tsx b/webview-ui/src/components/mcp/McpMarketplaceView.tsx deleted file mode 100644 index 42df34de9e..0000000000 --- a/webview-ui/src/components/mcp/McpMarketplaceView.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import { useEffect, useState } from "react" -import { VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" -import { useAppTranslation } from "../../i18n/TranslationContext" -import { vscode } from "@/utils/vscode" -import { McpMarketplaceItem } from "../../../../src/shared/mcp" - -const McpMarketplaceView = () => { - const { t } = useAppTranslation() - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) - const [items, setItems] = useState([]) - - useEffect(() => { - // Request marketplace data when component mounts - vscode.postMessage({ type: "fetchMcpMarketplace" }) - - const handleMessage = (event: MessageEvent) => { - const message = event.data - if (message.type === "mcpMarketplaceCatalog") { - setIsLoading(false) - if (message.error) { - setError(message.error) - } else if (message.mcpMarketplaceCatalog) { - setItems(message.mcpMarketplaceCatalog.items) - } - } - } - - window.addEventListener("message", handleMessage) - return () => window.removeEventListener("message", handleMessage) - }, []) - - const handleInstall = (mcpId: string) => { - vscode.postMessage({ - type: "downloadMcp", - mcpId, - }) - } - - if (isLoading) { - return ( -
- - {t("mcp:marketplace.loading")} -
- ) - } - - if (error) { - return ( -
- -

{error}

- { - setIsLoading(true) - setError(null) - vscode.postMessage({ type: "fetchMcpMarketplace" }) - }}> - {t("mcp:marketplace.retry")} - -
- ) - } - - return ( -
- {items.length === 0 ? ( -
- -

{t("mcp:marketplace.noServers")}

-
- ) : ( -
- {items.map((item) => ( -
- {/* Header with logo */} -
- {item.logoUrl ? ( - {item.name} - ) : ( - - )} -
-
{item.name}
-
- {t("mcp:marketplace.by", { author: item.author })} -
-
-
- - {/* Description */} -
- {item.description} -
- - {/* Stats and Install */} -
-
-
- - {item.githubStars} -
-
- - {item.downloadCount} -
-
- handleInstall(item.mcpId)} style={{ minWidth: "80px" }}> - {t("mcp:marketplace.install")} - -
-
- ))} -
- )} -
- ) -} - -export default McpMarketplaceView diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 36dd9acdb1..97255c2ab0 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -20,7 +20,7 @@ import { Tab, TabContent, TabHeader } from "../common/Tab" import McpToolRow from "./McpToolRow" import McpResourceRow from "./McpResourceRow" import McpEnabledToggle from "./McpEnabledToggle" -import McpMarketplaceView from "./McpMarketplaceView" +import McpMarketplaceView from "./marketplace/McpMarketplaceView" type McpViewProps = { onDone: () => void diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx new file mode 100644 index 0000000000..387f07009d --- /dev/null +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -0,0 +1,203 @@ +import { useCallback, useState, useRef } from "react" +import styled from "styled-components" +import { McpMarketplaceItem, McpServer } from "../../../../../src/shared/mcp" +import { vscode } from "../../../utils/vscode" +import { useEvent } from "react-use" + +interface McpMarketplaceCardProps { + item: McpMarketplaceItem + installedServers: McpServer[] +} + +const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) => { + const isInstalled = installedServers.some((server) => server.name === item.mcpId) + const [isDownloading, setIsDownloading] = useState(false) + const githubLinkRef = useRef(null) + + const handleMessage = useCallback((event: MessageEvent) => { + const message = event.data + switch (message.type) { + case "mcpDownloadDetails": + setIsDownloading(false) + break + } + }, []) + + useEvent("message", handleMessage) + + return ( + <> + +
{ + if (githubLinkRef.current?.contains(e.target as Node)) { + return + } + + vscode.postMessage({ + type: "openMcpMarketplaceServerDetails", + mcpId: item.mcpId, + }) + }} + style={{ + background: "var(--vscode-textCodeBlock-background)", + borderRadius: "6px", + overflow: "hidden", + }}> + {/* Header with logo */} +
+ {item.logoUrl ? ( + {item.name} + ) : ( + + )} +
+
{item.name}
+
+ by {item.author} +
+
+
+ + {/* Description */} +
+ {item.description} +
+ + {/* Stats and Install */} +
+ +
{ + e.stopPropagation() // Prevent card click when clicking install + if (!isInstalled && !isDownloading) { + setIsDownloading(true) + vscode.postMessage({ + type: "downloadMcp", + mcpId: item.mcpId, + }) + } + }}> + + {isInstalled ? "Installed" : isDownloading ? "Installing..." : "Install"} + +
+
+
+ + ) +} + +const StyledInstallButton = styled.div<{ disabled?: boolean; $isInstalled?: boolean }>` + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 2px; + font-size: 13px; + font-weight: 400; + padding: 4px 12px; + cursor: ${(props) => (props.disabled ? "default" : "pointer")}; + background: ${(props) => + props.$isInstalled ? "var(--vscode-button-secondaryBackground)" : "var(--vscode-button-background)"}; + color: var(--vscode-button-foreground); + + &:hover:not(:disabled) { + background: ${(props) => + props.$isInstalled + ? "var(--vscode-button-secondaryHoverBackground)" + : "var(--vscode-button-hoverBackground)"}; + } + + &:active:not(:disabled) { + background: ${(props) => + props.$isInstalled ? "var(--vscode-button-secondaryBackground)" : "var(--vscode-button-background)"}; + opacity: 0.7; + } + + &:disabled { + opacity: 0.5; + cursor: default; + } +` + +export default McpMarketplaceCard diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx new file mode 100644 index 0000000000..8c075f959d --- /dev/null +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -0,0 +1,119 @@ +import { useEffect, useState } from "react" +import { VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" +import { useAppTranslation } from "../../../i18n/TranslationContext" +import { useExtensionState } from "../../../context/ExtensionStateContext" +import { vscode } from "../../../utils/vscode" +import { McpMarketplaceItem } from "../../../../../src/shared/mcp" +import McpMarketplaceCard from "./McpMarketplaceCard" +import McpSubmitCard from "./McpSubmitCard" + +const McpMarketplaceView = () => { + const { t } = useAppTranslation() + const { mcpServers } = useExtensionState() + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [items, setItems] = useState([]) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type === "mcpMarketplaceCatalog") { + if (message.error) { + setError(message.error) + } else if (message.mcpMarketplaceCatalog) { + setError(null) + setItems(message.mcpMarketplaceCatalog.items) + } + setIsLoading(false) + } else if (message.type === "mcpDownloadDetails") { + if (message.error) { + setError(message.error) + } + } + } + + window.addEventListener("message", handleMessage) + + // Request marketplace data when component mounts + vscode.postMessage({ type: "fetchMcpMarketplace" }) + + return () => window.removeEventListener("message", handleMessage) + }, []) + + if (isLoading) { + return ( +
+ + {t("mcp:marketplace.loading")} +
+ ) + } + + if (error) { + return ( +
+ +

{error}

+ { + setIsLoading(true) + setError(null) + vscode.postMessage({ type: "fetchMcpMarketplace" }) + }}> + {t("mcp:marketplace.retry")} + +
+ ) + } + + return ( +
+ {items.length === 0 ? ( +
+ +

{t("mcp:marketplace.noServers")}

+
+ ) : ( +
+ {items.map((item) => ( + + ))} + +
+ )} +
+ ) +} + +export default McpMarketplaceView diff --git a/webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx b/webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx new file mode 100644 index 0000000000..d0829fcce3 --- /dev/null +++ b/webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx @@ -0,0 +1,51 @@ +const McpSubmitCard = () => { + return ( +
+ {/* Logo */} + Cline bot logo + + {/* Content */} +
+

+ Is something missing? +

+

+ Submit your own MCP servers to the marketplace by{" "} + submitting an issue on the official MCP + Marketplace repo on GitHub. +

+
+
+ ) +} + +export default McpSubmitCard From e11b80431e3e920fb184d31d797e87fde563292b Mon Sep 17 00:00:00 2001 From: HadasaSchechterV <100204928+HadasaSchechterV@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:30:15 +0200 Subject: [PATCH 4/7] Add search and sorting functionality to MCP marketplace view --- .../mcp/marketplace/McpMarketplaceView.tsx | 96 +++++++++++++++++-- webview-ui/src/i18n/locales/en/mcp.json | 9 +- 2 files changed, 95 insertions(+), 10 deletions(-) diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx index 8c075f959d..05c119e78d 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -1,5 +1,11 @@ -import { useEffect, useState } from "react" -import { VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" +import { useEffect, useState, useMemo } from "react" +import { + VSCodeButton, + VSCodeProgressRing, + VSCodeDropdown, + VSCodeOption, + VSCodeTextField, +} from "@vscode/webview-ui-toolkit/react" import { useAppTranslation } from "../../../i18n/TranslationContext" import { useExtensionState } from "../../../context/ExtensionStateContext" import { vscode } from "../../../utils/vscode" @@ -7,12 +13,17 @@ import { McpMarketplaceItem } from "../../../../../src/shared/mcp" import McpMarketplaceCard from "./McpMarketplaceCard" import McpSubmitCard from "./McpSubmitCard" +type SortOption = "stars" | "downloads" | "newest" | "updated" + const McpMarketplaceView = () => { const { t } = useAppTranslation() const { mcpServers } = useExtensionState() const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [items, setItems] = useState([]) + const [searchQuery, setSearchQuery] = useState("") + const [selectedCategory, setSelectedCategory] = useState("all") + const [sortBy, setSortBy] = useState("stars") useEffect(() => { const handleMessage = (event: MessageEvent) => { @@ -31,15 +42,52 @@ const McpMarketplaceView = () => { } } } - window.addEventListener("message", handleMessage) - - // Request marketplace data when component mounts vscode.postMessage({ type: "fetchMcpMarketplace" }) - return () => window.removeEventListener("message", handleMessage) }, []) + const categories = useMemo(() => { + const categorySet = new Set(items.map((item) => item.category)) + return ["all", ...Array.from(categorySet)] + }, [items]) + + const filteredAndSortedItems = useMemo(() => { + let filtered = items + + // Apply search filter + if (searchQuery) { + const query = searchQuery.toLowerCase() + filtered = filtered.filter( + (item) => + item.name.toLowerCase().includes(query) || + item.description.toLowerCase().includes(query) || + item.tags.some((tag) => tag.toLowerCase().includes(query)), + ) + } + + // Apply category filter + if (selectedCategory !== "all") { + filtered = filtered.filter((item) => item.category === selectedCategory) + } + + // Apply sorting + return [...filtered].sort((a, b) => { + switch (sortBy) { + case "stars": + return b.githubStars - a.githubStars + case "downloads": + return b.downloadCount - a.downloadCount + case "newest": + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + case "updated": + return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + default: + return 0 + } + }) + }, [items, searchQuery, selectedCategory, sortBy]) + if (isLoading) { return (
{ return (
- {items.length === 0 ? ( +
+ setSearchQuery((e.target as HTMLInputElement).value)} + style={{ flexGrow: 1 }} + /> + setSelectedCategory((e.target as HTMLSelectElement).value)}> + {categories.map((category) => ( + + {category === "all" ? t("mcp:marketplace.allCategories") : category} + + ))} + + setSortBy((e.target as HTMLSelectElement).value as SortOption)}> + {t("mcp:marketplace.sortByStars")} + {t("mcp:marketplace.sortByDownloads")} + {t("mcp:marketplace.sortByNewest")} + {t("mcp:marketplace.sortByUpdated")} + +
+ + {filteredAndSortedItems.length === 0 ? (
{ textAlign: "center", }}> -

{t("mcp:marketplace.noServers")}

+

+ {searchQuery || selectedCategory !== "all" + ? t("mcp:marketplace.noResults") + : t("mcp:marketplace.noServers")} +

) : (
{ gap: "16px", padding: "16px 0", }}> - {items.map((item) => ( + {filteredAndSortedItems.map((item) => ( ))} diff --git a/webview-ui/src/i18n/locales/en/mcp.json b/webview-ui/src/i18n/locales/en/mcp.json index 1021901582..2ecbe97dc6 100644 --- a/webview-ui/src/i18n/locales/en/mcp.json +++ b/webview-ui/src/i18n/locales/en/mcp.json @@ -31,9 +31,16 @@ "marketplace": { "loading": "Loading marketplace...", "noServers": "No servers available", + "noResults": "No matching servers found", "retry": "Retry", "install": "Install", - "by": "by {{author}}" + "by": "by {{author}}", + "searchPlaceholder": "Search servers...", + "allCategories": "All Categories", + "sortByStars": "Sort by Stars", + "sortByDownloads": "Sort by Downloads", + "sortByNewest": "Sort by Newest", + "sortByUpdated": "Sort by Last Updated" }, "networkTimeout": { "label": "Network Timeout", From c9ddca04e2be01c29cbb6f19f6c0a6485856d41b Mon Sep 17 00:00:00 2001 From: HadasaSchechterV <100204928+HadasaSchechterV@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:22:07 +0200 Subject: [PATCH 5/7] Enhance MCP view with tab navigation and improved localization --- src/core/webview/ClineProvider.ts | 443 +++++++++--------- webview-ui/src/components/mcp/McpView.tsx | 258 +++++----- .../mcp/marketplace/McpMarketplaceCard.tsx | 296 ++++++++---- .../mcp/marketplace/McpMarketplaceView.tsx | 322 ++++++++----- webview-ui/src/i18n/locales/en/mcp.json | 12 +- 5 files changed, 783 insertions(+), 548 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index ecd38d79fc..864ee44d27 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -714,6 +714,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { mcpServers: this.mcpHub.getAllServers(), }) } + this.prefetchMcpMarketplace() const cacheDir = await this.ensureCacheDirectoryExists() @@ -1116,6 +1117,28 @@ export class ClineProvider implements vscode.WebviewViewProvider { } break } + + case "fetchMcpMarketplace": { + await this.fetchMcpMarketplace(message.bool) + break + } + case "downloadMcp": { + if (message.mcpId) { + await this.downloadMcp(message.mcpId) + } + break + } + case "silentlyRefreshMcpMarketplace": { + await this.silentlyRefreshMcpMarketplace() + break + } + case "openMcpMarketplaceServerDetails": { + if (message.mcpId) { + await this.openMcpMarketplaceServerDetails(message.mcpId) + } + break + } + case "openCustomModesSettings": { const customModesFilePath = await this.customModesManager.getCustomModesFilePath() if (customModesFilePath) { @@ -1871,27 +1894,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.postStateToWebview() break } - - case "fetchMcpMarketplace": { - await this.fetchMcpMarketplace(message.bool) - break - } - case "downloadMcp": { - if (message.mcpId) { - await this.downloadMcp(message.mcpId) - } - break - } - case "silentlyRefreshMcpMarketplace": { - await this.silentlyRefreshMcpMarketplace() - break - } - case "openMcpMarketplaceServerDetails": { - if (message.mcpId) { - await this.openMcpMarketplaceServerDetails(message.mcpId) - } - break - } } }, null, @@ -2117,6 +2119,208 @@ export class ClineProvider implements vscode.WebviewViewProvider { return undefined } + // MCP Marketplace + + private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise { + try { + const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", { + headers: { + "Content-Type": "application/json", + }, + }) + + if (!response.data) { + throw new Error("Invalid response from MCP marketplace API") + } + + const catalog: McpMarketplaceCatalog = { + items: (response.data || []).map((item: any) => ({ + ...item, + githubStars: item.githubStars ?? 0, + downloadCount: item.downloadCount ?? 0, + tags: item.tags ?? [], + })), + } + + // Store in global state + await this.updateGlobalState("mcpMarketplaceCatalog", catalog) + return catalog + } catch (error) { + console.error("Failed to fetch MCP marketplace:", error) + if (!silent) { + const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace" + await this.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + error: errorMessage, + }) + vscode.window.showErrorMessage(errorMessage) + } + return undefined + } + } + + async prefetchMcpMarketplace() { + try { + await this.fetchMcpMarketplaceFromApi(true) + } catch (error) { + console.error("Failed to prefetch MCP marketplace:", error) + } + } + + async silentlyRefreshMcpMarketplace() { + try { + const catalog = await this.fetchMcpMarketplaceFromApi(true) + if (catalog) { + await this.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + mcpMarketplaceCatalog: catalog, + }) + } + } catch (error) { + console.error("Failed to silently refresh MCP marketplace:", error) + } + } + + private async fetchMcpMarketplace(forceRefresh: boolean = false) { + try { + // Check if we have cached data + const cachedCatalog = (await this.getGlobalState("mcpMarketplaceCatalog")) as + | McpMarketplaceCatalog + | undefined + if (!forceRefresh && cachedCatalog?.items) { + await this.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + mcpMarketplaceCatalog: cachedCatalog, + }) + return + } + + const catalog = await this.fetchMcpMarketplaceFromApi(false) + if (catalog) { + await this.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + mcpMarketplaceCatalog: catalog, + }) + } + } catch (error) { + console.error("Failed to handle cached MCP marketplace:", error) + const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace" + await this.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + error: errorMessage, + }) + vscode.window.showErrorMessage(errorMessage) + } + } + + private async downloadMcp(mcpId: string) { + try { + // First check if we already have this MCP server installed + const servers = this.mcpHub?.getServers() || [] + const isInstalled = servers.some((server: McpServer) => server.name === mcpId) + + 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 }, + { + headers: { "Content-Type": "application/json" }, + timeout: 10000, + }, + ) + + if (!response.data) { + 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 task with context from README + const task = `Set up the MCP server from ${mcpDetails.githubUrl}. Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json. Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}` + + // Initialize task and show chat view + await this.initClineWithTask(task) + await this.postMessageToWebview({ + type: "action", + action: "chatButtonClicked", + }) + } catch (error) { + console.error("Failed to download MCP:", error) + 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", + error: errorMessage, + }) + } + } + + private async openMcpMarketplaceServerDetails(mcpId: string) { + const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${mcpId}`) + const details: McpDownloadResponse = await response.json() + + if (details.readmeContent) { + // Disable markdown preview markers + const config = vscode.workspace.getConfiguration("markdown") + await config.update("preview.markEditorSelection", false, true) + + // Create URI with base64 encoded markdown content + const uri = vscode.Uri.parse( + `${DIFF_VIEW_URI_SCHEME}:${details.name} README?${Buffer.from(details.readmeContent).toString("base64")}`, + ) + + // close existing + const tabs = vscode.window.tabGroups.all + .flatMap((tg) => tg.tabs) + .filter((tab) => tab.label && tab.label.includes("README") && tab.label.includes("Preview")) + for (const tab of tabs) { + await vscode.window.tabGroups.close(tab) + } + + // Show only the preview + await vscode.commands.executeCommand("markdown.showPreview", uri, { + sideBySide: true, + preserveFocus: true, + }) + } + } + // OpenRouter async handleOpenRouterCallback(code: string) { @@ -2690,201 +2894,4 @@ export class ClineProvider implements vscode.WebviewViewProvider { return properties } - - // MCP Marketplace functions - private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise { - try { - const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", { - headers: { - "Content-Type": "application/json", - }, - }) - - if (!response.data) { - throw new Error("Invalid response from MCP marketplace API") - } - - const catalog: McpMarketplaceCatalog = { - items: (response.data || []).map((item: any) => ({ - ...item, - githubStars: item.githubStars ?? 0, - downloadCount: item.downloadCount ?? 0, - tags: item.tags ?? [], - })), - } - - // Store in global state - await this.updateGlobalState("mcpMarketplaceCatalog", catalog) - return catalog - } catch (error) { - console.error("Failed to fetch MCP marketplace:", error) - if (!silent) { - const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace" - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - error: errorMessage, - }) - vscode.window.showErrorMessage(errorMessage) - } - return undefined - } - } - - async prefetchMcpMarketplace() { - try { - await this.fetchMcpMarketplaceFromApi(true) - } catch (error) { - console.error("Failed to prefetch MCP marketplace:", error) - } - } - - async silentlyRefreshMcpMarketplace() { - try { - const catalog = await this.fetchMcpMarketplaceFromApi(true) - if (catalog) { - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - mcpMarketplaceCatalog: catalog, - }) - } - } catch (error) { - console.error("Failed to silently refresh MCP marketplace:", error) - } - } - - private async fetchMcpMarketplace(forceRefresh: boolean = false) { - try { - // Check if we have cached data - const cachedCatalog = (await this.getGlobalState("mcpMarketplaceCatalog")) as - | McpMarketplaceCatalog - | undefined - if (!forceRefresh && cachedCatalog?.items) { - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - mcpMarketplaceCatalog: cachedCatalog, - }) - return - } - - const catalog = await this.fetchMcpMarketplaceFromApi(false) - if (catalog) { - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - mcpMarketplaceCatalog: catalog, - }) - } - } catch (error) { - console.error("Failed to handle cached MCP marketplace:", error) - const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace" - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - error: errorMessage, - }) - vscode.window.showErrorMessage(errorMessage) - } - } - - private async downloadMcp(mcpId: string) { - try { - // First check if we already have this MCP server installed - const servers = this.mcpHub?.getServers() || [] - const isInstalled = servers.some((server: McpServer) => server.name === mcpId) - - 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 }, - { - headers: { "Content-Type": "application/json" }, - timeout: 10000, - }, - ) - - if (!response.data) { - throw new Error("Invalid response from MCP marketplace API") - } - - 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, - }) - - // Initialize task with setup instructions - const task = `Set up the MCP server from ${mcpDetails.githubUrl}. Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json. Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}` - await this.initClineWithTask(task) - await this.postMessageToWebview({ - type: "action", - action: "chatButtonClicked", - }) - } catch (error) { - console.error("Failed to download MCP:", error) - 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", - error: errorMessage, - }) - } - } - - private async openMcpMarketplaceServerDetails(mcpId: string) { - const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${mcpId}`) - const details: McpDownloadResponse = await response.json() - - if (details.readmeContent) { - // Disable markdown preview markers - const config = vscode.workspace.getConfiguration("markdown") - await config.update("preview.markEditorSelection", false, true) - - // Create URI with base64 encoded markdown content - const uri = vscode.Uri.parse( - `${DIFF_VIEW_URI_SCHEME}:${details.name} README?${Buffer.from(details.readmeContent).toString("base64")}`, - ) - - // close existing - const tabs = vscode.window.tabGroups.all - .flatMap((tg) => tg.tabs) - .filter((tab) => tab.label && tab.label.includes("README") && tab.label.includes("Preview")) - for (const tab of tabs) { - await vscode.window.tabGroups.close(tab) - } - - // Show only the preview - await vscode.commands.executeCommand("markdown.showPreview", uri, { - sideBySide: true, - preserveFocus: true, - }) - } - } } diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 97255c2ab0..85398a2264 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -1,4 +1,4 @@ -import { useState } from "react" +import { useEffect, useState } from "react" import { VSCodeButton, VSCodeCheckbox, @@ -21,6 +21,7 @@ import McpToolRow from "./McpToolRow" import McpResourceRow from "./McpResourceRow" import McpEnabledToggle from "./McpEnabledToggle" import McpMarketplaceView from "./marketplace/McpMarketplaceView" +import styled from "styled-components" type McpViewProps = { onDone: () => void @@ -35,7 +36,15 @@ const McpView = ({ onDone }: McpViewProps) => { setEnableMcpServerCreation, } = useExtensionState() const { t } = useAppTranslation() - const [activeTab, setActiveTab] = useState<"installed" | "marketplace">("installed") + const [activeTab, setActiveTab] = useState("installed") + + const handleTabChange = (tab: string) => { + setActiveTab(tab) + } + + useEffect(() => { + vscode.postMessage({ type: "silentlyRefreshMcpMarketplace" }) + }, []) return ( @@ -45,124 +54,155 @@ const McpView = ({ onDone }: McpViewProps) => { -
- - setActiveTab("installed")}> - {t("mcp:tabs.installed")} - - setActiveTab("marketplace")}> - {t("mcp:tabs.marketplace")} - - - -
- - - Model Context Protocol - - - community-made servers - - -
+
+ handleTabChange("installed")}> + {t("mcp:tabs.installed")} + + handleTabChange("marketplace")}> + {t("mcp:tabs.marketplace")} + +
- - - {mcpEnabled && ( - <> -
- { - setEnableMcpServerCreation(e.target.checked) - vscode.postMessage({ - type: "enableMcpServerCreation", - bool: e.target.checked, - }) - }}> - - {t("mcp:enableServerCreation.title")} - - -

- {t("mcp:enableServerCreation.description")} -

-
+ {activeTab === "installed" && ( + <> +
+ + + Model Context Protocol + + + community-made servers + + +
- {/* Server List */} - {servers.length > 0 ? ( -
- {servers.map((server) => ( - - ))} -
- ) : ( -
- + + {mcpEnabled && ( + <> +
+ { + setEnableMcpServerCreation(e.target.checked) + vscode.postMessage({ + type: "enableMcpServerCreation", + bool: e.target.checked, + }) + }}> + {t("mcp:enableServerCreation.title")} + +

+ {t("mcp:enableServerCreation.description")} +

+
+ + {/* Server List */} + {servers.length > 0 ? ( +
+ {servers.map((server) => ( + -

- {t("mcp:emptyState.noServers")} -

- setActiveTab("marketplace")} - appearance="secondary"> - {t("mcp:emptyState.browseMarketplace")} - -
- )} - - {/* Edit Settings Button */} -
+ ))} +
+ ) : ( +
+

+ {t("mcp:emptyState.noServers")} +

{ - vscode.postMessage({ type: "openMcpSettings" }) - }}> - - {t("mcp:editSettings")} + onClick={() => setActiveTab("marketplace")} + appearance="secondary"> + {t("mcp:emptyState.browseMarketplace")}
- - )} - - - - - - -
+ )} + + {/* Edit Settings Button */} +
+ { + vscode.postMessage({ type: "openMcpSettings" }) + }}> + + {t("mcp:editSettings")} + +
+ + )} + + )} + + {activeTab === "marketplace" && } ) } +const StyledTabButton = styled.button<{ isActive: boolean }>` + background: none; + border: none; + border-bottom: 2px solid ${(props) => (props.isActive ? "var(--vscode-foreground)" : "transparent")}; + color: ${(props) => (props.isActive ? "var(--vscode-foreground)" : "var(--vscode-descriptionForeground)")}; + padding: 8px 16px; + cursor: pointer; + font-size: 13px; + margin-bottom: -1px; + font-family: inherit; + + &:hover { + color: var(--vscode-foreground); + } +` + +const TabButton = ({ + children, + isActive, + onClick, +}: { + children: React.ReactNode + isActive: boolean + onClick: () => void +}) => ( + + {children} + +) + const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowMcp?: boolean }) => { const { t } = useAppTranslation() const [isExpanded, setIsExpanded] = useState(false) diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx index 387f07009d..ab7c56903c 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -12,6 +12,7 @@ interface McpMarketplaceCardProps { const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) => { const isInstalled = installedServers.some((server) => server.name === item.mcpId) const [isDownloading, setIsDownloading] = useState(false) + const [isLoading, setIsLoading] = useState(false) const githubLinkRef = useRef(null) const handleMessage = useCallback((event: MessageEvent) => { @@ -29,13 +30,13 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) <>
- {/* Header with logo */} -
- {item.logoUrl ? ( + {/* Main container with logo and content */} +
+ {/* Logo */} + {item.logoUrl && ( {item.name} - ) : ( - )} -
-
{item.name}
-
- by {item.author} -
-
-
- - {/* Description */} -
- {item.description} -
- {/* Stats and Install */} -
+ {/* Content section */}
-
- - {item.githubStars} -
-
- - {item.downloadCount} + {/* First row: name and install button */} +
+

+ {item.name} +

+
{ + e.stopPropagation() // Prevent card click when clicking install + if (!isInstalled && !isDownloading) { + setIsDownloading(true) + vscode.postMessage({ + type: "downloadMcp", + mcpId: item.mcpId, + }) + } + }} + style={{}}> + + {isInstalled ? "Installed" : isDownloading ? "Installing..." : "Install"} + +
- +
+ + {/* Description and tags */} +
+

{item.description}

{ - e.stopPropagation() // Prevent card click when clicking install - if (!isInstalled && !isDownloading) { - setIsDownloading(true) - vscode.postMessage({ - type: "downloadMcp", - mcpId: item.mcpId, - }) - } + style={{ + display: "flex", + gap: "6px", + flexWrap: "nowrap", + overflow: "hidden", + position: "relative", }}> - - {isInstalled ? "Installed" : isDownloading ? "Installing..." : "Install"} - + + {item.category} + + {item.tags.map((tag, index) => ( + + {tag} + {index === item.tags.length - 1 ? "" : ""} + + ))} +
@@ -168,15 +262,13 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) ) } -const StyledInstallButton = styled.div<{ disabled?: boolean; $isInstalled?: boolean }>` - display: inline-flex; - align-items: center; - justify-content: center; +const StyledInstallButton = styled.button<{ $isInstalled?: boolean }>` + font-size: 12px; + font-weight: 500; + padding: 2px 6px; border-radius: 2px; - font-size: 13px; - font-weight: 400; - padding: 4px 12px; - cursor: ${(props) => (props.disabled ? "default" : "pointer")}; + border: none; + cursor: pointer; background: ${(props) => props.$isInstalled ? "var(--vscode-button-secondaryBackground)" : "var(--vscode-button-background)"}; color: var(--vscode-button-foreground); diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx index 05c119e78d..d6473b2867 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -1,29 +1,61 @@ -import { useEffect, useState, useMemo } from "react" +import { useEffect, useMemo, useState } from "react" import { VSCodeButton, VSCodeProgressRing, + VSCodeRadioGroup, + VSCodeRadio, VSCodeDropdown, VSCodeOption, VSCodeTextField, } from "@vscode/webview-ui-toolkit/react" -import { useAppTranslation } from "../../../i18n/TranslationContext" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { McpMarketplaceItem } from "../../../../../src/shared/mcp" import { useExtensionState } from "../../../context/ExtensionStateContext" import { vscode } from "../../../utils/vscode" -import { McpMarketplaceItem } from "../../../../../src/shared/mcp" import McpMarketplaceCard from "./McpMarketplaceCard" import McpSubmitCard from "./McpSubmitCard" - -type SortOption = "stars" | "downloads" | "newest" | "updated" - const McpMarketplaceView = () => { const { t } = useAppTranslation() const { mcpServers } = useExtensionState() + const [items, setItems] = useState([]) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) - const [items, setItems] = useState([]) + const [isRefreshing, setIsRefreshing] = useState(false) const [searchQuery, setSearchQuery] = useState("") - const [selectedCategory, setSelectedCategory] = useState("all") - const [sortBy, setSortBy] = useState("stars") + const [selectedCategory, setSelectedCategory] = useState(null) + const [sortBy, setSortBy] = useState<"downloadCount" | "stars" | "name" | "newest">("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) + case "newest": + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + default: + return 0 + } + }) + }, [items, searchQuery, selectedCategory, sortBy]) useEffect(() => { const handleMessage = (event: MessageEvent) => { @@ -31,75 +63,50 @@ const McpMarketplaceView = () => { if (message.type === "mcpMarketplaceCatalog") { if (message.error) { setError(message.error) - } else if (message.mcpMarketplaceCatalog) { + } else { + setItems(message.mcpMarketplaceCatalog?.items || []) setError(null) - setItems(message.mcpMarketplaceCatalog.items) } setIsLoading(false) + setIsRefreshing(false) } else if (message.type === "mcpDownloadDetails") { if (message.error) { setError(message.error) } } } - window.addEventListener("message", handleMessage) - vscode.postMessage({ type: "fetchMcpMarketplace" }) - return () => window.removeEventListener("message", handleMessage) - }, []) - const categories = useMemo(() => { - const categorySet = new Set(items.map((item) => item.category)) - return ["all", ...Array.from(categorySet)] - }, [items]) + window.addEventListener("message", handleMessage) - const filteredAndSortedItems = useMemo(() => { - let filtered = items + // Fetch marketplace catalog + fetchMarketplace() - // Apply search filter - if (searchQuery) { - const query = searchQuery.toLowerCase() - filtered = filtered.filter( - (item) => - item.name.toLowerCase().includes(query) || - item.description.toLowerCase().includes(query) || - item.tags.some((tag) => tag.toLowerCase().includes(query)), - ) + return () => { + window.removeEventListener("message", handleMessage) } + }, []) - // Apply category filter - if (selectedCategory !== "all") { - filtered = filtered.filter((item) => item.category === selectedCategory) + const fetchMarketplace = (forceRefresh: boolean = false) => { + if (forceRefresh) { + setIsRefreshing(true) + } else { + setIsLoading(true) } + setError(null) + vscode.postMessage({ type: "fetchMcpMarketplace", bool: forceRefresh }) + } - // Apply sorting - return [...filtered].sort((a, b) => { - switch (sortBy) { - case "stars": - return b.githubStars - a.githubStars - case "downloads": - return b.downloadCount - a.downloadCount - case "newest": - return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - case "updated": - return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() - default: - return 0 - } - }) - }, [items, searchQuery, selectedCategory, sortBy]) - - if (isLoading) { + if (isLoading || isRefreshing) { return (
- - {t("mcp:marketplace.loading")} +
) } @@ -110,20 +117,15 @@ const McpMarketplaceView = () => { style={{ display: "flex", flexDirection: "column", - alignItems: "center", justifyContent: "center", - padding: "40px 20px", - color: "var(--vscode-errorForeground)", - textAlign: "center", + alignItems: "center", + height: "100%", + padding: "20px", + gap: "12px", }}> - -

{error}

- { - setIsLoading(true) - setError(null) - vscode.postMessage({ type: "fetchMcpMarketplace" }) - }}> +
{error}
+ fetchMarketplace(true)}> + {t("mcp:marketplace.retry")}
@@ -131,65 +133,157 @@ const McpMarketplaceView = () => { } return ( -
-
+
+
+ {/* Search row */} setSearchQuery((e.target as HTMLInputElement).value)} - style={{ flexGrow: 1 }} - /> - setSelectedCategory((e.target as HTMLSelectElement).value)}> - {categories.map((category) => ( - - {category === "all" ? t("mcp:marketplace.allCategories") : category} - - ))} - - setSortBy((e.target as HTMLSelectElement).value as SortOption)}> - {t("mcp:marketplace.sortByStars")} - {t("mcp:marketplace.sortByDownloads")} - {t("mcp:marketplace.sortByNewest")} - {t("mcp:marketplace.sortByUpdated")} - -
+ onInput={(e) => setSearchQuery((e.target as HTMLInputElement).value)}> +
+ {searchQuery && ( +
setSearchQuery("")} + slot="end" + style={{ + display: "flex", + justifyContent: "center", + alignItems: "center", + height: "100%", + cursor: "pointer", + }} + /> + )} + - {filteredAndSortedItems.length === 0 ? ( + {/* Filter row */}
- -

- {searchQuery || selectedCategory !== "all" - ? t("mcp:marketplace.noResults") - : t("mcp:marketplace.noServers")} -

+ + {t("mcp:marketplace.filter")} + +
+ setSelectedCategory((e.target as HTMLSelectElement).value || null)}> + {t("mcp:marketplace.allCategories")} + {categories.map((category) => ( + + {category} + + ))} + +
- ) : ( + + {/* Sort row */}
- {filteredAndSortedItems.map((item) => ( - - ))} - + + {t("mcp:marketplace.sort")} + + setSortBy((e.target as HTMLInputElement).value as typeof sortBy)}> + {t("mcp:marketplace.sortByDownloadCount")} + {t("mcp:marketplace.sortByStars")} + {t("mcp:marketplace.sortByNewest")} + {t("mcp:marketplace.sortByName")} +
- )} +
+ + +
+ {filteredItems.length === 0 ? ( +
+ {searchQuery || selectedCategory + ? t("mcp:marketplace.noResults") + : t("mcp:marketplace.noServers")} +
+ ) : ( + filteredItems.map((item) => ( + + )) + )} + +
) } diff --git a/webview-ui/src/i18n/locales/en/mcp.json b/webview-ui/src/i18n/locales/en/mcp.json index 2ecbe97dc6..2ab52f7da7 100644 --- a/webview-ui/src/i18n/locales/en/mcp.json +++ b/webview-ui/src/i18n/locales/en/mcp.json @@ -17,7 +17,7 @@ "noDescription": "No description" }, "tabs": { - "installed": "Installed", + "installed": "Installed & Settings", "marketplace": "Marketplace", "tools": "Tools", "resources": "Resources" @@ -36,11 +36,13 @@ "install": "Install", "by": "by {{author}}", "searchPlaceholder": "Search servers...", + "filter": "Filter:", "allCategories": "All Categories", - "sortByStars": "Sort by Stars", - "sortByDownloads": "Sort by Downloads", - "sortByNewest": "Sort by Newest", - "sortByUpdated": "Sort by Last Updated" + "sort": "Sort:", + "sortByStars": "Most Stars", + "sortByDownloadCount": "Most Installs", + "sortByNewest": "Newest", + "sortByName": "Name" }, "networkTimeout": { "label": "Network Timeout", From 9fcb4ee18798a462fe96fea7337bf2bd9645ae9a Mon Sep 17 00:00:00 2001 From: HadasaSchechterV <100204928+HadasaSchechterV@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:54:44 +0200 Subject: [PATCH 6/7] Fix unit test & localization check --- .../webview/__tests__/ClineProvider.test.ts | 5 ++++ webview-ui/src/i18n/locales/ca/mcp.json | 22 +++++++++++++++- webview-ui/src/i18n/locales/de/mcp.json | 22 +++++++++++++++- webview-ui/src/i18n/locales/es/mcp.json | 24 +++++++++++++++-- webview-ui/src/i18n/locales/fr/mcp.json | 22 +++++++++++++++- webview-ui/src/i18n/locales/hi/mcp.json | 22 +++++++++++++++- webview-ui/src/i18n/locales/it/mcp.json | 22 +++++++++++++++- webview-ui/src/i18n/locales/ja/mcp.json | 24 +++++++++++++++-- webview-ui/src/i18n/locales/ko/mcp.json | 22 +++++++++++++++- webview-ui/src/i18n/locales/pl/mcp.json | 24 +++++++++++++++-- webview-ui/src/i18n/locales/pt-BR/mcp.json | 22 +++++++++++++++- webview-ui/src/i18n/locales/tr/mcp.json | 24 +++++++++++++++-- webview-ui/src/i18n/locales/vi/mcp.json | 26 ++++++++++++++++--- webview-ui/src/i18n/locales/zh-CN/mcp.json | 22 +++++++++++++++- webview-ui/src/i18n/locales/zh-TW/mcp.json | 22 +++++++++++++++- 15 files changed, 305 insertions(+), 20 deletions(-) diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 7530052b85..3b2aa2cba5 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -110,6 +110,11 @@ jest.mock( { virtual: true }, ) +// Mock DiffViewProvider +jest.mock("../../../integrations/editor/DiffViewProvider", () => ({ + DIFF_VIEW_URI_SCHEME: "DIFF_VIEW_URI_SCHEME", +})) + // Initialize mocks const mockAddCustomInstructions = jest.fn().mockResolvedValue("Combined instructions") ;(jest.requireMock("../../prompts/sections/custom-instructions") as any).addCustomInstructions = diff --git a/webview-ui/src/i18n/locales/ca/mcp.json b/webview-ui/src/i18n/locales/ca/mcp.json index 1f339dd09f..a5af833845 100644 --- a/webview-ui/src/i18n/locales/ca/mcp.json +++ b/webview-ui/src/i18n/locales/ca/mcp.json @@ -17,12 +17,32 @@ "noDescription": "Sense descripció" }, "tabs": { + "installed": "Instal·lats i Configuració", + "marketplace": "Mercat", "tools": "Eines", "resources": "Recursos" }, "emptyState": { "noTools": "No s'han trobat eines", - "noResources": "No s'han trobat recursos" + "noResources": "No s'han trobat recursos", + "noServers": "No hi ha servidors instal·lats", + "browseMarketplace": "Explorar el Mercat" + }, + "marketplace": { + "loading": "Carregant el mercat...", + "noServers": "No hi ha servidors disponibles", + "noResults": "No s'han trobat servidors coincidents", + "retry": "Tornar a provar", + "install": "Instal·lar", + "by": "per {{author}}", + "searchPlaceholder": "Cercar servidors...", + "filter": "Filtre:", + "allCategories": "Totes les categories", + "sort": "Ordenar:", + "sortByStars": "Més estrelles", + "sortByDownloadCount": "Més instal·lacions", + "sortByNewest": "Més recents", + "sortByName": "Nom" }, "networkTimeout": { "label": "Temps d'espera de xarxa", diff --git a/webview-ui/src/i18n/locales/de/mcp.json b/webview-ui/src/i18n/locales/de/mcp.json index c7d11df051..73b2e4f0e7 100644 --- a/webview-ui/src/i18n/locales/de/mcp.json +++ b/webview-ui/src/i18n/locales/de/mcp.json @@ -17,12 +17,32 @@ "noDescription": "Keine Beschreibung" }, "tabs": { + "installed": "Installiert & Einstellungen", + "marketplace": "Marketplace", "tools": "Tools", "resources": "Ressourcen" }, "emptyState": { "noTools": "Keine Tools gefunden", - "noResources": "Keine Ressourcen gefunden" + "noResources": "Keine Ressourcen gefunden", + "noServers": "Keine Server installiert", + "browseMarketplace": "Marketplace durchsuchen" + }, + "marketplace": { + "loading": "Lade Marketplace...", + "noServers": "Keine Server verfügbar", + "noResults": "Keine passenden Server gefunden", + "retry": "Wiederholen", + "install": "Installieren", + "by": "von {{author}}", + "searchPlaceholder": "Server suchen...", + "filter": "Filter:", + "allCategories": "Alle Kategorien", + "sort": "Sortieren:", + "sortByStars": "Meiste Sterne", + "sortByDownloadCount": "Meiste Installationen", + "sortByNewest": "Neueste", + "sortByName": "Name" }, "networkTimeout": { "label": "Netzwerk-Timeout", diff --git a/webview-ui/src/i18n/locales/es/mcp.json b/webview-ui/src/i18n/locales/es/mcp.json index 8cc199ad25..70bd41b972 100644 --- a/webview-ui/src/i18n/locales/es/mcp.json +++ b/webview-ui/src/i18n/locales/es/mcp.json @@ -18,11 +18,31 @@ }, "tabs": { "tools": "Herramientas", - "resources": "Recursos" + "resources": "Recursos", + "installed": "Instalados y Configuración", + "marketplace": "Marketplace" }, "emptyState": { "noTools": "No se encontraron herramientas", - "noResources": "No se encontraron recursos" + "noResources": "No se encontraron recursos", + "noServers": "No hay servidores instalados", + "browseMarketplace": "Explorar Marketplace" + }, + "marketplace": { + "loading": "Cargando marketplace...", + "noServers": "No hay servidores disponibles", + "noResults": "No se encontraron servidores coincidentes", + "retry": "Reintentar", + "install": "Instalar", + "by": "por {{author}}", + "searchPlaceholder": "Buscar servidores...", + "filter": "Filtrar:", + "allCategories": "Todas las categorías", + "sort": "Ordenar:", + "sortByStars": "Más estrellas", + "sortByDownloadCount": "Más instalaciones", + "sortByNewest": "Más recientes", + "sortByName": "Nombre" }, "networkTimeout": { "label": "Tiempo de espera de red", diff --git a/webview-ui/src/i18n/locales/fr/mcp.json b/webview-ui/src/i18n/locales/fr/mcp.json index 2a239cd33e..98a11f6be3 100644 --- a/webview-ui/src/i18n/locales/fr/mcp.json +++ b/webview-ui/src/i18n/locales/fr/mcp.json @@ -17,12 +17,32 @@ "noDescription": "Aucune description" }, "tabs": { + "installed": "Installés & Paramètres", + "marketplace": "Marketplace", "tools": "Outils", "resources": "Ressources" }, "emptyState": { "noTools": "Aucun outil trouvé", - "noResources": "Aucune ressource trouvée" + "noResources": "Aucune ressource trouvée", + "noServers": "Aucun serveur installé", + "browseMarketplace": "Parcourir la Marketplace" + }, + "marketplace": { + "loading": "Chargement de la marketplace...", + "noServers": "Aucun serveur disponible", + "noResults": "Aucun serveur correspondant trouvé", + "retry": "Réessayer", + "install": "Installer", + "by": "par {{author}}", + "searchPlaceholder": "Rechercher des serveurs...", + "filter": "Filtrer :", + "allCategories": "Toutes les catégories", + "sort": "Trier :", + "sortByStars": "Plus d'étoiles", + "sortByDownloadCount": "Plus d'installations", + "sortByNewest": "Plus récents", + "sortByName": "Nom" }, "networkTimeout": { "label": "Délai d'attente réseau", diff --git a/webview-ui/src/i18n/locales/hi/mcp.json b/webview-ui/src/i18n/locales/hi/mcp.json index 5827787b67..4c836eba18 100644 --- a/webview-ui/src/i18n/locales/hi/mcp.json +++ b/webview-ui/src/i18n/locales/hi/mcp.json @@ -17,12 +17,32 @@ "noDescription": "कोई विवरण नहीं" }, "tabs": { + "installed": "स्थापित और सेटिंग्स", + "marketplace": "मार्केटप्लेस", "tools": "उपकरण", "resources": "संसाधन" }, "emptyState": { "noTools": "कोई उपकरण नहीं मिला", - "noResources": "कोई संसाधन नहीं मिला" + "noResources": "कोई संसाधन नहीं मिला", + "noServers": "कोई सर्वर स्थापित नहीं है", + "browseMarketplace": "मार्केटप्लेस ब्राउज़ करें" + }, + "marketplace": { + "loading": "मार्केटप्लेस लोड हो रहा है...", + "noServers": "कोई सर्वर उपलब्ध नहीं है", + "noResults": "कोई मिलान करने वाला सर्वर नहीं मिला", + "retry": "पुनः प्रयास करें", + "install": "स्थापित करें", + "by": "{{author}} द्वारा", + "searchPlaceholder": "सर्वर खोजें...", + "filter": "फ़िल्टर:", + "allCategories": "सभी श्रेणियां", + "sort": "क्रमबद्ध करें:", + "sortByStars": "सर्वाधिक स्टार", + "sortByDownloadCount": "सर्वाधिक स्थापना", + "sortByNewest": "नवीनतम", + "sortByName": "नाम" }, "networkTimeout": { "label": "नेटवर्क टाइमआउट", diff --git a/webview-ui/src/i18n/locales/it/mcp.json b/webview-ui/src/i18n/locales/it/mcp.json index c297cca91a..c7bf47ec8d 100644 --- a/webview-ui/src/i18n/locales/it/mcp.json +++ b/webview-ui/src/i18n/locales/it/mcp.json @@ -17,12 +17,32 @@ "noDescription": "Nessuna descrizione" }, "tabs": { + "installed": "Installati & Impostazioni", + "marketplace": "Marketplace", "tools": "Strumenti", "resources": "Risorse" }, "emptyState": { "noTools": "Nessuno strumento trovato", - "noResources": "Nessuna risorsa trovata" + "noResources": "Nessuna risorsa trovata", + "noServers": "Nessun server installato", + "browseMarketplace": "Sfoglia il Marketplace" + }, + "marketplace": { + "loading": "Caricamento marketplace...", + "noServers": "Nessun server disponibile", + "noResults": "Nessun server corrispondente trovato", + "retry": "Riprova", + "install": "Installa", + "by": "di {{author}}", + "searchPlaceholder": "Cerca server...", + "filter": "Filtra:", + "allCategories": "Tutte le categorie", + "sort": "Ordina:", + "sortByStars": "Più stelle", + "sortByDownloadCount": "Più installazioni", + "sortByNewest": "Più recenti", + "sortByName": "Nome" }, "networkTimeout": { "label": "Timeout di rete", diff --git a/webview-ui/src/i18n/locales/ja/mcp.json b/webview-ui/src/i18n/locales/ja/mcp.json index 62e340db84..66a81301db 100644 --- a/webview-ui/src/i18n/locales/ja/mcp.json +++ b/webview-ui/src/i18n/locales/ja/mcp.json @@ -18,11 +18,31 @@ }, "tabs": { "tools": "ツール", - "resources": "リソース" + "resources": "リソース", + "installed": "インストール済み & 設定", + "marketplace": "マーケットプレイス" }, "emptyState": { "noTools": "ツールが見つかりません", - "noResources": "リソースが見つかりません" + "noResources": "リソースが見つかりません", + "noServers": "サーバーがインストールされていません", + "browseMarketplace": "マーケットプレイスを閲覧" + }, + "marketplace": { + "loading": "マーケットプレイスを読み込み中...", + "noServers": "利用可能なサーバーがありません", + "noResults": "一致するサーバーが見つかりません", + "retry": "再試行", + "install": "インストール", + "by": "作成者:{{author}}", + "searchPlaceholder": "サーバーを検索...", + "filter": "フィルター:", + "allCategories": "すべてのカテゴリー", + "sort": "並び替え:", + "sortByStars": "スター数順", + "sortByDownloadCount": "インストール数順", + "sortByNewest": "新着順", + "sortByName": "名前順" }, "networkTimeout": { "label": "ネットワークタイムアウト", diff --git a/webview-ui/src/i18n/locales/ko/mcp.json b/webview-ui/src/i18n/locales/ko/mcp.json index f79409d441..d6aac5dbbb 100644 --- a/webview-ui/src/i18n/locales/ko/mcp.json +++ b/webview-ui/src/i18n/locales/ko/mcp.json @@ -17,12 +17,32 @@ "noDescription": "설명 없음" }, "tabs": { + "installed": "설치됨 & 설정", + "marketplace": "마켓플레이스", "tools": "도구", "resources": "리소스" }, "emptyState": { "noTools": "도구를 찾을 수 없음", - "noResources": "리소스를 찾을 수 없음" + "noResources": "리소스를 찾을 수 없음", + "noServers": "설치된 서버 없음", + "browseMarketplace": "마켓플레이스 둘러보기" + }, + "marketplace": { + "loading": "마켓플레이스 로딩 중...", + "noServers": "사용 가능한 서버 없음", + "noResults": "일치하는 서버를 찾을 수 없음", + "retry": "재시도", + "install": "설치", + "by": "작성자: {{author}}", + "searchPlaceholder": "서버 검색...", + "filter": "필터:", + "allCategories": "모든 카테고리", + "sort": "정렬:", + "sortByStars": "별점순", + "sortByDownloadCount": "설치 횟수순", + "sortByNewest": "최신순", + "sortByName": "이름순" }, "networkTimeout": { "label": "네트워크 타임아웃", diff --git a/webview-ui/src/i18n/locales/pl/mcp.json b/webview-ui/src/i18n/locales/pl/mcp.json index 7308698d79..e9da6b7bed 100644 --- a/webview-ui/src/i18n/locales/pl/mcp.json +++ b/webview-ui/src/i18n/locales/pl/mcp.json @@ -18,11 +18,31 @@ }, "tabs": { "tools": "Narzędzia", - "resources": "Zasoby" + "resources": "Zasoby", + "installed": "Zainstalowane & Ustawienia", + "marketplace": "Marketplace" }, "emptyState": { "noTools": "Nie znaleziono narzędzi", - "noResources": "Nie znaleziono zasobów" + "noResources": "Nie znaleziono zasobów", + "noServers": "Brak zainstalowanych serwerów", + "browseMarketplace": "Przeglądaj Marketplace" + }, + "marketplace": { + "loading": "Ładowanie marketplace...", + "noServers": "Brak dostępnych serwerów", + "noResults": "Nie znaleziono pasujących serwerów", + "retry": "Spróbuj ponownie", + "install": "Instaluj", + "by": "autor: {{author}}", + "searchPlaceholder": "Szukaj serwerów...", + "filter": "Filtruj:", + "allCategories": "Wszystkie kategorie", + "sort": "Sortuj:", + "sortByStars": "Najwięcej gwiazdek", + "sortByDownloadCount": "Najwięcej instalacji", + "sortByNewest": "Najnowsze", + "sortByName": "Nazwa" }, "networkTimeout": { "label": "Limit czasu sieci", diff --git a/webview-ui/src/i18n/locales/pt-BR/mcp.json b/webview-ui/src/i18n/locales/pt-BR/mcp.json index e5713608eb..5652cc56c5 100644 --- a/webview-ui/src/i18n/locales/pt-BR/mcp.json +++ b/webview-ui/src/i18n/locales/pt-BR/mcp.json @@ -17,12 +17,32 @@ "noDescription": "Sem descrição" }, "tabs": { + "installed": "Instalados e Configurações", + "marketplace": "Marketplace", "tools": "Ferramentas", "resources": "Recursos" }, "emptyState": { "noTools": "Nenhuma ferramenta encontrada", - "noResources": "Nenhum recurso encontrado" + "noResources": "Nenhum recurso encontrado", + "noServers": "Nenhum servidor instalado", + "browseMarketplace": "Explorar Marketplace" + }, + "marketplace": { + "loading": "Carregando marketplace...", + "noServers": "Nenhum servidor disponível", + "noResults": "Nenhum servidor correspondente encontrado", + "retry": "Tentar novamente", + "install": "Instalar", + "by": "por {{author}}", + "searchPlaceholder": "Pesquisar servidores...", + "filter": "Filtrar:", + "allCategories": "Todas as Categorias", + "sort": "Ordenar:", + "sortByStars": "Mais Estrelas", + "sortByDownloadCount": "Mais Instalações", + "sortByNewest": "Mais Recentes", + "sortByName": "Nome" }, "networkTimeout": { "label": "Tempo limite de rede", diff --git a/webview-ui/src/i18n/locales/tr/mcp.json b/webview-ui/src/i18n/locales/tr/mcp.json index f0610630c0..81567007fa 100644 --- a/webview-ui/src/i18n/locales/tr/mcp.json +++ b/webview-ui/src/i18n/locales/tr/mcp.json @@ -18,11 +18,31 @@ }, "tabs": { "tools": "Araçlar", - "resources": "Kaynaklar" + "resources": "Kaynaklar", + "installed": "Yüklü & Ayarlar", + "marketplace": "Pazar" }, "emptyState": { "noTools": "Araç bulunamadı", - "noResources": "Kaynak bulunamadı" + "noResources": "Kaynak bulunamadı", + "noServers": "Yüklü sunucu yok", + "browseMarketplace": "Pazarı Gözat" + }, + "marketplace": { + "loading": "Pazar yükleniyor...", + "noServers": "Kullanılabilir sunucu yok", + "noResults": "Eşleşen sunucu bulunamadı", + "retry": "Tekrar dene", + "install": "Yükle", + "by": "yazar: {{author}}", + "searchPlaceholder": "Sunucu ara...", + "filter": "Filtrele:", + "allCategories": "Tüm Kategoriler", + "sort": "Sırala:", + "sortByStars": "En çok yıldız", + "sortByDownloadCount": "En çok yükleme", + "sortByNewest": "En yeni", + "sortByName": "İsim" }, "networkTimeout": { "label": "Ağ Zaman Aşımı", diff --git a/webview-ui/src/i18n/locales/vi/mcp.json b/webview-ui/src/i18n/locales/vi/mcp.json index 37f16cbec4..05fa170ce0 100644 --- a/webview-ui/src/i18n/locales/vi/mcp.json +++ b/webview-ui/src/i18n/locales/vi/mcp.json @@ -18,11 +18,31 @@ }, "tabs": { "tools": "Công cụ", - "resources": "Tài nguyên" + "resources": "Tài nguyên", + "installed": "Đã cài đặt & Cài đặt", + "marketplace": "Marketplace" }, "emptyState": { - "noTools": "Không tìm thấy công cụ nào", - "noResources": "Không tìm thấy tài nguyên nào" + "noTools": "Không tìm thấy công cụ", + "noResources": "Không tìm thấy tài nguyên", + "noServers": "Chưa cài đặt máy chủ", + "browseMarketplace": "Duyệt Marketplace" + }, + "marketplace": { + "loading": "Đang tải marketplace...", + "noServers": "Không có máy chủ khả dụng", + "noResults": "Không tìm thấy máy chủ phù hợp", + "retry": "Thử lại", + "install": "Cài đặt", + "by": "bởi {{author}}", + "searchPlaceholder": "Tìm kiếm máy chủ...", + "filter": "Lọc:", + "allCategories": "Tất cả danh mục", + "sort": "Sắp xếp:", + "sortByStars": "Nhiều sao nhất", + "sortByDownloadCount": "Nhiều lượt cài đặt nhất", + "sortByNewest": "Mới nhất", + "sortByName": "Tên" }, "networkTimeout": { "label": "Thời gian chờ mạng", diff --git a/webview-ui/src/i18n/locales/zh-CN/mcp.json b/webview-ui/src/i18n/locales/zh-CN/mcp.json index f57f419091..32593a5378 100644 --- a/webview-ui/src/i18n/locales/zh-CN/mcp.json +++ b/webview-ui/src/i18n/locales/zh-CN/mcp.json @@ -17,12 +17,32 @@ "noDescription": "无描述" }, "tabs": { + "installed": "已安装 & 设置", + "marketplace": "应用市场", "tools": "工具", "resources": "资源" }, "emptyState": { "noTools": "未找到工具", - "noResources": "未找到资源" + "noResources": "未找到资源", + "noServers": "未安装服务器", + "browseMarketplace": "浏览应用市场" + }, + "marketplace": { + "loading": "正在加载应用市场...", + "noServers": "无可用服务器", + "noResults": "未找到匹配的服务器", + "retry": "重试", + "install": "安装", + "by": "作者:{{author}}", + "searchPlaceholder": "搜索服务器...", + "filter": "筛选:", + "allCategories": "所有类别", + "sort": "排序:", + "sortByStars": "最多星标", + "sortByDownloadCount": "最多安装", + "sortByNewest": "最新", + "sortByName": "名称" }, "networkTimeout": { "label": "网络超时", diff --git a/webview-ui/src/i18n/locales/zh-TW/mcp.json b/webview-ui/src/i18n/locales/zh-TW/mcp.json index e8ddba2f2c..e0509dc1ce 100644 --- a/webview-ui/src/i18n/locales/zh-TW/mcp.json +++ b/webview-ui/src/i18n/locales/zh-TW/mcp.json @@ -17,12 +17,32 @@ "noDescription": "無說明" }, "tabs": { + "installed": "已安裝 & 設定", + "marketplace": "應用市集", "tools": "工具", "resources": "資源" }, "emptyState": { "noTools": "未找到工具", - "noResources": "未找到資源" + "noResources": "未找到資源", + "noServers": "未安裝伺服器", + "browseMarketplace": "瀏覽應用市集" + }, + "marketplace": { + "loading": "正在載入應用市集...", + "noServers": "無可用伺服器", + "noResults": "未找到匹配的伺服器", + "retry": "重試", + "install": "安裝", + "by": "作者:{{author}}", + "searchPlaceholder": "搜尋伺服器...", + "filter": "篩選:", + "allCategories": "所有類別", + "sort": "排序:", + "sortByStars": "最多星標", + "sortByDownloadCount": "最多安裝", + "sortByNewest": "最新", + "sortByName": "名稱" }, "networkTimeout": { "label": "網路逾時", From f73d3c4f16fff02569bdc6f7a46c9525461ca82d Mon Sep 17 00:00:00 2001 From: HadasaSchechterV <100204928+HadasaSchechterV@users.noreply.github.com> Date: Thu, 20 Mar 2025 10:51:37 +0200 Subject: [PATCH 7/7] move mcp marketplace from client provider to its own service file --- src/core/webview/ClineProvider.ts | 218 +--------------------- src/services/mcp/McpMarketplaceService.ts | 210 +++++++++++++++++++++ 2 files changed, 219 insertions(+), 209 deletions(-) create mode 100644 src/services/mcp/McpMarketplaceService.ts diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 7825063fba..10607c90b4 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -67,8 +67,7 @@ import { getUri } from "./getUri" import { telemetryService } from "../../services/telemetry/TelemetryService" import { TelemetrySetting } from "../../shared/TelemetrySetting" import { getWorkspacePath } from "../../utils/path" -import { McpMarketplaceCatalog, McpServer, McpDownloadResponse } from "../../shared/mcp" -import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider" +import { McpMarketplaceService } from "../../services/mcp/McpMarketplaceService" /** * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -93,6 +92,7 @@ export class ClineProvider extends EventEmitter implements private contextProxy: ContextProxy configManager: ConfigManager customModesManager: CustomModesManager + private mcpMarketplaceService: McpMarketplaceService get cwd() { return getWorkspacePath() } @@ -124,6 +124,8 @@ export class ClineProvider extends EventEmitter implements .catch((error) => { this.outputChannel.appendLine(`Failed to initialize MCP Hub: ${error}`) }) + + this.mcpMarketplaceService = new McpMarketplaceService(this) } // Adds a new Cline instance to clineStack, marking the start of a new task. @@ -746,7 +748,7 @@ export class ClineProvider extends EventEmitter implements mcpServers: this.mcpHub.getAllServers(), }) } - this.prefetchMcpMarketplace() + this.mcpMarketplaceService.prefetchMcpMarketplace() const cacheDir = await this.ensureCacheDirectoryExists() @@ -1194,22 +1196,22 @@ export class ClineProvider extends EventEmitter implements } case "fetchMcpMarketplace": { - await this.fetchMcpMarketplace(message.bool) + await this.mcpMarketplaceService.fetchMcpMarketplace(message.bool) break } case "downloadMcp": { if (message.mcpId) { - await this.downloadMcp(message.mcpId) + await this.mcpMarketplaceService.downloadMcp(this.mcpHub, message.mcpId) } break } case "silentlyRefreshMcpMarketplace": { - await this.silentlyRefreshMcpMarketplace() + await this.mcpMarketplaceService.silentlyRefreshMcpMarketplace() break } case "openMcpMarketplaceServerDetails": { if (message.mcpId) { - await this.openMcpMarketplaceServerDetails(message.mcpId) + await this.mcpMarketplaceService.openMcpMarketplaceServerDetails(message.mcpId) } break } @@ -2272,208 +2274,6 @@ export class ClineProvider extends EventEmitter implements return undefined } - // MCP Marketplace - - private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise { - try { - const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", { - headers: { - "Content-Type": "application/json", - }, - }) - - if (!response.data) { - throw new Error("Invalid response from MCP marketplace API") - } - - const catalog: McpMarketplaceCatalog = { - items: (response.data || []).map((item: any) => ({ - ...item, - githubStars: item.githubStars ?? 0, - downloadCount: item.downloadCount ?? 0, - tags: item.tags ?? [], - })), - } - - // Store in global state - await this.updateGlobalState("mcpMarketplaceCatalog", catalog) - return catalog - } catch (error) { - console.error("Failed to fetch MCP marketplace:", error) - if (!silent) { - const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace" - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - error: errorMessage, - }) - vscode.window.showErrorMessage(errorMessage) - } - return undefined - } - } - - async prefetchMcpMarketplace() { - try { - await this.fetchMcpMarketplaceFromApi(true) - } catch (error) { - console.error("Failed to prefetch MCP marketplace:", error) - } - } - - async silentlyRefreshMcpMarketplace() { - try { - const catalog = await this.fetchMcpMarketplaceFromApi(true) - if (catalog) { - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - mcpMarketplaceCatalog: catalog, - }) - } - } catch (error) { - console.error("Failed to silently refresh MCP marketplace:", error) - } - } - - private async fetchMcpMarketplace(forceRefresh: boolean = false) { - try { - // Check if we have cached data - const cachedCatalog = (await this.getGlobalState("mcpMarketplaceCatalog")) as - | McpMarketplaceCatalog - | undefined - if (!forceRefresh && cachedCatalog?.items) { - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - mcpMarketplaceCatalog: cachedCatalog, - }) - return - } - - const catalog = await this.fetchMcpMarketplaceFromApi(false) - if (catalog) { - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - mcpMarketplaceCatalog: catalog, - }) - } - } catch (error) { - console.error("Failed to handle cached MCP marketplace:", error) - const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace" - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - error: errorMessage, - }) - vscode.window.showErrorMessage(errorMessage) - } - } - - private async downloadMcp(mcpId: string) { - try { - // First check if we already have this MCP server installed - const servers = this.mcpHub?.getServers() || [] - const isInstalled = servers.some((server: McpServer) => server.name === mcpId) - - 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 }, - { - headers: { "Content-Type": "application/json" }, - timeout: 10000, - }, - ) - - if (!response.data) { - 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 task with context from README - const task = `Set up the MCP server from ${mcpDetails.githubUrl}. Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json. Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}` - - // Initialize task and show chat view - await this.initClineWithTask(task) - await this.postMessageToWebview({ - type: "action", - action: "chatButtonClicked", - }) - } catch (error) { - console.error("Failed to download MCP:", error) - 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", - error: errorMessage, - }) - } - } - - private async openMcpMarketplaceServerDetails(mcpId: string) { - const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${mcpId}`) - const details: McpDownloadResponse = await response.json() - - if (details.readmeContent) { - // Disable markdown preview markers - const config = vscode.workspace.getConfiguration("markdown") - await config.update("preview.markEditorSelection", false, true) - - // Create URI with base64 encoded markdown content - const uri = vscode.Uri.parse( - `${DIFF_VIEW_URI_SCHEME}:${details.name} README?${Buffer.from(details.readmeContent).toString("base64")}`, - ) - - // close existing - const tabs = vscode.window.tabGroups.all - .flatMap((tg) => tg.tabs) - .filter((tab) => tab.label && tab.label.includes("README") && tab.label.includes("Preview")) - for (const tab of tabs) { - await vscode.window.tabGroups.close(tab) - } - - // Show only the preview - await vscode.commands.executeCommand("markdown.showPreview", uri, { - sideBySide: true, - preserveFocus: true, - }) - } - } - // OpenRouter async handleOpenRouterCallback(code: string) { diff --git a/src/services/mcp/McpMarketplaceService.ts b/src/services/mcp/McpMarketplaceService.ts new file mode 100644 index 0000000000..d21dc8fb39 --- /dev/null +++ b/src/services/mcp/McpMarketplaceService.ts @@ -0,0 +1,210 @@ +import axios from "axios" +import * as vscode from "vscode" +import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider" +import { McpMarketplaceCatalog, McpServer, McpDownloadResponse } from "../../shared/mcp" +import { McpHub } from "./McpHub" +import { ClineProvider } from "../../core/webview/ClineProvider" + +export class McpMarketplaceService { + constructor(private provider: ClineProvider) {} + + private async fetchMcpMarketplaceFromApi(silent: boolean = false): Promise { + try { + const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", { + headers: { + "Content-Type": "application/json", + }, + }) + + if (!response.data) { + throw new Error("Invalid response from MCP marketplace API") + } + + const catalog: McpMarketplaceCatalog = { + items: (response.data || []).map((item: any) => ({ + ...item, + githubStars: item.githubStars ?? 0, + downloadCount: item.downloadCount ?? 0, + tags: item.tags ?? [], + })), + } + + // Store in global state + await this.provider.updateGlobalState("mcpMarketplaceCatalog", catalog) + return catalog + } catch (error) { + console.error("Failed to fetch MCP marketplace:", error) + if (!silent) { + const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace" + await this.provider.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + error: errorMessage, + }) + vscode.window.showErrorMessage(errorMessage) + } + return undefined + } + } + + async prefetchMcpMarketplace() { + try { + await this.fetchMcpMarketplaceFromApi(true) + } catch (error) { + console.error("Failed to prefetch MCP marketplace:", error) + } + } + + async silentlyRefreshMcpMarketplace() { + try { + const catalog = await this.fetchMcpMarketplaceFromApi(true) + if (catalog) { + await this.provider.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + mcpMarketplaceCatalog: catalog, + }) + } + } catch (error) { + console.error("Failed to silently refresh MCP marketplace:", error) + } + } + + async fetchMcpMarketplace(forceRefresh: boolean = false) { + try { + // Check if we have cached data + const cachedCatalog = (await this.provider.getGlobalState("mcpMarketplaceCatalog")) as + | McpMarketplaceCatalog + | undefined + if (!forceRefresh && cachedCatalog?.items) { + await this.provider.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + mcpMarketplaceCatalog: cachedCatalog, + }) + return + } + + const catalog = await this.fetchMcpMarketplaceFromApi(false) + if (catalog) { + await this.provider.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + mcpMarketplaceCatalog: catalog, + }) + } + } catch (error) { + console.error("Failed to handle cached MCP marketplace:", error) + const errorMessage = error instanceof Error ? error.message : "Failed to handle cached MCP marketplace" + await this.provider.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + error: errorMessage, + }) + vscode.window.showErrorMessage(errorMessage) + } + } + + async downloadMcp(mcpHub: McpHub | undefined, mcpId: string) { + try { + // First check if we already have this MCP server installed + const servers = mcpHub?.getServers() || [] + const isInstalled = servers.some((server: McpServer) => server.name === mcpId) + + 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 }, + { + headers: { "Content-Type": "application/json" }, + timeout: 10000, + }, + ) + + if (!response.data) { + 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.provider.postMessageToWebview({ + type: "mcpDownloadDetails", + mcpDownloadDetails: mcpDetails, + }) + + // Create task with context from README + const task = `Set up the MCP server from ${mcpDetails.githubUrl}. Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json. Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}` + + // Initialize task and show chat view + await this.provider.initClineWithTask(task) + await this.provider.postMessageToWebview({ + type: "action", + action: "chatButtonClicked", + }) + } catch (error) { + console.error("Failed to download MCP:", error) + 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.provider.postMessageToWebview({ + type: "mcpDownloadDetails", + error: errorMessage, + }) + } + } + + async openMcpMarketplaceServerDetails(mcpId: string) { + const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${mcpId}`) + const details: McpDownloadResponse = await response.json() + + if (details.readmeContent) { + // Disable markdown preview markers + const config = vscode.workspace.getConfiguration("markdown") + await config.update("preview.markEditorSelection", false, true) + + // Create URI with base64 encoded markdown content + const uri = vscode.Uri.parse( + `${DIFF_VIEW_URI_SCHEME}:${details.name} README?${Buffer.from(details.readmeContent).toString("base64")}`, + ) + + // close existing + const tabs = vscode.window.tabGroups.all + .flatMap((tg) => tg.tabs) + .filter((tab) => tab.label && tab.label.includes("README") && tab.label.includes("Preview")) + for (const tab of tabs) { + await vscode.window.tabGroups.close(tab) + } + + // Show only the preview + await vscode.commands.executeCommand("markdown.showPreview", uri, { + sideBySide: true, + preserveFocus: true, + }) + } + } +}