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 diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 8ef786f4dc..94204867d4 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 { 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" @@ -28,6 +29,7 @@ import { getUri } from "./getUri" import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings" import { BrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../shared/BrowserSettings" import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "../../shared/ChatSettings" +import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider" import { searchCommits } from "../../utils/git" /* @@ -89,6 +91,7 @@ type GlobalStateKey = | "qwenApiLine" | "requestyModelId" | "togetherModelId" + | "mcpMarketplaceCatalog" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -401,6 +404,9 @@ 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) + // Prefetch marketplace and OpenRouter models + + 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) @@ -414,6 +420,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { } } }) + break case "newTask": // Code that should run in response to the hello message command @@ -780,6 +787,55 @@ export class ClineProvider implements vscode.WebviewViewProvider { } break } + case "fetchMcpMarketplace": { + await this.fetchMcpMarketplace(message.bool) + break + } + case "downloadMcp": { + if (message.mcpId) { + await this.downloadMcp(message.mcpId) + } + break + } + case "silentlyRefreshMcpMarketplace": { + await this.silentlyRefreshMcpMarketplace() + break + } + case "openMcpMarketplaceServerDetails": { + if (message.mcpId) { + const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${message.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, + }) + } + } + + this.postMessageToWebview({ type: "relinquishControl" }) + + break + } case "toggleMcpServer": { try { await this.mcpHub?.toggleServerDisabled(message.serverName!, message.disabled!) @@ -1018,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 is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}` + + // Initialize task and show chat view + await this.initClineWithTask(task) + await this.postMessageToWebview({ + type: "action", + action: "chatButtonClicked", + }) + } catch (error) { + console.error("Failed to download MCP:", error) + let errorMessage = "Failed to download MCP" + + if (axios.isAxiosError(error)) { + if (error.code === "ECONNABORTED") { + errorMessage = "Request timed out. Please try again." + } else if (error.response?.status === 404) { + errorMessage = "MCP server not found in marketplace." + } else if (error.response?.status === 500) { + errorMessage = "Internal server error. Please try again later." + } else if (!error.response && error.request) { + errorMessage = "Network error. Please check your internet connection." + } + } else if (error instanceof Error) { + errorMessage = error.message + } + + // Show error in both notification and marketplace UI + vscode.window.showErrorMessage(errorMessage) + await this.postMessageToWebview({ + type: "mcpDownloadDetails", + error: errorMessage, + }) + } + } + // OpenAi async getOpenAiModels(baseUrl?: string, apiKey?: string) { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 6350ed466d..8fcff1f293 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -6,7 +6,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, McpDownloadResponse } from "./mcp" // webview will hold state export interface ExtensionMessage { @@ -27,6 +27,8 @@ export interface ExtensionMessage { | "vsCodeLmModels" | "requestVsCodeLmModels" | "emailSubscribed" + | "mcpMarketplaceCatalog" + | "mcpDownloadDetails" | "commitSearchResults" text?: string action?: @@ -48,6 +50,9 @@ export interface ExtensionMessage { openRouterModels?: Record openAiModels?: string[] mcpServers?: McpServer[] + mcpMarketplaceCatalog?: McpMarketplaceCatalog + error?: string + mcpDownloadDetails?: McpDownloadResponse commits?: GitCommit[] } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 9b0f43ee9b..5c6ea5a3d5 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -43,6 +43,10 @@ export interface WebviewMessage { | "accountLoginClicked" | "accountLogoutClicked" | "subscribeEmail" + | "fetchMcpMarketplace" + | "downloadMcp" + | "openMcpMarketplaceServerDetails" + | "silentlyRefreshMcpMarketplace" | "searchCommits" // | "relaunchChromeDebugMode" text?: string @@ -56,6 +60,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..f0a09afa2d 100644 --- a/src/shared/mcp.ts +++ b/src/shared/mcp.ts @@ -66,3 +66,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 b8afdbb05f..511ac2de3c 100644 --- a/webview-ui/src/components/mcp/McpView.tsx +++ b/webview-ui/src/components/mcp/McpView.tsx @@ -1,10 +1,12 @@ 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" import McpToolRow from "./McpToolRow" import McpResourceRow from "./McpResourceRow" +import McpMarketplaceView from "./marketplace/McpMarketplaceView" +import styled from "styled-components" type McpViewProps = { onDone: () => void @@ -12,6 +14,15 @@ type McpViewProps = { const McpView = ({ onDone }: McpViewProps) => { const { mcpServers: servers } = useExtensionState() + const [activeTab, setActiveTab] = useState("marketplace") + + const handleTabChange = (tab: string) => { + setActiveTab(tab) + } + + useEffect(() => { + vscode.postMessage({ type: "silentlyRefreshMcpMarketplace" }) + }, []) // const [servers, setServers] = useState([ // // Add some mock servers for testing @@ -90,83 +101,141 @@ const McpView = ({ onDone }: McpViewProps) => { display: "flex", justifyContent: "space-between", alignItems: "center", - padding: "10px 17px 10px 20px", + padding: "10px 17px 5px 20px", }}>

MCP Servers

Done -
+
+ {/* Tabs container */}
- 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. - + handleTabChange("marketplace")}> + Marketplace + + handleTabChange("installed")}> + Installed +
- {servers.length > 0 && ( -
- {servers.map((server) => ( - - ))} -
- )} + {/* Content container */} +
+ {activeTab === "marketplace" && } + {activeTab === "installed" && ( +
+
+ 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 - + {/* Settings Section */} +
+ { + vscode.postMessage({ type: "openMcpSettings" }) + }}> + + Configure MCP Servers + + +
+ { + vscode.postMessage({ + type: "openExtensionSettings", + text: "cline.mcp", + }) + }} + style={{ fontSize: "12px" }}> + Advanced MCP Settings + +
+
+
+ )}
- - {/* Advanced Settings Link */} -
- { - vscode.postMessage({ - type: "openExtensionSettings", - text: "cline.mcp", - }) - }} - style={{ fontSize: "12px" }}> - Advanced MCP Settings - -
- - {/* Bottom padding */} -
) } +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) @@ -211,7 +280,7 @@ const ServerRow = ({ server }: { server: McpServer }) => { {!server.error && ( )} - {server.name} + {server.name}
e.stopPropagation()}>
{ + 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 + case "relinquishControl": + setIsLoading(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 */} + +
+
+ + {/* 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..2204709415 --- /dev/null +++ b/webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx @@ -0,0 +1,287 @@ +import { useEffect, useMemo, useState } from "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" +import McpMarketplaceCard from "./McpMarketplaceCard" +import McpSubmitCard from "./McpSubmitCard" +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" | "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)}> + + 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 */} +
+ + Filter: + +
+ setSelectedCategory((e.target as HTMLSelectElement).value || null)}> + All Categories + {categories.map((category) => ( + + {category} + + ))} + +
+
+ + {/* Sort row */} +
+ + Sort: + + setSortBy((e.target as HTMLInputElement).value as typeof sortBy)}> + Most Installs + Most Stars + Newest + Name + +
+
+ + +
+ {filteredItems.length === 0 ? ( +
+ {searchQuery || selectedCategory + ? "No matching MCP servers found" + : "No MCP servers found in the marketplace"} +
+ ) : ( + 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..b00eedc5de --- /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