Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ export interface WebviewMessage {
| "checkpointDiff"
| "checkpointRestore"
| "deleteMcpServer"
| "installMcpServer"
| "codebaseIndexEnabled"
| "searchFiles"
| "toggleApiConfigPin"
Expand Down
18 changes: 18 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,24 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
}
break
}
case "installMcpServer": {
if (!message.serverName || !message.config) {
break
}

try {
provider.log(`Attempting to install MCP server from marketplace: ${message.serverName}`)
await provider.getMcpHub()?.addServer(message.serverName, message.config)
provider.log(`Successfully installed MCP server: ${message.serverName}`)

// Refresh the webview state
await provider.postStateToWebview()
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
provider.log(`Failed to install MCP server: ${errorMessage}`)
}
break
}
case "restartMcpServer": {
try {
await provider.getMcpHub()?.restartConnection(message.text!, message.source as "global" | "project")
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/locales/en/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,9 @@
"refreshing_all": "Refreshing all MCP servers...",
"all_refreshed": "All MCP servers have been refreshed.",
"project_config_deleted": "Project MCP configuration file deleted. All project MCP servers have been disconnected."
},
"marketplace": {
"installed": "Added MCP server: {{serverName}}",
"alreadyInstalled": "Server \"{{serverName}}\" already exists in your MCP settings."
}
}
50 changes: 50 additions & 0 deletions src/services/mcp/McpHub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1708,6 +1708,56 @@ export class McpHub {
}
}

/**
* Add a new MCP server to the global settings file.
* Used by the MCP Marketplace to install pre-configured servers.
*/
public async addServer(serverName: string, serverConfig: Record<string, unknown>): Promise<void> {
try {
const configPath = await this.getMcpSettingsFilePath()

// Ensure the settings file exists and is accessible
try {
await fs.access(configPath)
} catch {
throw new Error("Settings file not accessible")
}

const content = await fs.readFile(configPath, "utf-8")
const config = JSON.parse(content)

if (!config || typeof config !== "object") {
throw new Error("Invalid config structure")
}

if (!config.mcpServers || typeof config.mcpServers !== "object") {
config.mcpServers = {}
}

// Check if server already exists
if (config.mcpServers[serverName]) {
vscode.window.showWarningMessage(t("mcp:marketplace.alreadyInstalled", { serverName }))
return
}

config.mcpServers[serverName] = serverConfig

const updatedConfig = {
mcpServers: config.mcpServers,
}

await safeWriteJson(configPath, updatedConfig, { prettyPrint: true })

// Trigger server connections update
await this.updateServerConnections(config.mcpServers, "global")

vscode.window.showInformationMessage(t("mcp:marketplace.installed", { serverName }))
} catch (error) {
this.showErrorMessage(`Failed to add MCP server ${serverName}`, error)
throw error
}
}

async readResource(serverName: string, uri: string, source?: "global" | "project"): Promise<McpResourceResponse> {
const connection = this.findConnection(serverName, source)
if (!connection || connection.type !== "connected") {
Expand Down
49 changes: 49 additions & 0 deletions src/shared/__tests__/mcpMarketplaceCatalog.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { mcpMarketplaceCatalog, type McpMarketplaceItem } from "../mcpMarketplaceCatalog"

describe("mcpMarketplaceCatalog", () => {
it("should export a non-empty array of marketplace items", () => {
expect(mcpMarketplaceCatalog).toBeInstanceOf(Array)
expect(mcpMarketplaceCatalog.length).toBeGreaterThan(0)
})

it("should have unique names for each item", () => {
const names = mcpMarketplaceCatalog.map((item) => item.name)
const uniqueNames = new Set(names)
expect(uniqueNames.size).toBe(names.length)
})

it("each item should have required fields", () => {
for (const item of mcpMarketplaceCatalog) {
expect(item.name).toBeTruthy()
expect(item.displayName).toBeTruthy()
expect(item.description).toBeTruthy()
expect(item.category).toBeTruthy()
expect(item.config).toBeDefined()
expect(item.config.command).toBeTruthy()
expect(item.config.args).toBeInstanceOf(Array)
}
})

it("items with requiresSetup should have setupEnvKeys", () => {
const setupItems = mcpMarketplaceCatalog.filter((item) => item.requiresSetup)
for (const item of setupItems) {
expect(item.setupEnvKeys).toBeDefined()
expect(item.setupEnvKeys!.length).toBeGreaterThan(0)
}
})

it("should include DuckDuckGo search server", () => {
const ddg = mcpMarketplaceCatalog.find((item) => item.name === "ddg-search")
expect(ddg).toBeDefined()
expect(ddg!.config.command).toBe("uvx")
expect(ddg!.config.args).toContain("duckduckgo-mcp-server")
expect(ddg!.requiresSetup).toBeFalsy()
})

it("should include SearXNG server with setup required", () => {
const searxng = mcpMarketplaceCatalog.find((item) => item.name === "searxng")
expect(searxng).toBeDefined()
expect(searxng!.requiresSetup).toBe(true)
expect(searxng!.setupEnvKeys).toContain("SEARXNG_URL")
})
})
76 changes: 76 additions & 0 deletions src/shared/mcpMarketplaceCatalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Pre-configured MCP server templates for one-click installation from the MCP Marketplace.
* Each entry defines a server configuration that can be written to the user's MCP settings file.
*/

export interface McpMarketplaceItem {
/** Unique key used as the server name in mcpServers config */
name: string
/** Human-readable display name */
displayName: string
/** Short description of what the server does */
description: string
/** Category tag for grouping */
category: "search" | "tools" | "data"
/** The MCP server configuration to write */
config: {
command: string
args: string[]
env?: Record<string, string>
alwaysAllow?: string[]
}
/** Whether the server requires user-provided env variables before installation */
requiresSetup?: boolean
/** Keys in env that need user customization (e.g. instance URLs) */
setupEnvKeys?: string[]
/** URL for more info / docs */
url?: string
}

export const mcpMarketplaceCatalog: McpMarketplaceItem[] = [
{
name: "ddg-search",
displayName: "DuckDuckGo Search",
description: "Free web search via DuckDuckGo. No API key required.",
category: "search",
config: {
command: "uvx",
args: ["duckduckgo-mcp-server"],
env: {
DDG_SAFE_SEARCH: "OFF",
DDG_REGION: "wt-wt",
},
alwaysAllow: ["search", "fetch_content"],
},
url: "https://pypi.org/project/duckduckgo-mcp-server/",
},
{
name: "searxng",
displayName: "SearXNG",
description: "Privacy-focused metasearch engine. Requires a self-hosted SearXNG instance URL.",
category: "search",
config: {
command: "npx",
args: ["-y", "mcp-searxng"],
env: {
SEARXNG_URL: "https://searxng.example.com",
},
alwaysAllow: ["searxng_web_search", "web_url_read"],
},
requiresSetup: true,
setupEnvKeys: ["SEARXNG_URL"],
url: "https://www.npmjs.com/package/mcp-searxng",
},
{
name: "web-search",
displayName: "Web Search (DuckDuckGo)",
description: "Lightweight web search via DuckDuckGo. No API key required. Uses npx.",
category: "search",
config: {
command: "npx",
args: ["-y", "github:tiagohanna123/web-search-mcp"],
alwaysAllow: [],
},
url: "https://github.com/tiagohanna123/web-search-mcp",
},
]
160 changes: 160 additions & 0 deletions webview-ui/src/components/mcp/McpMarketplace.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import React, { useState } from "react"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"

import { vscode } from "@src/utils/vscode"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Button } from "@src/components/ui"

import { mcpMarketplaceCatalog, type McpMarketplaceItem } from "../../../../src/shared/mcpMarketplaceCatalog"

const McpMarketplace = () => {
const { mcpServers: servers } = useExtensionState()
const { t } = useAppTranslation()

const installedServerNames = new Set(servers.map((s) => s.name))

return (
<div style={{ marginTop: "15px" }}>
<div
style={{
fontWeight: 500,
fontSize: "13px",
color: "var(--vscode-foreground)",
marginBottom: "6px",
}}>
{t("mcp:marketplace.title")}
</div>
<div
style={{
fontSize: "12px",
color: "var(--vscode-descriptionForeground)",
marginBottom: "10px",
}}>
{t("mcp:marketplace.description")}
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
{mcpMarketplaceCatalog.map((item) => (
<MarketplaceRow key={item.name} item={item} isInstalled={installedServerNames.has(item.name)} />
))}
</div>
</div>
)
}

const MarketplaceRow = ({ item, isInstalled }: { item: McpMarketplaceItem; isInstalled: boolean }) => {
const { t } = useAppTranslation()
const [envValues, setEnvValues] = useState<Record<string, string>>(() => {
const initial: Record<string, string> = {}
if (item.setupEnvKeys) {
for (const key of item.setupEnvKeys) {
initial[key] = item.config.env?.[key] ?? ""
}
}
return initial
})

const handleInstall = () => {
const config = { ...item.config }
if (item.requiresSetup && item.setupEnvKeys) {
config.env = { ...config.env }
for (const key of item.setupEnvKeys) {
if (envValues[key]) {
config.env[key] = envValues[key]
}
}
}
vscode.postMessage({
type: "installMcpServer",
serverName: item.name,
config,
})
}

return (
<div
className="rounded bg-vscode-textCodeBlock-background p-2"
style={{
opacity: isInstalled ? 0.6 : 1,
}}>
<div className="flex items-center justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<span className="codicon codicon-search text-xs" />
<span className="font-medium text-[13px] text-vscode-foreground">{item.displayName}</span>
{item.requiresSetup && (
<span
className="text-[10px] px-1 py-0.5 rounded"
style={{
background: "var(--vscode-editorWarning-foreground)",
color: "var(--vscode-editor-background)",
}}
title={t("mcp:marketplace.requiresSetupHint", {
keys: item.setupEnvKeys?.join(", ") ?? "",
})}>
{t("mcp:marketplace.requiresSetup")}
</span>
)}
</div>
<div className="text-xs text-vscode-descriptionForeground mt-0.5">{item.description}</div>
</div>
<div className="flex items-center gap-2 shrink-0">
{item.url && (
<VSCodeLink href={item.url} style={{ fontSize: "11px" }}>
{t("mcp:marketplace.learnMore")}
</VSCodeLink>
)}
<Button
variant="secondary"
disabled={isInstalled}
onClick={handleInstall}
style={{ minWidth: "60px", fontSize: "12px" }}>
{isInstalled ? (
<>
<span className="codicon codicon-check mr-1" />
Added
</>
) : (
<>
<span className="codicon codicon-add mr-1" />
{t("mcp:marketplace.install")}
</>
)}
</Button>
</div>
</div>
{item.requiresSetup && item.setupEnvKeys && !isInstalled && (
<div className="mt-2 flex flex-col gap-1.5">
{item.setupEnvKeys.map((key) => (
<div key={key} className="flex items-center gap-2">
<label
className="text-[11px] text-vscode-descriptionForeground shrink-0"
style={{ minWidth: "90px" }}>
{key}:
</label>
<input
type="text"
value={envValues[key] ?? ""}
onChange={(e) =>
setEnvValues((prev) => ({
...prev,
[key]: e.target.value,
}))
}
placeholder={item.config.env?.[key] ?? ""}
className="flex-1 rounded px-1.5 py-0.5 text-xs"
style={{
background: "var(--vscode-input-background)",
color: "var(--vscode-input-foreground)",
border: "1px solid var(--vscode-input-border, transparent)",
}}
/>
</div>
))}
</div>
)}
</div>
)
}

export default McpMarketplace
Loading
Loading