From 7d67000d1f5b9eb4f012cca08c1a641945b864a8 Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Thu, 13 Feb 2025 17:23:22 -0800 Subject: [PATCH 01/30] marketplace wip --- src/core/webview/ClineProvider.ts | 113 ++++++++++++ src/shared/ExtensionMessage.ts | 7 +- src/shared/WebviewMessage.ts | 3 + src/shared/mcp.ts | 24 +++ webview-ui/src/components/mcp/McpView.tsx | 164 +++++++++++------- .../mcp/marketplace/McpMarketplaceCard.tsx | 141 +++++++++++++++ .../mcp/marketplace/McpMarketplaceView.tsx | 117 +++++++++++++ 7 files changed, 506 insertions(+), 63 deletions(-) create mode 100644 webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx create mode 100644 webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 3e59dfad2a..50a08bd532 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -14,6 +14,7 @@ import { selectImages } from "../../integrations/misc/process-images" import { getTheme } from "../../integrations/theme/getTheme" import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker" import { McpHub } from "../../services/mcp/McpHub" +import { McpMarketplaceCatalog, McpMarketplaceItem } from "../../shared/mcp" import { FirebaseAuthManager, UserInfo } from "../../services/auth/FirebaseAuthManager" import { ApiProvider, ModelInfo } from "../../shared/api" import { findLast } from "../../shared/array" @@ -88,6 +89,7 @@ type GlobalStateKey = | "qwenApiLine" | "requestyModelId" | "togetherModelId" + | "mcpMarketplaceCatalog" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -375,6 +377,107 @@ export class ClineProvider implements vscode.WebviewViewProvider { * * @param webview A reference to the extension webview */ + 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 + } + + 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, + downloads: item.downloads ?? 0, + tags: item.tags ?? [], + })), + } + + // Store in global state + await this.updateGlobalState("mcpMarketplaceCatalog", catalog) + + await this.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + mcpMarketplaceCatalog: catalog, + }) + } catch (error) { + console.error("Failed to fetch MCP marketplace:", error) + const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace" + await this.postMessageToWebview({ + type: "mcpMarketplaceCatalog", + error: errorMessage, + }) + vscode.window.showErrorMessage(errorMessage) + } + } 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 { + const response = await axios.post( + "https://api.cline.bot/v1/mcp/download", + { + mcpId, + }, + { + headers: { + "Content-Type": "application/json", + }, + }, + ) + + if (!response.data) { + throw new Error("Invalid response from MCP download API") + } + + const mcpDetails = response.data + await this.postMessageToWebview({ + type: "mcpDownloadDetails", + mcpDownloadDetails: mcpDetails, + }) + + // Create a new task for Cline to set up the MCP server + const task = `Set up the MCP server from ${mcpDetails.githubUrl}. Here's some additional context from the README:\n\n${mcpDetails.readmeContent}` + await this.initClineWithTask(task) + await this.postMessageToWebview({ + type: "action", + action: "chatButtonClicked", + }) + } catch (error) { + console.error("Failed to download MCP:", error) + const errorMessage = error instanceof Error ? error.message : "Failed to download MCP" + vscode.window.showErrorMessage(errorMessage) + await this.postMessageToWebview({ + type: "mcpDownloadDetails", + error: errorMessage, + }) + } + } + private setWebviewMessageListener(webview: vscode.Webview) { webview.onDidReceiveMessage( async (message: WebviewMessage) => { @@ -779,6 +882,16 @@ 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 "toggleMcpServer": { try { await this.mcpHub?.toggleServerDisabled(message.serverName!, message.disabled!) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 70a58fbfd8..ee2d1a4d9c 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -5,7 +5,7 @@ import { AutoApprovalSettings } from "./AutoApprovalSettings" import { BrowserSettings } from "./BrowserSettings" import { ChatSettings } from "./ChatSettings" import { HistoryItem } from "./HistoryItem" -import { McpServer } from "./mcp" +import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem } from "./mcp" // webview will hold state export interface ExtensionMessage { @@ -26,6 +26,8 @@ export interface ExtensionMessage { | "vsCodeLmModels" | "requestVsCodeLmModels" | "emailSubscribed" + | "mcpMarketplaceCatalog" + | "mcpDownloadDetails" text?: string action?: | "chatButtonClicked" @@ -46,6 +48,9 @@ export interface ExtensionMessage { openRouterModels?: Record openAiModels?: string[] mcpServers?: McpServer[] + mcpMarketplaceCatalog?: McpMarketplaceCatalog + error?: string + mcpDownloadDetails?: McpMarketplaceItem } export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 447193dd18..b691315d45 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -43,6 +43,8 @@ export interface WebviewMessage { | "accountLoginClicked" | "accountLogoutClicked" | "subscribeEmail" + | "fetchMcpMarketplace" + | "downloadMcp" // | "relaunchChromeDebugMode" text?: string disabled?: boolean @@ -55,6 +57,7 @@ export interface WebviewMessage { browserSettings?: BrowserSettings chatSettings?: ChatSettings chatContent?: ChatContent + mcpId?: string // For toggleToolAutoApprove serverName?: string diff --git a/src/shared/mcp.ts b/src/shared/mcp.ts index a8ae7f70f6..d859dc7b8f 100644 --- a/src/shared/mcp.ts +++ b/src/shared/mcp.ts @@ -66,3 +66,27 @@ export type McpToolCallResponse = { > isError?: boolean } + +export interface McpMarketplaceItem { + mcpId: string + githubUrl: string + name: string + author: string + description: string + codegenIcon: string + logoUrl: string + category: string + tags: string[] + requiresApiKey: boolean + readmeContent?: string + isRecommended: boolean + githubStars: number + downloads: number + createdAt: string + updatedAt: string + lastGithubSync: string +} + +export interface McpMarketplaceCatalog { + items: McpMarketplaceItem[] +} diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index b8afdbb05f..c2e64d24b7 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -1,10 +1,18 @@ -import { VSCodeButton, VSCodeLink, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react" +import { + VSCodeButton, + VSCodeLink, + VSCodePanels, + VSCodePanelTab, + VSCodePanelView, + VSCodeDivider, +} from "@vscode/webview-ui-toolkit/react" import { useState } from "react" import { vscode } from "../../utils/vscode" import { useExtensionState } from "../../context/ExtensionStateContext" import { McpServer } from "../../../../src/shared/mcp" import McpToolRow from "./McpToolRow" import McpResourceRow from "./McpResourceRow" +import McpMarketplaceView from "./marketplace/McpMarketplaceView" type McpViewProps = { onDone: () => void @@ -12,6 +20,7 @@ type McpViewProps = { const McpView = ({ onDone }: McpViewProps) => { const { mcpServers: servers } = useExtensionState() + const [activeTab, setActiveTab] = useState(servers.length === 0 ? "marketplace" : "installed") // const [servers, setServers] = useState([ // // Add some mock servers for testing @@ -96,72 +105,103 @@ const McpView = ({ onDone }: McpViewProps) => { Done -
-
- The{" "} - - Model Context Protocol - {" "} - enables communication with locally running MCP servers that provide additional tools and resources to extend - Cline's capabilities. You can use{" "} - - community-made servers - {" "} - or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest npm docs").{" "} - - See a demo here. - -
+
+ setActiveTab(e.target.activeid)}> + Installed + Marketplace + Settings - {servers.length > 0 && ( -
- {servers.map((server) => ( - - ))} -
- )} + +
+
+ The{" "} + + Model Context Protocol + {" "} + enables communication with locally running MCP servers that provide additional tools and resources + to extend Cline's capabilities. You can use{" "} + + community-made servers + {" "} + or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest + npm docs").{" "} + + See a demo here. + +
- {/* Server Configuration Button */} + {servers.length > 0 ? ( +
+ {servers.map((server) => ( + + ))} +
+ ) : ( +
+
No MCP servers installed yet
+ setActiveTab("marketplace")}> + + Browse Marketplace + +
+ )} +
+
-
- { - vscode.postMessage({ type: "openMcpSettings" }) - }}> - - Configure MCP Servers - -
+ + + - {/* Advanced Settings Link */} -
- { - vscode.postMessage({ - type: "openExtensionSettings", - text: "cline.mcp", - }) - }} - style={{ fontSize: "12px" }}> - Advanced MCP Settings - -
+ +
+ {/* Server Configuration Button */} +
+ { + vscode.postMessage({ type: "openMcpSettings" }) + }}> + + Configure MCP Servers + +
- {/* Bottom padding */} -
+ {/* Advanced Settings Link */} +
+ { + vscode.postMessage({ + type: "openExtensionSettings", + text: "cline.mcp", + }) + }} + style={{ fontSize: "12px" }}> + Advanced MCP Settings + +
+
+ +
) 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..2eebaa6da5 --- /dev/null +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -0,0 +1,141 @@ +import { useEffect, useState } from "react" +import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" +import { McpMarketplaceItem, McpServer } from "../../../../../src/shared/mcp" +import { vscode } from "../../../utils/vscode" + +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) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type === "mcpDownloadDetails") { + setIsDownloading(false) + } + } + + window.addEventListener("message", handleMessage) + return () => { + window.removeEventListener("message", handleMessage) + } + }, []) + + return ( +
+
+ {item.logoUrl && ( + {`${item.name} + )} +
+
+
+

{item.name}

+
by {item.author}
+
+ { + if (!isInstalled && !isDownloading) { + setIsDownloading(true) + vscode.postMessage({ + type: "downloadMcp", + mcpId: item.mcpId, + }) + } + }}> + + {isInstalled ? "Installed" : isDownloading ? "Downloading..." : "Download"} + +
+

{item.description}

+
+ vscode.postMessage({ type: "openFile", text: item.githubUrl })} + title="View on GitHub"> + + +
+ + {item.githubStars?.toLocaleString() ?? 0} +
+
+ + {item.downloads?.toLocaleString() ?? 0} +
+ {item.requiresApiKey && ( +
+ +
+ )} + {item.isRecommended && ( +
+ +
+ )} +
+
+ + {item.category} + + {item.tags.map((tag) => ( + + {tag} + + ))} +
+
+
+
+ ) +} + +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..0c9301a151 --- /dev/null +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -0,0 +1,117 @@ +import { useEffect, useState } from "react" +import { VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" +import { McpMarketplaceItem } from "../../../../../src/shared/mcp" +import { useExtensionState } from "../../../context/ExtensionStateContext" +import { vscode } from "../../../utils/vscode" +import McpMarketplaceCard from "./McpMarketplaceCard" + +const McpMarketplaceView = () => { + const { mcpServers } = useExtensionState() + const [items, setItems] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [isRefreshing, setIsRefreshing] = useState(false) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type === "mcpMarketplaceCatalog") { + if (message.error) { + setError(message.error) + } else { + setItems(message.mcpMarketplaceCatalog?.items || []) + setError(null) + } + setIsLoading(false) + setIsRefreshing(false) + } else if (message.type === "mcpDownloadDetails") { + if (message.error) { + setError(message.error) + } + } + } + + window.addEventListener("message", handleMessage) + + // Fetch marketplace catalog + fetchMarketplace() + + return () => { + window.removeEventListener("message", handleMessage) + } + }, []) + + const fetchMarketplace = (forceRefresh: boolean = false) => { + if (forceRefresh) { + setIsRefreshing(true) + } else { + setIsLoading(true) + } + setError(null) + vscode.postMessage({ type: "fetchMcpMarketplace", bool: forceRefresh }) + } + + if (isLoading || isRefreshing) { + return ( +
+ +
+ ) + } + + if (error) { + return ( +
+
{error}
+ fetchMarketplace(true)}> + + Retry + +
+ ) + } + + return ( +
+
+ fetchMarketplace(true)} disabled={isRefreshing}> + + Refresh + +
+ {items.length === 0 ? ( +
+ No MCP servers found in the marketplace +
+ ) : ( + items.map((item) => ) + )} +
+ ) +} + +export default McpMarketplaceView From e3cfb405fbfc7bcc6b278c08d49640e2e2ed84a7 Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Thu, 13 Feb 2025 18:22:33 -0800 Subject: [PATCH 02/30] added some logging + error handling --- src/core/webview/ClineProvider.ts | 69 +++++++-- src/shared/ExtensionMessage.ts | 4 +- src/shared/mcp.ts | 12 +- .../mcp/marketplace/McpMarketplaceCard.tsx | 14 +- .../mcp/marketplace/McpMarketplaceView.tsx | 132 +++++++++++++++++- 5 files changed, 209 insertions(+), 22 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 50a08bd532..0b6a2c2424 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -14,7 +14,7 @@ import { selectImages } from "../../integrations/misc/process-images" import { getTheme } from "../../integrations/theme/getTheme" import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker" import { McpHub } from "../../services/mcp/McpHub" -import { McpMarketplaceCatalog, McpMarketplaceItem } from "../../shared/mcp" +import { McpDownloadResponse, McpMarketplaceCatalog, McpMarketplaceItem, McpServer } from "../../shared/mcp" import { FirebaseAuthManager, UserInfo } from "../../services/auth/FirebaseAuthManager" import { ApiProvider, ModelInfo } from "../../shared/api" import { findLast } from "../../shared/array" @@ -404,7 +404,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { items: (response.data || []).map((item: any) => ({ ...item, githubStars: item.githubStars ?? 0, - downloads: item.downloads ?? 0, + downloadCount: item.downloadCount ?? 0, tags: item.tags ?? [], })), } @@ -438,30 +438,59 @@ export class ClineProvider implements vscode.WebviewViewProvider { private async downloadMcp(mcpId: string) { try { - const response = await axios.post( + // First check if we already have this MCP server installed + const servers = this.mcpHub?.getServers() || [] + const isInstalled = servers.some((server: McpServer) => { + try { + const config = JSON.parse(server.config) + const serverConfig = config.mcpServers[server.name] + const githubUrl = serverConfig.args?.find((arg: string) => arg.includes("github.com")) + return githubUrl?.includes(mcpId) + } catch { + return false + } + }) + + if (isInstalled) { + throw new Error("This MCP server is already installed") + } + + // Fetch server details from marketplace + const response = await axios.post( "https://api.cline.bot/v1/mcp/download", + { mcpId }, { - mcpId, - }, - { - headers: { - "Content-Type": "application/json", - }, + headers: { "Content-Type": "application/json" }, + timeout: 10000, }, ) if (!response.data) { - throw new Error("Invalid response from MCP download API") + throw new Error("Invalid response from MCP marketplace API") } + console.log("[downloadMcp] Response from download API", { response }) + const mcpDetails = response.data + + // Validate required fields + if (!mcpDetails.githubUrl) { + throw new Error("Missing GitHub URL in MCP download response") + } + if (!mcpDetails.readmeContent) { + throw new Error("Missing README content in MCP download response") + } + + // Send details to webview await this.postMessageToWebview({ type: "mcpDownloadDetails", mcpDownloadDetails: mcpDetails, }) - // Create a new task for Cline to set up the MCP server + // Create task with context from README const task = `Set up the MCP server from ${mcpDetails.githubUrl}. Here's some additional context from the README:\n\n${mcpDetails.readmeContent}` + + // Initialize task and show chat view await this.initClineWithTask(task) await this.postMessageToWebview({ type: "action", @@ -469,7 +498,23 @@ export class ClineProvider implements vscode.WebviewViewProvider { }) } catch (error) { console.error("Failed to download MCP:", error) - const errorMessage = error instanceof Error ? error.message : "Failed to download MCP" + let errorMessage = "Failed to download MCP" + + if (axios.isAxiosError(error)) { + if (error.code === "ECONNABORTED") { + errorMessage = "Request timed out. Please try again." + } else if (error.response?.status === 404) { + errorMessage = "MCP server not found in marketplace." + } else if (error.response?.status === 500) { + errorMessage = "Internal server error. Please try again later." + } else if (!error.response && error.request) { + errorMessage = "Network error. Please check your internet connection." + } + } else if (error instanceof Error) { + errorMessage = error.message + } + + // Show error in both notification and marketplace UI vscode.window.showErrorMessage(errorMessage) await this.postMessageToWebview({ type: "mcpDownloadDetails", diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index ee2d1a4d9c..8e00540534 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -5,7 +5,7 @@ import { AutoApprovalSettings } from "./AutoApprovalSettings" import { BrowserSettings } from "./BrowserSettings" import { ChatSettings } from "./ChatSettings" import { HistoryItem } from "./HistoryItem" -import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem } from "./mcp" +import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp" // webview will hold state export interface ExtensionMessage { @@ -50,7 +50,7 @@ export interface ExtensionMessage { mcpServers?: McpServer[] mcpMarketplaceCatalog?: McpMarketplaceCatalog error?: string - mcpDownloadDetails?: McpMarketplaceItem + mcpDownloadDetails?: McpDownloadResponse } export type Platform = "aix" | "darwin" | "freebsd" | "linux" | "openbsd" | "sunos" | "win32" | "unknown" diff --git a/src/shared/mcp.ts b/src/shared/mcp.ts index d859dc7b8f..28f4692c93 100644 --- a/src/shared/mcp.ts +++ b/src/shared/mcp.ts @@ -81,7 +81,7 @@ export interface McpMarketplaceItem { readmeContent?: string isRecommended: boolean githubStars: number - downloads: number + downloadCount: number createdAt: string updatedAt: string lastGithubSync: string @@ -90,3 +90,13 @@ export interface McpMarketplaceItem { export interface McpMarketplaceCatalog { items: McpMarketplaceItem[] } + +export interface McpDownloadResponse { + mcpId: string + githubUrl: string + name: string + author: string + description: string + readmeContent: string + requiresApiKey: boolean +} diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx index 2eebaa6da5..b5471bc1e7 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -9,7 +9,17 @@ interface McpMarketplaceCardProps { } const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) => { - const isInstalled = installedServers.some((server) => server.name === item.mcpId) + const isInstalled = installedServers.some((server) => { + try { + const config = JSON.parse(server.config) + const serverConfig = config.mcpServers[server.name] + // Extract GitHub URL from args if it's an npm package + const githubUrl = serverConfig.args?.find((arg: string) => arg.includes("github.com")) + return githubUrl?.includes(item.mcpId) || githubUrl?.includes(item.githubUrl) + } catch { + return false + } + }) const [isDownloading, setIsDownloading] = useState(false) useEffect(() => { @@ -94,7 +104,7 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps)
- {item.downloads?.toLocaleString() ?? 0} + {item.downloadCount?.toLocaleString() ?? 0}
{item.requiresApiKey && (
diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx index 0c9301a151..e346db3cea 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -1,16 +1,70 @@ -import { useEffect, useState } from "react" +import { useEffect, useMemo, useState } from "react" import { VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" import { McpMarketplaceItem } from "../../../../../src/shared/mcp" import { useExtensionState } from "../../../context/ExtensionStateContext" import { vscode } from "../../../utils/vscode" import McpMarketplaceCard from "./McpMarketplaceCard" +const searchInputStyles = { + width: "100%", + padding: "4px 8px 4px 28px", + background: "var(--vscode-input-background)", + border: "1px solid var(--vscode-input-border)", + color: "var(--vscode-input-foreground)", + borderRadius: "2px", + outline: "none", + transition: "border-color 0.1s ease-in-out, opacity 0.1s ease-in-out", +} + +const selectStyles = { + padding: "4px 8px", + background: "var(--vscode-dropdown-background)", + border: "1px solid var(--vscode-dropdown-border)", + color: "var(--vscode-dropdown-foreground)", + borderRadius: "2px", + outline: "none", + transition: "border-color 0.1s ease-in-out, opacity 0.1s ease-in-out", +} + const McpMarketplaceView = () => { const { mcpServers } = useExtensionState() const [items, setItems] = useState([]) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [isRefreshing, setIsRefreshing] = useState(false) + const [searchQuery, setSearchQuery] = useState("") + const [selectedCategory, setSelectedCategory] = useState(null) + const [sortBy, setSortBy] = useState<"downloadCount" | "stars" | "name">("downloadCount") + + const categories = useMemo(() => { + const uniqueCategories = new Set(items.map((item) => item.category)) + return Array.from(uniqueCategories).sort() + }, [items]) + + const filteredItems = useMemo(() => { + return items + .filter((item) => { + const matchesSearch = + searchQuery === "" || + item.name.toLowerCase().includes(searchQuery.toLowerCase()) || + item.description.toLowerCase().includes(searchQuery.toLowerCase()) || + item.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase())) + const matchesCategory = !selectedCategory || item.category === selectedCategory + return matchesSearch && matchesCategory + }) + .sort((a, b) => { + switch (sortBy) { + case "downloadCount": + return b.downloadCount - a.downloadCount + case "stars": + return b.githubStars - a.githubStars + case "name": + return a.name.localeCompare(b.name) + default: + return 0 + } + }) + }, [items, searchQuery, selectedCategory, sortBy]) useEffect(() => { const handleMessage = (event: MessageEvent) => { @@ -89,13 +143,79 @@ const McpMarketplaceView = () => { return (
-
+
+
+
+ setSearchQuery(e.target.value)} + className="mcp-search-input" + style={searchInputStyles} + /> + +
+ + +
fetchMarketplace(true)} disabled={isRefreshing}> Refresh
- {items.length === 0 ? ( + + {filteredItems.length === 0 ? (
{ padding: "20px", color: "var(--vscode-descriptionForeground)", }}> - No MCP servers found in the marketplace + {searchQuery || selectedCategory + ? "No matching MCP servers found" + : "No MCP servers found in the marketplace"}
) : ( - items.map((item) => ) + filteredItems.map((item) => ) )}
) From b8024497f2e556b0ee48409f28e5ce88ebaa7708 Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Thu, 13 Feb 2025 19:11:08 -0800 Subject: [PATCH 03/30] mcp marketplace working --- src/core/webview/ClineProvider.ts | 13 ++----------- .../mcp/marketplace/McpMarketplaceCard.tsx | 12 +----------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 0b6a2c2424..d4603e0322 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -440,16 +440,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { try { // First check if we already have this MCP server installed const servers = this.mcpHub?.getServers() || [] - const isInstalled = servers.some((server: McpServer) => { - try { - const config = JSON.parse(server.config) - const serverConfig = config.mcpServers[server.name] - const githubUrl = serverConfig.args?.find((arg: string) => arg.includes("github.com")) - return githubUrl?.includes(mcpId) - } catch { - return false - } - }) + const isInstalled = servers.some((server: McpServer) => server.name === mcpId) if (isInstalled) { throw new Error("This MCP server is already installed") @@ -488,7 +479,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { }) // Create task with context from README - const task = `Set up the MCP server from ${mcpDetails.githubUrl}. Here's some additional context from the README:\n\n${mcpDetails.readmeContent}` + const task = `Set up the MCP server from ${mcpDetails.githubUrl}. Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json. Here's some additional context from the README:\n\n${mcpDetails.readmeContent}` // Initialize task and show chat view await this.initClineWithTask(task) diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx index b5471bc1e7..bbe3ace015 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -9,17 +9,7 @@ interface McpMarketplaceCardProps { } const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) => { - const isInstalled = installedServers.some((server) => { - try { - const config = JSON.parse(server.config) - const serverConfig = config.mcpServers[server.name] - // Extract GitHub URL from args if it's an npm package - const githubUrl = serverConfig.args?.find((arg: string) => arg.includes("github.com")) - return githubUrl?.includes(item.mcpId) || githubUrl?.includes(item.githubUrl) - } catch { - return false - } - }) + const isInstalled = installedServers.some((server) => server.name === item.mcpId) const [isDownloading, setIsDownloading] = useState(false) useEffect(() => { From 267b60c09e55e2c4c84572f4fb63adc081ecd80d Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Fri, 14 Feb 2025 13:48:26 -0800 Subject: [PATCH 04/30] ui polish --- webview-ui/src/components/mcp/McpView.tsx | 8 ++- .../mcp/marketplace/McpMarketplaceView.tsx | 69 +++++++++++++------ 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index c2e64d24b7..9450140f2f 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -105,7 +105,7 @@ const McpView = ({ onDone }: McpViewProps) => { Done
-
+
setActiveTab(e.target.activeid)}> Installed Marketplace @@ -168,11 +168,13 @@ const McpView = ({ onDone }: McpViewProps) => { - +
+ +
-
+
{/* Server Configuration Button */}
{ const { mcpServers } = useExtensionState() @@ -142,16 +154,19 @@ const McpMarketplaceView = () => { } return ( -
+
-
+
+ {" "} + {/* Added minWidth: 0 to prevent flex item from overflowing */}
{ className="codicon codicon-search" style={{ position: "absolute", - left: "8px", + left: "10px", top: "50%", transform: "translateY(-50%)", color: "var(--vscode-input-placeholderForeground)", + pointerEvents: "none", + fontSize: "14px", // Match input text size + lineHeight: 1, // Ensure icon is centered properly }} />
@@ -194,26 +212,33 @@ const McpMarketplaceView = () => {
- fetchMarketplace(true)} disabled={isRefreshing}> + fetchMarketplace(true)} + disabled={isRefreshing} + style={refreshStyles}> Refresh
{filteredItems.length === 0 ? (
Date: Fri, 14 Feb 2025 14:25:58 -0800 Subject: [PATCH 05/30] ui polish --- .../mcp/marketplace/McpMarketplaceCard.tsx | 181 +++++++++--------- 1 file changed, 95 insertions(+), 86 deletions(-) diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx index bbe3ace015..da11b6f78b 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react" -import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" +import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react" import { McpMarketplaceItem, McpServer } from "../../../../../src/shared/mcp" import { vscode } from "../../../utils/vscode" @@ -36,101 +36,110 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) flexDirection: "column", gap: "12px", }}> -
- {item.logoUrl && ( - {`${item.name} - )} -
-
-
-

{item.name}

-
by {item.author}
-
- { - if (!isInstalled && !isDownloading) { - setIsDownloading(true) - vscode.postMessage({ - type: "downloadMcp", - mcpId: item.mcpId, - }) - } - }}> - - {isInstalled ? "Installed" : isDownloading ? "Downloading..." : "Download"} - -
-

{item.description}

-
- vscode.postMessage({ type: "openFile", text: item.githubUrl })} - title="View on GitHub"> - - -
- - {item.githubStars?.toLocaleString() ?? 0} -
-
- - {item.downloadCount?.toLocaleString() ?? 0} -
- {item.requiresApiKey && ( -
- -
- )} - {item.isRecommended && ( -
- -
- )} -
-
- +
+ {item.logoUrl && ( + {`${item.name} + )} +
+
+
+

{item.name}

+
+ by {item.author} +
+
+ { + if (!isInstalled && !isDownloading) { + setIsDownloading(true) + vscode.postMessage({ + type: "downloadMcp", + mcpId: item.mcpId, + }) + } + }}> + + {isInstalled ? "Installed" : isDownloading ? "Downloading..." : "Download"} + +
+

{item.description}

+
- {item.category} - - {item.tags.map((tag) => ( + + + +
+ + {item.githubStars?.toLocaleString() ?? 0} +
+
+ + {item.downloadCount?.toLocaleString() ?? 0} +
+ {item.requiresApiKey && ( +
+ +
+ )} + {item.isRecommended && ( +
+ +
+ )} +
+
- {tag} + {item.category} - ))} + {item.tags.map((tag) => ( + + {tag} + + ))} +
From 0cbd6b321f2175298421ff4beea2f96ede62a6a0 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:33:58 -0800 Subject: [PATCH 06/30] update ui to look like vs marketplace --- webview-ui/src/components/mcp/McpView.tsx | 100 ++++--- .../mcp/marketplace/McpMarketplaceCard.tsx | 261 ++++++++++++------ .../mcp/marketplace/McpMarketplaceView.tsx | 183 +++++++----- 3 files changed, 360 insertions(+), 184 deletions(-) diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 9450140f2f..c3f615efaa 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -13,6 +13,7 @@ import { McpServer } from "../../../../src/shared/mcp" import McpToolRow from "./McpToolRow" import McpResourceRow from "./McpResourceRow" import McpMarketplaceView from "./marketplace/McpMarketplaceView" +import styled from "styled-components" type McpViewProps = { onDone: () => void @@ -20,7 +21,7 @@ type McpViewProps = { const McpView = ({ onDone }: McpViewProps) => { const { mcpServers: servers } = useExtensionState() - const [activeTab, setActiveTab] = useState(servers.length === 0 ? "marketplace" : "installed") + const [activeTab, setActiveTab] = useState("marketplace") // const [servers, setServers] = useState([ // // Add some mock servers for testing @@ -99,20 +100,34 @@ const McpView = ({ onDone }: McpViewProps) => { display: "flex", justifyContent: "space-between", alignItems: "center", - padding: "10px 17px 10px 20px", + padding: "10px 17px 5px 20px", }}>

MCP Servers

Done
-
- setActiveTab(e.target.activeid)}> - Installed - Marketplace - Settings +
+ {/* Tabs container */} +
+ setActiveTab("marketplace")}> + Marketplace + + setActiveTab("installed")}> + Installed + +
- -
+ {/* Content container */} +
+ {activeTab === "marketplace" && } + {activeTab === "installed" && ( +
{
)} -
- - -
- -
-
- - -
- {/* Server Configuration Button */} -
+ {/* Settings Section */} +
{ vscode.postMessage({ type: "openMcpSettings" }) }}> Configure MCP Servers -
- {/* Advanced Settings Link */} -
- { - vscode.postMessage({ - type: "openExtensionSettings", - text: "cline.mcp", - }) - }} - style={{ fontSize: "12px" }}> - Advanced MCP Settings - +
+ { + vscode.postMessage({ + type: "openExtensionSettings", + text: "cline.mcp", + }) + }} + style={{ fontSize: "12px" }}> + Advanced MCP Settings + +
- - + )} +
) } +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} + +) + // Server Row Component const ServerRow = ({ server }: { server: McpServer }) => { 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 da11b6f78b..2e6ec6a708 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -1,5 +1,6 @@ -import { useEffect, useState } from "react" -import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { CSSProperties, useEffect, useState } from "react" +import styled from "styled-components" import { McpMarketplaceItem, McpServer } from "../../../../../src/shared/mcp" import { vscode } from "../../../utils/vscode" @@ -27,40 +28,62 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) }, []) return ( -
-
-
+ <> + +
{ + console.log("Card clicked:", item.mcpId) + }} + style={{ + borderBottom: "1px solid var(--vscode-textCodeBlock-background)", + padding: "12px 16px", + }}> + {/* Main container with logo and content */} +
+ {/* Logo */} {item.logoUrl && ( {`${item.name} )} -
-
-
-

{item.name}

-
- by {item.author} -
-
- { + + {/* Content section */} +
+ {/* First row: name and install button */} +
+

+ {item.name} +

+
{ + e.stopPropagation() // Prevent card click when clicking install if (!isInstalled && !isDownloading) { setIsDownloading(true) vscode.postMessage({ @@ -68,22 +91,25 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) mcpId: item.mcpId, }) } - }}> - - {isInstalled ? "Installed" : isDownloading ? "Downloading..." : "Download"} - + }} + style={{}}> + + {isInstalled ? "Installed" : isDownloading ? "Installing..." : "Install"} + +
-

{item.description}

+ + {/* Second row: metadata */}
- + -
- - {item.githubStars?.toLocaleString() ?? 0} -
-
- - {item.downloadCount?.toLocaleString() ?? 0} -
- {item.requiresApiKey && ( -
- -
- )} - {item.isRecommended && ( -
- -
- )} -
-
- {item.category} + {item.author} - {item.tags.map((tag) => ( - - {tag} - - ))} + | +
+ + {item.githubStars?.toLocaleString() ?? 0} +
+ | +
+ + {item.downloadCount?.toLocaleString() ?? 0} +
+ {item.requiresApiKey && ( + + )} + {item.isRecommended && ( + + )}
+ + {/* Description and tags */} +
+

{item.description}

+
+ + {item.category} + + {item.tags.map((tag, index) => ( + + {tag} + {index === item.tags.length - 1 ? "" : ""} + + ))} +
+
+
-
+ ) } +const StyledInstallButton = styled.button<{ $isInstalled?: boolean }>` + font-size: 12px; + font-weight: 500; + padding: 2px 6px; + border-radius: 2px; + border: none; + cursor: 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 index 966dd5bd47..2b34c90194 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -1,5 +1,13 @@ import { useEffect, useMemo, useState } from "react" -import { VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" +import { + VSCodeButton, + VSCodeProgressRing, + VSCodeRadioGroup, + VSCodeRadio, + VSCodeDropdown, + VSCodeOption, + VSCodeTextField, +} from "@vscode/webview-ui-toolkit/react" import { McpMarketplaceItem } from "../../../../../src/shared/mcp" import { useExtensionState } from "../../../context/ExtensionStateContext" import { vscode } from "../../../utils/vscode" @@ -33,11 +41,6 @@ const selectStyles = { cursor: "pointer", // Show pointer cursor on hover } as const -const refreshStyles = { - height: controlHeight, - alignSelf: "flex-end", -} as const - const McpMarketplaceView = () => { const { mcpServers } = useExtensionState() const [items, setItems] = useState([]) @@ -46,7 +49,7 @@ const McpMarketplaceView = () => { const [isRefreshing, setIsRefreshing] = useState(false) const [searchQuery, setSearchQuery] = useState("") const [selectedCategory, setSelectedCategory] = useState(null) - const [sortBy, setSortBy] = useState<"downloadCount" | "stars" | "name">("downloadCount") + const [sortBy, setSortBy] = useState<"downloadCount" | "stars" | "name" | "newest">("downloadCount") const categories = useMemo(() => { const uniqueCategories = new Set(items.map((item) => item.category)) @@ -72,6 +75,8 @@ const McpMarketplaceView = () => { return b.githubStars - a.githubStars case "name": return a.name.localeCompare(b.name) + case "newest": + return b.githubStars - a.githubStars // FIXME: b.createdAt - a.createdAt // Assuming there's a createdAt field default: return 0 } @@ -154,73 +159,115 @@ const McpMarketplaceView = () => { } return ( -
-
-
- {" "} - {/* Added minWidth: 0 to prevent flex item from overflowing */} -
- setSearchQuery(e.target.value)} - className="mcp-search-input" - style={searchInputStyles} - /> - +
+ {/* Search row */} + setSearchQuery((e.target as HTMLInputElement).value)}> +
+ {searchQuery && ( +
setSearchQuery("")} + slot="end" style={{ - position: "absolute", - left: "10px", - top: "50%", - transform: "translateY(-50%)", - color: "var(--vscode-input-placeholderForeground)", - pointerEvents: "none", - fontSize: "14px", // Match input text size - lineHeight: 1, // Ensure icon is centered properly + display: "flex", + justifyContent: "center", + alignItems: "center", + height: "100%", + cursor: "pointer", }} /> + )} + + + {/* Filter row */} +
+ + Filter: + +
+ setSelectedCategory((e.target as HTMLSelectElement).value || null)}> + All Categories + {categories.map((category) => ( + + {category} + + ))} +
- -
- fetchMarketplace(true)} - disabled={isRefreshing} - style={refreshStyles}> - - Refresh - + + {/* Sort row */} +
+ + Sort: + + setSortBy((e.target as HTMLInputElement).value as typeof sortBy)}> + Most Installs + Most Stars + Newest + Name + +
+
{ + onClick={(e) => { + if (githubLinkRef.current?.contains(e.target as Node)) { + return + } + console.log("Card clicked:", item.mcpId) setIsLoading(true) vscode.postMessage({ @@ -121,18 +124,27 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) flexWrap: "wrap", minWidth: 0, }}> - - - +
+ { + e.currentTarget.style.opacity = "0.8" + }} + onMouseLeave={(e) => { + e.currentTarget.style.opacity = "0.5" + }}> + + +
Date: Sat, 15 Feb 2025 16:49:06 -0800 Subject: [PATCH 11/30] including llmsInstall + removed borders between items --- src/core/webview/ClineProvider.ts | 2 +- src/shared/mcp.ts | 2 ++ .../src/components/mcp/marketplace/McpMarketplaceCard.tsx | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 74d30edbfe..16f827d0ac 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -480,7 +480,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { }) // 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's some additional context from the README:\n\n${mcpDetails.readmeContent}` + const task = `Set up the MCP server from ${mcpDetails.githubUrl}. Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json. Here's some additional context from the README:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}` // Initialize task and show chat view await this.initClineWithTask(task) diff --git a/src/shared/mcp.ts b/src/shared/mcp.ts index 28f4692c93..fec640d1ae 100644 --- a/src/shared/mcp.ts +++ b/src/shared/mcp.ts @@ -79,6 +79,7 @@ export interface McpMarketplaceItem { tags: string[] requiresApiKey: boolean readmeContent?: string + llmsInstallationContent?: string isRecommended: boolean githubStars: number downloadCount: number @@ -98,5 +99,6 @@ export interface McpDownloadResponse { author: string description: string readmeContent: string + llmsInstallationContent: string requiresApiKey: boolean } diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx index bfafb722b3..32c566f8a4 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -57,7 +57,6 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) }) }} style={{ - borderBottom: "1px solid var(--vscode-textCodeBlock-background)", padding: "12px 16px", cursor: isLoading ? "wait" : "pointer", }}> From 338116fbd42fa12aad7c61fbdc5224b4916ecbb3 Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Sat, 15 Feb 2025 17:23:08 -0800 Subject: [PATCH 12/30] consistent spacing with flexbox gap --- webview-ui/src/components/mcp/McpView.tsx | 2 +- .../mcp/marketplace/McpMarketplaceCard.tsx | 84 +++++++++++-------- .../mcp/marketplace/McpMarketplaceView.tsx | 5 +- 3 files changed, 50 insertions(+), 41 deletions(-) diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index c3f615efaa..f05f4d0546 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -112,7 +112,7 @@ const McpView = ({ onDone }: McpViewProps) => { style={{ display: "flex", gap: "1px", - padding: "0 8px 0 10px", + padding: "0 20px 0 20px", borderBottom: "1px solid var(--vscode-panel-border)", }}> setActiveTab("marketplace")}> diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx index 32c566f8a4..dae8fa70ff 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -57,33 +57,43 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) }) }} style={{ - padding: "12px 16px", + padding: "16px 20px", + display: "flex", + flexDirection: "column", + gap: 16, cursor: isLoading ? "wait" : "pointer", }}> {/* Main container with logo and content */} -
+
{/* Logo */} {item.logoUrl && ( {`${item.name} )} {/* Content section */} -
+
{/* First row: name and install button */}

-
- +
+ { + e.currentTarget.style.opacity = "0.8" + }} + onMouseLeave={(e) => { + e.currentTarget.style.opacity = "0.5" + }}> + + +
+ { - e.currentTarget.style.opacity = "0.8" - }} - onMouseLeave={(e) => { - e.currentTarget.style.opacity = "0.5" }}> - -
+ {item.author} +
- - {item.author} - |
{/* Description and tags */} -
-

{item.description}

+
+

{item.description}

{ flexDirection: "column", width: "100%", }}> -
+
{/* Search row */} { className="codicon codicon-search" style={{ fontSize: 13, - marginTop: 2.5, opacity: 0.8, }} /> @@ -204,7 +203,6 @@ const McpMarketplaceView = () => { display: "flex", alignItems: "center", gap: "8px", - marginTop: "8px", }}> { style={{ display: "flex", gap: "8px", - marginTop: "8px", }}> Date: Sat, 15 Feb 2025 17:37:02 -0800 Subject: [PATCH 13/30] more cleanup --- .../mcp/marketplace/McpMarketplaceCard.tsx | 57 +++++++++---------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx index dae8fa70ff..787e9fbe4b 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -129,43 +129,39 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) gap: "12px", fontSize: "12px", color: "var(--vscode-descriptionForeground)", - marginTop: "2px", flexWrap: "wrap", minWidth: 0, }}> -
-
- { + e.currentTarget.style.opacity = "0.8" + }} + onMouseLeave={(e) => { + e.currentTarget.style.opacity = "0.5" + }}> +
+ + { - e.currentTarget.style.opacity = "0.8" - }} - onMouseLeave={(e) => { - e.currentTarget.style.opacity = "0.5" }}> - - + {item.author} +
- - {item.author} - -
- | +
{item.githubStars?.toLocaleString() ?? 0}
- |
Date: Sat, 15 Feb 2025 18:20:10 -0800 Subject: [PATCH 14/30] cleanup --- .../components/mcp/marketplace/McpMarketplaceCard.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx index 787e9fbe4b..12555244f2 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -40,6 +40,15 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) .mcp-card:hover { background-color: var(--vscode-list-hoverBackground); } + vscode-link::part(control) { + text-decoration: none !important; + border: none !important; + } + vscode-link:hover::part(control) { + color: var(--link-active-foreground); + text-decoration: none !important; + border: none !important; + } `}
{ e.currentTarget.style.opacity = "0.8" }} From a4d1ae1af44cc7c01fa1f72c36e407998b8f4f3b Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Sat, 15 Feb 2025 19:00:08 -0800 Subject: [PATCH 15/30] codegenicon renamed to codiconIcon --- src/shared/mcp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/mcp.ts b/src/shared/mcp.ts index fec640d1ae..f0a09afa2d 100644 --- a/src/shared/mcp.ts +++ b/src/shared/mcp.ts @@ -73,7 +73,7 @@ export interface McpMarketplaceItem { name: string author: string description: string - codegenIcon: string + codiconIcon: string logoUrl: string category: string tags: string[] From 31eb212beef4d8c3542a2de4e9227ceca3272e7e Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Mon, 17 Feb 2025 15:03:10 -0800 Subject: [PATCH 16/30] faster detail view --- src/core/webview/ClineProvider.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 16f827d0ac..fe38269249 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -931,14 +931,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { } case "openMcpMarketplaceServerDetails": { if (message.mcpId) { - // 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) - } - const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${message.mcpId}`) const details: McpDownloadResponse = await response.json() @@ -952,6 +944,14 @@ export class ClineProvider implements vscode.WebviewViewProvider { `${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, From 09c4202cb216a4945f5ff5b5376378f87e14435e Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Mon, 17 Feb 2025 16:14:36 -0800 Subject: [PATCH 17/30] added submit card --- .../mcp/marketplace/McpMarketplaceView.tsx | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx index 28e1e440a6..d07cdb4e1a 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -12,6 +12,7 @@ import { McpMarketplaceItem } from "../../../../../src/shared/mcp" import { useExtensionState } from "../../../context/ExtensionStateContext" import { vscode } from "../../../utils/vscode" import McpMarketplaceCard from "./McpMarketplaceCard" +import McpSubmitCard from "./McpSubmitCard" const controlHeight = "28px" @@ -284,23 +285,26 @@ const McpMarketplaceView = () => { } `} - {filteredItems.length === 0 ? ( -
- {searchQuery || selectedCategory - ? "No matching MCP servers found" - : "No MCP servers found in the marketplace"} -
- ) : ( - filteredItems.map((item) => ) - )} +
+ {filteredItems.length === 0 ? ( +
+ {searchQuery || selectedCategory + ? "No matching MCP servers found" + : "No MCP servers found in the marketplace"} +
+ ) : ( + filteredItems.map((item) => ) + )} + +
) } From 7722234761c93cabfe2c85ee90313471d929a596 Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Mon, 17 Feb 2025 16:21:22 -0800 Subject: [PATCH 18/30] submit card --- .../mcp/marketplace/McpSubmitCard.tsx | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx 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..3373a96783 --- /dev/null +++ b/webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx @@ -0,0 +1,79 @@ +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import styled from "styled-components" + +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. +

+
+
+ ) +} + +const StyledGitHubButton = styled.div` + font-size: 13px; + font-weight: 500; + padding: 8px; + border-radius: 2px; + border: 1px solid var(--vscode-button-border, transparent); + cursor: pointer; + background: var(--vscode-button-background); + color: var(--vscode-button-foreground); + display: flex; + align-items: center; + justify-content: center; + width: 100%; + + &:hover { + background: var(--vscode-button-hoverBackground); + } + + &:active { + background: var(--vscode-button-background); + opacity: 0.7; + } +` + +export default McpSubmitCard From 2d05dfd986a1231a86ece4a27906c47baac062ae Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Mon, 17 Feb 2025 16:34:02 -0800 Subject: [PATCH 19/30] silent prefetching --- src/core/webview/ClineProvider.ts | 125 ++++++++++++++-------- src/shared/WebviewMessage.ts | 1 + webview-ui/src/components/mcp/McpView.tsx | 11 +- 3 files changed, 92 insertions(+), 45 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index fe38269249..7a87370a44 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -378,6 +378,66 @@ export class ClineProvider implements vscode.WebviewViewProvider { * * @param webview A reference to the extension webview */ + 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 @@ -390,41 +450,12 @@ export class ClineProvider implements vscode.WebviewViewProvider { return } - 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) - + const catalog = await this.fetchMcpMarketplaceFromApi(false) + if (catalog) { await this.postMessageToWebview({ type: "mcpMarketplaceCatalog", mcpMarketplaceCatalog: catalog, }) - } catch (error) { - console.error("Failed to fetch MCP marketplace:", error) - const errorMessage = error instanceof Error ? error.message : "Failed to fetch MCP marketplace" - await this.postMessageToWebview({ - type: "mcpMarketplaceCatalog", - error: errorMessage, - }) - vscode.window.showErrorMessage(errorMessage) } } catch (error) { console.error("Failed to handle cached MCP marketplace:", error) @@ -540,19 +571,23 @@ export class ClineProvider implements vscode.WebviewViewProvider { // gui relies on model info to be up-to-date to provide the most accurate pricing, so we need to fetch the latest details on launch. // we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point // (see normalizeApiConfiguration > openrouter) - this.refreshOpenRouterModels().then(async (openRouterModels) => { - if (openRouterModels) { - // update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there) - const { apiConfiguration } = await this.getState() - if (apiConfiguration.openRouterModelId) { - await this.updateGlobalState( - "openRouterModelInfo", - openRouterModels[apiConfiguration.openRouterModelId], - ) - await this.postStateToWebview() + // Prefetch marketplace and OpenRouter models + Promise.all([ + this.prefetchMcpMarketplace(), + this.refreshOpenRouterModels().then(async (openRouterModels) => { + if (openRouterModels) { + // update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there) + const { apiConfiguration } = await this.getState() + if (apiConfiguration.openRouterModelId) { + await this.updateGlobalState( + "openRouterModelInfo", + openRouterModels[apiConfiguration.openRouterModelId], + ) + await this.postStateToWebview() + } } - } - }) + }), + ]).catch(console.error) break case "newTask": // Code that should run in response to the hello message command @@ -929,6 +964,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { } break } + case "silentlyRefreshMcpMarketplace": { + await this.silentlyRefreshMcpMarketplace() + break + } case "openMcpMarketplaceServerDetails": { if (message.mcpId) { const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${message.mcpId}`) diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 5390ff0bdb..2d82474382 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -46,6 +46,7 @@ export interface WebviewMessage { | "fetchMcpMarketplace" | "downloadMcp" | "openMcpMarketplaceServerDetails" + | "silentlyRefreshMcpMarketplace" // | "relaunchChromeDebugMode" text?: string disabled?: boolean diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index f05f4d0546..3c0eef4811 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -23,6 +23,13 @@ const McpView = ({ onDone }: McpViewProps) => { const { mcpServers: servers } = useExtensionState() const [activeTab, setActiveTab] = useState("marketplace") + const handleTabChange = (tab: string) => { + setActiveTab(tab) + if (tab === "marketplace") { + vscode.postMessage({ type: "silentlyRefreshMcpMarketplace" }) + } + } + // const [servers, setServers] = useState([ // // Add some mock servers for testing // { @@ -115,10 +122,10 @@ const McpView = ({ onDone }: McpViewProps) => { padding: "0 20px 0 20px", borderBottom: "1px solid var(--vscode-panel-border)", }}> - setActiveTab("marketplace")}> + handleTabChange("marketplace")}> Marketplace - setActiveTab("installed")}> + handleTabChange("installed")}> Installed
From 31443f2a86f17e5cb281f179e26dbc3b09fceb72 Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Mon, 17 Feb 2025 16:36:18 -0800 Subject: [PATCH 20/30] removed top border from submit card --- webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx | 1 + webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx index d07cdb4e1a..711530eb58 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -295,6 +295,7 @@ const McpMarketplaceView = () => { height: "100%", padding: "20px", color: "var(--vscode-descriptionForeground)", + borderBottom: "1px solid var(--vscode-list-inactiveSelectionBackground)", }}> {searchQuery || selectedCategory ? "No matching MCP servers found" diff --git a/webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx b/webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx index 3373a96783..717fdf4688 100644 --- a/webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx @@ -10,7 +10,6 @@ const McpSubmitCard = () => { alignItems: "center", gap: "24px", padding: "32px 20px", - borderTop: "1px solid var(--vscode-list-inactiveSelectionBackground)", marginTop: "16px", }}> {/* Logo */} From 15b269c8f2e37684045fc737e203ace621394323 Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Mon, 17 Feb 2025 16:39:40 -0800 Subject: [PATCH 21/30] not using vscodelink just a tag --- .../mcp/marketplace/McpMarketplaceCard.tsx | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx index 12555244f2..b35cef80b4 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -40,15 +40,6 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) .mcp-card:hover { background-color: var(--vscode-list-hoverBackground); } - vscode-link::part(control) { - text-decoration: none !important; - border: none !important; - } - vscode-link:hover::part(control) { - color: var(--link-active-foreground); - text-decoration: none !important; - border: none !important; - } `}
- { e.currentTarget.style.opacity = "0.8" + e.currentTarget.style.color = "var(--link-active-foreground)" }} onMouseLeave={(e) => { e.currentTarget.style.opacity = "0.5" + e.currentTarget.style.color = "var(--vscode-foreground)" }}>
@@ -172,7 +164,7 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) {item.author}
-
+
Date: Mon, 17 Feb 2025 17:27:31 -0800 Subject: [PATCH 22/30] cleaned up unused vars --- webview-ui/src/components/mcp/McpView.tsx | 9 +----- .../mcp/marketplace/McpMarketplaceCard.tsx | 3 +- .../mcp/marketplace/McpMarketplaceView.tsx | 29 ------------------- .../mcp/marketplace/McpSubmitCard.tsx | 27 ----------------- 4 files changed, 2 insertions(+), 66 deletions(-) diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 3c0eef4811..4e7d2b0da9 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -1,11 +1,4 @@ -import { - VSCodeButton, - VSCodeLink, - VSCodePanels, - VSCodePanelTab, - VSCodePanelView, - VSCodeDivider, -} from "@vscode/webview-ui-toolkit/react" +import { VSCodeButton, VSCodeLink, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react" import { useState } from "react" import { vscode } from "../../utils/vscode" import { useExtensionState } from "../../context/ExtensionStateContext" diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx index b35cef80b4..18840f2b70 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -1,5 +1,4 @@ -import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import { CSSProperties, useCallback, useEffect, useState, useRef } from "react" +import { useCallback, useState, useRef } from "react" import styled from "styled-components" import { McpMarketplaceItem, McpServer } from "../../../../../src/shared/mcp" import { vscode } from "../../../utils/vscode" diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx index 711530eb58..29a2502135 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -13,35 +13,6 @@ import { useExtensionState } from "../../../context/ExtensionStateContext" import { vscode } from "../../../utils/vscode" import McpMarketplaceCard from "./McpMarketplaceCard" import McpSubmitCard from "./McpSubmitCard" - -const controlHeight = "28px" - -const searchInputStyles = { - width: "100%", - height: controlHeight, - padding: "0 8px 0 32px", // Removed vertical padding since we're using fixed height - background: "var(--vscode-input-background)", - border: "1px solid var(--vscode-input-border)", - color: "var(--vscode-input-foreground)", - borderRadius: "2px", - outline: "none", - transition: "border-color 0.1s ease-in-out, opacity 0.1s ease-in-out", - cursor: "text", // Show text cursor for input -} as const - -const selectStyles = { - height: controlHeight, - padding: "0 12px", // Removed vertical padding since we're using fixed height - background: "var(--vscode-dropdown-background)", - border: "1px solid var(--vscode-dropdown-border)", - color: "var(--vscode-dropdown-foreground)", - borderRadius: "2px", - outline: "none", - transition: "border-color 0.1s ease-in-out, opacity 0.1s ease-in-out", - minWidth: "140px", // Ensure consistent width for dropdowns - cursor: "pointer", // Show pointer cursor on hover -} as const - const McpMarketplaceView = () => { const { mcpServers } = useExtensionState() const [items, setItems] = useState([]) diff --git a/webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx b/webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx index 717fdf4688..b00eedc5de 100644 --- a/webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx @@ -1,6 +1,3 @@ -import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import styled from "styled-components" - const McpSubmitCard = () => { return (
{ ) } -const StyledGitHubButton = styled.div` - font-size: 13px; - font-weight: 500; - padding: 8px; - border-radius: 2px; - border: 1px solid var(--vscode-button-border, transparent); - cursor: pointer; - background: var(--vscode-button-background); - color: var(--vscode-button-foreground); - display: flex; - align-items: center; - justify-content: center; - width: 100%; - - &:hover { - background: var(--vscode-button-hoverBackground); - } - - &:active { - background: var(--vscode-button-background); - opacity: 0.7; - } -` - export default McpSubmitCard From 5f57415785607da768f4c51111269801bc9da530 Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Mon, 17 Feb 2025 19:08:29 -0800 Subject: [PATCH 23/30] removed promise all --- src/core/webview/ClineProvider.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 525f41792e..461b62e1a2 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -573,22 +573,22 @@ export class ClineProvider implements vscode.WebviewViewProvider { // we do this for all users since many users switch between api providers and if they were to switch back to openrouter it would be showing outdated model info if we hadn't retrieved the latest at this point // (see normalizeApiConfiguration > openrouter) // Prefetch marketplace and OpenRouter models - Promise.all([ - this.prefetchMcpMarketplace(), - this.refreshOpenRouterModels().then(async (openRouterModels) => { - if (openRouterModels) { - // update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there) - const { apiConfiguration } = await this.getState() - if (apiConfiguration.openRouterModelId) { - await this.updateGlobalState( - "openRouterModelInfo", - openRouterModels[apiConfiguration.openRouterModelId], - ) - await this.postStateToWebview() - } + + this.prefetchMcpMarketplace() + this.refreshOpenRouterModels().then(async (openRouterModels) => { + if (openRouterModels) { + // update model info in state (this needs to be done here since we don't want to update state while settings is open, and we may refresh models there) + const { apiConfiguration } = await this.getState() + if (apiConfiguration.openRouterModelId) { + await this.updateGlobalState( + "openRouterModelInfo", + openRouterModels[apiConfiguration.openRouterModelId], + ) + await this.postStateToWebview() } - }), - ]).catch(console.error) + } + }) + break case "newTask": // Code that should run in response to the hello message command From 8927879d939c8ed3c6410f2e7993ebfc5f53f7e1 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:31:13 -0800 Subject: [PATCH 24/30] Refactor --- src/core/webview/ClineProvider.ts | 338 +++++++++++++++--------------- 1 file changed, 170 insertions(+), 168 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 461b62e1a2..6fc4d051ce 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -379,174 +379,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { * * @param webview A reference to the extension webview */ - 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's some additional context from the README:\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 setWebviewMessageListener(webview: vscode.Webview) { webview.onDidReceiveMessage( async (message: WebviewMessage) => { @@ -1242,6 +1074,176 @@ export class ClineProvider implements vscode.WebviewViewProvider { } } + // 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's some additional context from the README:\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, + }) + } + } + // OpenAi async getOpenAiModels(baseUrl?: string, apiKey?: string) { From 24d3bfbccb95ff3779928e8f8c15f6d9986fd22a Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:50:35 -0800 Subject: [PATCH 25/30] Adjust styling --- .../components/mcp/marketplace/McpMarketplaceCard.tsx | 9 +++++---- .../components/mcp/marketplace/McpMarketplaceView.tsx | 7 +++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx index 18840f2b70..45383e68d2 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -56,14 +56,14 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps) }) }} style={{ - padding: "16px 20px", + padding: "14px 16px", display: "flex", flexDirection: "column", - gap: 16, + gap: 12, cursor: isLoading ? "wait" : "pointer", }}> {/* Main container with logo and content */} -
+
{/* Logo */} {item.logoUrl && ( {/* Description and tags */} -
+

{item.description}

{ color: "var(--vscode-descriptionForeground)", textTransform: "uppercase", fontWeight: 500, + flexShrink: 0, }}> Filter:
setSelectedCategory((e.target as HTMLSelectElement).value || null)}> All Categories From 691cf91a4ddf2a3baa6a00efece4f33ee4d609e0 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Mon, 17 Feb 2025 20:05:16 -0800 Subject: [PATCH 26/30] Fixes --- src/core/webview/ClineProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6fc4d051ce..94204867d4 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1209,7 +1209,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { }) // 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's some additional context from the README:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}` + 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) From b7519a3669da9c3444d7aee8e64f8375dc85c9ea Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Mon, 17 Feb 2025 20:13:57 -0800 Subject: [PATCH 27/30] Create stupid-mayflies-melt.md --- .changeset/stupid-mayflies-melt.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/stupid-mayflies-melt.md diff --git a/.changeset/stupid-mayflies-melt.md b/.changeset/stupid-mayflies-melt.md new file mode 100644 index 0000000000..cd2a31cbc8 --- /dev/null +++ b/.changeset/stupid-mayflies-melt.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +Add MCP Marketplace From 9c840fc89c876d3937ee4374fb85995ba71160ff Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Mon, 17 Feb 2025 20:27:11 -0800 Subject: [PATCH 28/30] Update webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- .../src/components/mcp/marketplace/McpMarketplaceView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx index 2271e1d17d..2204709415 100644 --- a/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -48,7 +48,7 @@ const McpMarketplaceView = () => { case "name": return a.name.localeCompare(b.name) case "newest": - return b.githubStars - a.githubStars // FIXME: b.createdAt - a.createdAt // Assuming there's a createdAt field + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() default: return 0 } From e587115d62ba27d84b8b8b89a2fe7d71601c0045 Mon Sep 17 00:00:00 2001 From: pashpashpash Date: Mon, 17 Feb 2025 20:33:25 -0800 Subject: [PATCH 29/30] added silent fetching to when mcp view is clicked --- webview-ui/src/components/mcp/McpView.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 4e7d2b0da9..e1da3dff71 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -1,5 +1,5 @@ import { VSCodeButton, VSCodeLink, VSCodePanels, VSCodePanelTab, VSCodePanelView } from "@vscode/webview-ui-toolkit/react" -import { useState } from "react" +import { useState, useEffect } from "react" import { vscode } from "../../utils/vscode" import { useExtensionState } from "../../context/ExtensionStateContext" import { McpServer } from "../../../../src/shared/mcp" @@ -18,11 +18,12 @@ const McpView = ({ onDone }: McpViewProps) => { const handleTabChange = (tab: string) => { setActiveTab(tab) - if (tab === "marketplace") { - vscode.postMessage({ type: "silentlyRefreshMcpMarketplace" }) - } } + useEffect(() => { + vscode.postMessage({ type: "silentlyRefreshMcpMarketplace" }) + }, []) + // const [servers, setServers] = useState([ // // Add some mock servers for testing // { From 95acb1e95c16b04febd94d4ffaf06b0ff504880f Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Mon, 17 Feb 2025 20:34:29 -0800 Subject: [PATCH 30/30] Fix overflowing mcp server name --- webview-ui/src/components/mcp/McpView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index e1da3dff71..511ac2de3c 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -280,7 +280,7 @@ const ServerRow = ({ server }: { server: McpServer }) => { {!server.error && ( )} - {server.name} + {server.name}
e.stopPropagation()}>