diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 168179f4df..10607c90b4 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -67,6 +67,7 @@ import { getUri } from "./getUri" import { telemetryService } from "../../services/telemetry/TelemetryService" import { TelemetrySetting } from "../../shared/TelemetrySetting" import { getWorkspacePath } from "../../utils/path" +import { McpMarketplaceService } from "../../services/mcp/McpMarketplaceService" /** * https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -91,6 +92,7 @@ export class ClineProvider extends EventEmitter implements private contextProxy: ContextProxy configManager: ConfigManager customModesManager: CustomModesManager + private mcpMarketplaceService: McpMarketplaceService get cwd() { return getWorkspacePath() } @@ -122,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. @@ -744,6 +748,7 @@ export class ClineProvider extends EventEmitter implements mcpServers: this.mcpHub.getAllServers(), }) } + this.mcpMarketplaceService.prefetchMcpMarketplace() const cacheDir = await this.ensureCacheDirectoryExists() @@ -1189,6 +1194,28 @@ export class ClineProvider extends EventEmitter implements } break } + + case "fetchMcpMarketplace": { + await this.mcpMarketplaceService.fetchMcpMarketplace(message.bool) + break + } + case "downloadMcp": { + if (message.mcpId) { + await this.mcpMarketplaceService.downloadMcp(this.mcpHub, message.mcpId) + } + break + } + case "silentlyRefreshMcpMarketplace": { + await this.mcpMarketplaceService.silentlyRefreshMcpMarketplace() + break + } + case "openMcpMarketplaceServerDetails": { + if (message.mcpId) { + await this.mcpMarketplaceService.openMcpMarketplaceServerDetails(message.mcpId) + } + break + } + case "openCustomModesSettings": { const customModesFilePath = await this.customModesManager.getCustomModesFilePath() if (customModesFilePath) { diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index de87845eaa..9299657926 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -183,6 +183,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/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index 886290d3d1..ea5eccb41c 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -256,6 +256,7 @@ export type GlobalStateKey = | "language" | "maxReadFileLine" | "fakeAi" + | "mcpMarketplaceCatalog" export type ConfigurationKey = GlobalStateKey | SecretKey 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, + }) + } + } +} diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index b7219de2f8..018724d854 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" @@ -85,6 +87,8 @@ export interface ExtensionMessage { requestyModels?: Record openAiModels?: string[] mcpServers?: McpServer[] + mcpMarketplaceCatalog?: McpMarketplaceCatalog + mcpDownloadDetails?: McpDownloadResponse commits?: GitCommit[] listApiConfig?: ApiConfigMeta[] mode?: Mode diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d87be2a716..ceacc019a8 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -116,6 +116,10 @@ export interface WebviewMessage { | "language" | "maxReadFileLine" | "searchFiles" + | "fetchMcpMarketplace" + | "downloadMcp" + | "openMcpMarketplaceServerDetails" + | "silentlyRefreshMcpMarketplace" text?: string disabled?: boolean askResponse?: ClineAskResponse @@ -141,6 +145,7 @@ export interface WebviewMessage { source?: "global" | "project" requestId?: string ids?: string[] + mcpId?: string } export const checkoutDiffPayloadSchema = z.object({ diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index 5896bee9cd..b1d1771a55 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -124,6 +124,7 @@ export const GLOBAL_STATE_KEYS = [ "maxWorkspaceFiles", "maxReadFileLine", "fakeAi", + "mcpMarketplaceCatalog", ] as const export const PASS_THROUGH_STATE_KEYS = ["taskHistory"] as const 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 +} diff --git a/webview-ui/src/components/mcp/McpView.tsx b/webview-ui/src/components/mcp/McpView.tsx index 97111e1332..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, @@ -20,6 +20,8 @@ import { Tab, TabContent, TabHeader } from "../common/Tab" 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 @@ -34,6 +36,15 @@ const McpView = ({ onDone }: McpViewProps) => { setEnableMcpServerCreation, } = useExtensionState() const { t } = useAppTranslation() + const [activeTab, setActiveTab] = useState("installed") + + const handleTabChange = (tab: string) => { + setActiveTab(tab) + } + + useEffect(() => { + vscode.postMessage({ type: "silentlyRefreshMcpMarketplace" }) + }, []) return ( @@ -45,74 +56,153 @@ const McpView = ({ onDone }: McpViewProps) => {
- - - Model Context Protocol - - - community-made servers - - + handleTabChange("installed")}> + {t("mcp:tabs.installed")} + + handleTabChange("marketplace")}> + {t("mcp:tabs.marketplace")} +
- - - {mcpEnabled && ( + {activeTab === "installed" && ( <> -
- { - setEnableMcpServerCreation(e.target.checked) - vscode.postMessage({ type: "enableMcpServerCreation", bool: e.target.checked }) - }}> - {t("mcp:enableServerCreation.title")} - -

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

+
+ + + Model Context Protocol + + + community-made servers + +
- {/* Server List */} - {servers.length > 0 && ( -
- {servers.map((server) => ( - - ))} -
- )} + - {/* Edit Settings Button */} -
- { - vscode.postMessage({ type: "openMcpSettings" }) - }}> - - {t("mcp:editSettings")} - -
+ {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 */} +
+ { + 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 new file mode 100644 index 0000000000..ab7c56903c --- /dev/null +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx @@ -0,0 +1,295 @@ +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 [isLoading, setIsLoading] = 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 + } + + console.log("Card clicked:", item.mcpId) + setIsLoading(true) + vscode.postMessage({ + type: "openMcpMarketplaceServerDetails", + mcpId: item.mcpId, + }) + }} + style={{ + padding: "14px 16px", + display: "flex", + flexDirection: "column", + gap: 12, + cursor: isLoading ? "wait" : "pointer", + }}> + {/* Main container with logo and content */} +
+ {/* Logo */} + {item.logoUrl && ( + {`${item.name} + )} + + {/* 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({ + type: "downloadMcp", + mcpId: item.mcpId, + }) + } + }} + style={{}}> + + {isInstalled ? "Installed" : isDownloading ? "Installing..." : "Install"} + +
+
+ + {/* Second row: metadata */} +
+ { + 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)" + }}> +
+ + + {item.author} + +
+
+
+ + + {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 new file mode 100644 index 0000000000..d6473b2867 --- /dev/null +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -0,0 +1,291 @@ +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 { McpMarketplaceItem } from "../../../../../src/shared/mcp" +import { useExtensionState } from "../../../context/ExtensionStateContext" +import { vscode } from "../../../utils/vscode" +import McpMarketplaceCard from "./McpMarketplaceCard" +import McpSubmitCard from "./McpSubmitCard" +const McpMarketplaceView = () => { + const { t } = useAppTranslation() + 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" | "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) => { + 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)}> + + {t("mcp:marketplace.retry")} + +
+ ) + } + + return ( +
+
+ {/* Search row */} + setSearchQuery((e.target as HTMLInputElement).value)}> +
+ {searchQuery && ( +
setSearchQuery("")} + slot="end" + style={{ + display: "flex", + justifyContent: "center", + alignItems: "center", + height: "100%", + cursor: "pointer", + }} + /> + )} + + + {/* Filter row */} +
+ + {t("mcp:marketplace.filter")} + +
+ setSelectedCategory((e.target as HTMLSelectElement).value || null)}> + {t("mcp:marketplace.allCategories")} + {categories.map((category) => ( + + {category} + + ))} + +
+
+ + {/* Sort row */} +
+ + {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) => ( + + )) + )} + +
+
+ ) +} + +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 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/en/mcp.json b/webview-ui/src/i18n/locales/en/mcp.json index 710b787e5d..2ab52f7da7 100644 --- a/webview-ui/src/i18n/locales/en/mcp.json +++ b/webview-ui/src/i18n/locales/en/mcp.json @@ -17,12 +17,32 @@ "noDescription": "No description" }, "tabs": { + "installed": "Installed & Settings", + "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", + "noResults": "No matching servers found", + "retry": "Retry", + "install": "Install", + "by": "by {{author}}", + "searchPlaceholder": "Search servers...", + "filter": "Filter:", + "allCategories": "All Categories", + "sort": "Sort:", + "sortByStars": "Most Stars", + "sortByDownloadCount": "Most Installs", + "sortByNewest": "Newest", + "sortByName": "Name" }, "networkTimeout": { "label": "Network 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": "網路逾時",