mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
Merge pull request #1841 from cline/pashpashpash/mcp-marketplace
Add MCP Marketplace
This commit is contained in:
commit
ee50e9710a
9 changed files with 1030 additions and 62 deletions
5
.changeset/stupid-mayflies-melt.md
Normal file
5
.changeset/stupid-mayflies-melt.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Add MCP Marketplace
|
||||
|
|
@ -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<McpMarketplaceCatalog | undefined> {
|
||||
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<McpDownloadResponse>(
|
||||
"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) {
|
||||
|
|
|
|||
|
|
@ -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<string, ModelInfo>
|
||||
openAiModels?: string[]
|
||||
mcpServers?: McpServer[]
|
||||
mcpMarketplaceCatalog?: McpMarketplaceCatalog
|
||||
error?: string
|
||||
mcpDownloadDetails?: McpDownloadResponse
|
||||
commits?: GitCommit[]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<McpServer[]>([
|
||||
// // 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",
|
||||
}}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>MCP Servers</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
|
||||
<div style={{ flex: 1, overflow: "auto" }}>
|
||||
{/* Tabs container */}
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
fontSize: "13px",
|
||||
marginBottom: "16px",
|
||||
marginTop: "5px",
|
||||
display: "flex",
|
||||
gap: "1px",
|
||||
padding: "0 20px 0 20px",
|
||||
borderBottom: "1px solid var(--vscode-panel-border)",
|
||||
}}>
|
||||
The{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol" style={{ display: "inline" }}>
|
||||
Model Context Protocol
|
||||
</VSCodeLink>{" "}
|
||||
enables communication with locally running MCP servers that provide additional tools and resources to extend
|
||||
Cline's capabilities. You can use{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
|
||||
community-made servers
|
||||
</VSCodeLink>{" "}
|
||||
or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest npm docs").{" "}
|
||||
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
|
||||
See a demo here.
|
||||
</VSCodeLink>
|
||||
<TabButton isActive={activeTab === "marketplace"} onClick={() => handleTabChange("marketplace")}>
|
||||
Marketplace
|
||||
</TabButton>
|
||||
<TabButton isActive={activeTab === "installed"} onClick={() => handleTabChange("installed")}>
|
||||
Installed
|
||||
</TabButton>
|
||||
</div>
|
||||
|
||||
{servers.length > 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
}}>
|
||||
{servers.map((server) => (
|
||||
<ServerRow key={server.name} server={server} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Content container */}
|
||||
<div style={{ width: "100%" }}>
|
||||
{activeTab === "marketplace" && <McpMarketplaceView />}
|
||||
{activeTab === "installed" && (
|
||||
<div style={{ padding: "16px 20px" }}>
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
fontSize: "13px",
|
||||
marginBottom: "16px",
|
||||
marginTop: "5px",
|
||||
}}>
|
||||
The{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol" style={{ display: "inline" }}>
|
||||
Model Context Protocol
|
||||
</VSCodeLink>{" "}
|
||||
enables communication with locally running MCP servers that provide additional tools and resources
|
||||
to extend Cline's capabilities. You can use{" "}
|
||||
<VSCodeLink href="https://github.com/modelcontextprotocol/servers" style={{ display: "inline" }}>
|
||||
community-made servers
|
||||
</VSCodeLink>{" "}
|
||||
or ask Cline to create new tools specific to your workflow (e.g., "add a tool that gets the latest
|
||||
npm docs").{" "}
|
||||
<VSCodeLink href="https://x.com/sdrzn/status/1867271665086074969" style={{ display: "inline" }}>
|
||||
See a demo here.
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
|
||||
{/* Server Configuration Button */}
|
||||
{servers.length > 0 ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
}}>
|
||||
{servers.map((server) => (
|
||||
<ServerRow key={server.name} server={server} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
marginTop: "20px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<div>No MCP servers installed yet</div>
|
||||
<VSCodeButton appearance="primary" onClick={() => setActiveTab("marketplace")}>
|
||||
<span className="codicon codicon-cloud-download" style={{ marginRight: "6px" }} />
|
||||
Browse Marketplace
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: "10px", width: "100%" }}>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
style={{ width: "100%" }}
|
||||
onClick={() => {
|
||||
vscode.postMessage({ type: "openMcpSettings" })
|
||||
}}>
|
||||
<span className="codicon codicon-server" style={{ marginRight: "6px" }}></span>
|
||||
Configure MCP Servers
|
||||
</VSCodeButton>
|
||||
{/* Settings Section */}
|
||||
<div style={{ marginBottom: "20px", marginTop: 10 }}>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
style={{ width: "100%", marginBottom: "5px" }}
|
||||
onClick={() => {
|
||||
vscode.postMessage({ type: "openMcpSettings" })
|
||||
}}>
|
||||
<span className="codicon codicon-server" style={{ marginRight: "6px" }}></span>
|
||||
Configure MCP Servers
|
||||
</VSCodeButton>
|
||||
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<VSCodeLink
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "openExtensionSettings",
|
||||
text: "cline.mcp",
|
||||
})
|
||||
}}
|
||||
style={{ fontSize: "12px" }}>
|
||||
Advanced MCP Settings
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Advanced Settings Link */}
|
||||
<div style={{ textAlign: "center", marginTop: "5px" }}>
|
||||
<VSCodeLink
|
||||
onClick={() => {
|
||||
vscode.postMessage({
|
||||
type: "openExtensionSettings",
|
||||
text: "cline.mcp",
|
||||
})
|
||||
}}
|
||||
style={{ fontSize: "12px" }}>
|
||||
Advanced MCP Settings
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
|
||||
{/* Bottom padding */}
|
||||
<div style={{ height: "20px" }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 }) => (
|
||||
<StyledTabButton isActive={isActive} onClick={onClick}>
|
||||
{children}
|
||||
</StyledTabButton>
|
||||
)
|
||||
|
||||
// Server Row Component
|
||||
const ServerRow = ({ server }: { server: McpServer }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
|
@ -211,7 +280,7 @@ const ServerRow = ({ server }: { server: McpServer }) => {
|
|||
{!server.error && (
|
||||
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`} style={{ marginRight: "8px" }} />
|
||||
)}
|
||||
<span style={{ flex: 1 }}>{server.name}</span>
|
||||
<span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{server.name}</span>
|
||||
<div style={{ display: "flex", alignItems: "center", marginRight: "8px" }} onClick={(e) => e.stopPropagation()}>
|
||||
<div
|
||||
role="switch"
|
||||
|
|
|
|||
284
webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx
Normal file
284
webview-ui/src/components/mcp/marketplace/McpMarketplaceCard.tsx
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
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<HTMLDivElement>(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 (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
.mcp-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
.mcp-card:hover {
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div
|
||||
className="mcp-card"
|
||||
onClick={(e) => {
|
||||
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 */}
|
||||
<div style={{ display: "flex", gap: "12px" }}>
|
||||
{/* Logo */}
|
||||
{item.logoUrl && (
|
||||
<img
|
||||
src={item.logoUrl}
|
||||
alt={`${item.name} logo`}
|
||||
style={{
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Content section */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
}}>
|
||||
{/* First row: name and install button */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
}}>
|
||||
<h3
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: "13px",
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
{item.name}
|
||||
</h3>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation() // Prevent card click when clicking install
|
||||
if (!isInstalled && !isDownloading) {
|
||||
setIsDownloading(true)
|
||||
vscode.postMessage({
|
||||
type: "downloadMcp",
|
||||
mcpId: item.mcpId,
|
||||
})
|
||||
}
|
||||
}}
|
||||
style={{}}>
|
||||
<StyledInstallButton disabled={isInstalled || isDownloading} $isInstalled={isInstalled}>
|
||||
{isInstalled ? "Installed" : isDownloading ? "Installing..." : "Install"}
|
||||
</StyledInstallButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Second row: metadata */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
flexWrap: "wrap",
|
||||
minWidth: 0,
|
||||
rowGap: 0, // Add this to remove vertical gap
|
||||
}}>
|
||||
<a
|
||||
href={item.githubUrl}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: "var(--vscode-foreground)",
|
||||
minWidth: 0,
|
||||
opacity: 0.5,
|
||||
textDecoration: "none",
|
||||
border: "none !important",
|
||||
}}
|
||||
className="github-link"
|
||||
onMouseEnter={(e) => {
|
||||
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)"
|
||||
}}>
|
||||
<div style={{ display: "flex", gap: "4px", alignItems: "center" }} ref={githubLinkRef}>
|
||||
<span className="codicon codicon-github" style={{ fontSize: "14px" }} />
|
||||
<span
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordBreak: "break-all",
|
||||
minWidth: 0,
|
||||
}}>
|
||||
{item.author}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
minWidth: 0,
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<span className="codicon codicon-star-full" />
|
||||
<span style={{ wordBreak: "break-all" }}>{item.githubStars?.toLocaleString() ?? 0}</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
minWidth: 0,
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<span className="codicon codicon-cloud-download" />
|
||||
<span style={{ wordBreak: "break-all" }}>{item.downloadCount?.toLocaleString() ?? 0}</span>
|
||||
</div>
|
||||
{item.requiresApiKey && (
|
||||
<span className="codicon codicon-key" title="Requires API key" style={{ flexShrink: 0 }} />
|
||||
)}
|
||||
{item.isRecommended && (
|
||||
<span className="codicon codicon-verified" title="Recommended" style={{ flexShrink: 0 }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description and tags */}
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
||||
<p style={{ fontSize: "13px", margin: 0 }}>{item.description}</p>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "6px",
|
||||
flexWrap: "nowrap",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
padding: "1px 4px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent)",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
whiteSpace: "nowrap",
|
||||
}}>
|
||||
{item.category}
|
||||
</span>
|
||||
{item.tags.map((tag, index) => (
|
||||
<span
|
||||
key={tag}
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
padding: "1px 4px",
|
||||
borderRadius: "3px",
|
||||
border: "1px solid color-mix(in srgb, var(--vscode-descriptionForeground) 50%, transparent)",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
whiteSpace: "nowrap",
|
||||
display: "inline-flex",
|
||||
}}>
|
||||
{tag}
|
||||
{index === item.tags.length - 1 ? "" : ""}
|
||||
</span>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: "32px",
|
||||
background: "linear-gradient(to right, transparent, var(--vscode-sideBar-background))",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
287
webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx
Normal file
287
webview-ui/src/components/mcp/marketplace/McpMarketplaceView.tsx
Normal file
|
|
@ -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<McpMarketplaceItem[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(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 (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
padding: "20px",
|
||||
}}>
|
||||
<VSCodeProgressRing />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
padding: "20px",
|
||||
gap: "12px",
|
||||
}}>
|
||||
<div style={{ color: "var(--vscode-errorForeground)" }}>{error}</div>
|
||||
<VSCodeButton appearance="secondary" onClick={() => fetchMarketplace(true)}>
|
||||
<span className="codicon codicon-refresh" style={{ marginRight: "6px" }} />
|
||||
Retry
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
}}>
|
||||
<div style={{ padding: "20px 20px 5px", display: "flex", flexDirection: "column", gap: "16px" }}>
|
||||
{/* Search row */}
|
||||
<VSCodeTextField
|
||||
style={{ width: "100%" }}
|
||||
placeholder="Search MCPs..."
|
||||
value={searchQuery}
|
||||
onInput={(e) => setSearchQuery((e.target as HTMLInputElement).value)}>
|
||||
<div
|
||||
slot="start"
|
||||
className="codicon codicon-search"
|
||||
style={{
|
||||
fontSize: 13,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
/>
|
||||
{searchQuery && (
|
||||
<div
|
||||
className="codicon codicon-close"
|
||||
aria-label="Clear search"
|
||||
onClick={() => setSearchQuery("")}
|
||||
slot="end"
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</VSCodeTextField>
|
||||
|
||||
{/* Filter row */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 500,
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
Filter:
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
zIndex: 2,
|
||||
flex: 1,
|
||||
}}>
|
||||
<VSCodeDropdown
|
||||
style={{
|
||||
width: "100%",
|
||||
}}
|
||||
value={selectedCategory || ""}
|
||||
onChange={(e) => setSelectedCategory((e.target as HTMLSelectElement).value || null)}>
|
||||
<VSCodeOption value="">All Categories</VSCodeOption>
|
||||
{categories.map((category) => (
|
||||
<VSCodeOption key={category} value={category}>
|
||||
{category}
|
||||
</VSCodeOption>
|
||||
))}
|
||||
</VSCodeDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sort row */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 500,
|
||||
marginTop: "3px",
|
||||
}}>
|
||||
Sort:
|
||||
</span>
|
||||
<VSCodeRadioGroup
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
marginTop: "-2.5px",
|
||||
}}
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy((e.target as HTMLInputElement).value as typeof sortBy)}>
|
||||
<VSCodeRadio value="downloadCount">Most Installs</VSCodeRadio>
|
||||
<VSCodeRadio value="stars">Most Stars</VSCodeRadio>
|
||||
<VSCodeRadio value="newest">Newest</VSCodeRadio>
|
||||
<VSCodeRadio value="name">Name</VSCodeRadio>
|
||||
</VSCodeRadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{`
|
||||
.mcp-search-input,
|
||||
.mcp-select {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mcp-search-input {
|
||||
min-width: 140px;
|
||||
}
|
||||
.mcp-search-input:focus,
|
||||
.mcp-select:focus {
|
||||
border-color: var(--vscode-focusBorder) !important;
|
||||
}
|
||||
.mcp-search-input:hover,
|
||||
.mcp-select:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
{filteredItems.length === 0 ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
padding: "20px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
borderBottom: "1px solid var(--vscode-list-inactiveSelectionBackground)",
|
||||
}}>
|
||||
{searchQuery || selectedCategory
|
||||
? "No matching MCP servers found"
|
||||
: "No MCP servers found in the marketplace"}
|
||||
</div>
|
||||
) : (
|
||||
filteredItems.map((item) => <McpMarketplaceCard key={item.mcpId} item={item} installedServers={mcpServers} />)
|
||||
)}
|
||||
<McpSubmitCard />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpMarketplaceView
|
||||
51
webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx
Normal file
51
webview-ui/src/components/mcp/marketplace/McpSubmitCard.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
const McpSubmitCard = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "24px",
|
||||
padding: "32px 20px",
|
||||
marginTop: "16px",
|
||||
}}>
|
||||
{/* Logo */}
|
||||
<img
|
||||
src="https://storage.googleapis.com/cline_public_images/cline.png"
|
||||
alt="Cline bot logo"
|
||||
style={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
textAlign: "center",
|
||||
maxWidth: "480px",
|
||||
}}>
|
||||
<h3
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: "16px",
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
Is something missing?
|
||||
</h3>
|
||||
<p style={{ fontSize: "13px", margin: 0, color: "var(--vscode-descriptionForeground)" }}>
|
||||
Submit your own MCP servers to the marketplace by{" "}
|
||||
<a href="https://github.com/cline/mcp-marketplace">submitting an issue</a> on the official MCP Marketplace
|
||||
repo on GitHub.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpSubmitCard
|
||||
Loading…
Add table
Reference in a new issue