Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit e6db23b

Browse files
committed
feat: add MCP marketplace with free search engine servers (DuckDuckGo, SearXNG)
Adds a "Quick Add Servers" section to the MCP settings view that lets users install pre-configured free search engine MCP servers with one click: - DuckDuckGo Search (via uvx duckduckgo-mcp-server) - no API key needed - SearXNG (via npx mcp-searxng) - requires self-hosted instance URL - Web Search (via npx web-search-mcp) - no API key needed Implementation: - New mcpMarketplaceCatalog with extensible server templates - McpHub.addServer() method for programmatic server installation - installMcpServer webview message type and handler - McpMarketplace UI component with install buttons and env config - i18n translations for marketplace strings - Tests for catalog validation and UI component Closes #12361
1 parent e921f9d commit e6db23b

10 files changed

Lines changed: 510 additions & 0 deletions

File tree

packages/types/src/vscode-extension-host.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ export interface WebviewMessage {
451451
| "checkpointDiff"
452452
| "checkpointRestore"
453453
| "deleteMcpServer"
454+
| "installMcpServer"
454455
| "codebaseIndexEnabled"
455456
| "searchFiles"
456457
| "toggleApiConfigPin"

src/core/webview/webviewMessageHandler.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,24 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
12991299
}
13001300
break
13011301
}
1302+
case "installMcpServer": {
1303+
if (!message.serverName || !message.config) {
1304+
break
1305+
}
1306+
1307+
try {
1308+
provider.log(`Attempting to install MCP server from marketplace: ${message.serverName}`)
1309+
await provider.getMcpHub()?.addServer(message.serverName, message.config)
1310+
provider.log(`Successfully installed MCP server: ${message.serverName}`)
1311+
1312+
// Refresh the webview state
1313+
await provider.postStateToWebview()
1314+
} catch (error) {
1315+
const errorMessage = error instanceof Error ? error.message : String(error)
1316+
provider.log(`Failed to install MCP server: ${errorMessage}`)
1317+
}
1318+
break
1319+
}
13021320
case "restartMcpServer": {
13031321
try {
13041322
await provider.getMcpHub()?.restartConnection(message.text!, message.source as "global" | "project")

src/i18n/locales/en/mcp.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,9 @@
2424
"refreshing_all": "Refreshing all MCP servers...",
2525
"all_refreshed": "All MCP servers have been refreshed.",
2626
"project_config_deleted": "Project MCP configuration file deleted. All project MCP servers have been disconnected."
27+
},
28+
"marketplace": {
29+
"installed": "Added MCP server: {{serverName}}",
30+
"alreadyInstalled": "Server \"{{serverName}}\" already exists in your MCP settings."
2731
}
2832
}

src/services/mcp/McpHub.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1708,6 +1708,56 @@ export class McpHub {
17081708
}
17091709
}
17101710

1711+
/**
1712+
* Add a new MCP server to the global settings file.
1713+
* Used by the MCP Marketplace to install pre-configured servers.
1714+
*/
1715+
public async addServer(serverName: string, serverConfig: Record<string, unknown>): Promise<void> {
1716+
try {
1717+
const configPath = await this.getMcpSettingsFilePath()
1718+
1719+
// Ensure the settings file exists and is accessible
1720+
try {
1721+
await fs.access(configPath)
1722+
} catch {
1723+
throw new Error("Settings file not accessible")
1724+
}
1725+
1726+
const content = await fs.readFile(configPath, "utf-8")
1727+
const config = JSON.parse(content)
1728+
1729+
if (!config || typeof config !== "object") {
1730+
throw new Error("Invalid config structure")
1731+
}
1732+
1733+
if (!config.mcpServers || typeof config.mcpServers !== "object") {
1734+
config.mcpServers = {}
1735+
}
1736+
1737+
// Check if server already exists
1738+
if (config.mcpServers[serverName]) {
1739+
vscode.window.showWarningMessage(t("mcp:marketplace.alreadyInstalled", { serverName }))
1740+
return
1741+
}
1742+
1743+
config.mcpServers[serverName] = serverConfig
1744+
1745+
const updatedConfig = {
1746+
mcpServers: config.mcpServers,
1747+
}
1748+
1749+
await safeWriteJson(configPath, updatedConfig, { prettyPrint: true })
1750+
1751+
// Trigger server connections update
1752+
await this.updateServerConnections(config.mcpServers, "global")
1753+
1754+
vscode.window.showInformationMessage(t("mcp:marketplace.installed", { serverName }))
1755+
} catch (error) {
1756+
this.showErrorMessage(`Failed to add MCP server ${serverName}`, error)
1757+
throw error
1758+
}
1759+
}
1760+
17111761
async readResource(serverName: string, uri: string, source?: "global" | "project"): Promise<McpResourceResponse> {
17121762
const connection = this.findConnection(serverName, source)
17131763
if (!connection || connection.type !== "connected") {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { mcpMarketplaceCatalog, type McpMarketplaceItem } from "../mcpMarketplaceCatalog"
2+
3+
describe("mcpMarketplaceCatalog", () => {
4+
it("should export a non-empty array of marketplace items", () => {
5+
expect(mcpMarketplaceCatalog).toBeInstanceOf(Array)
6+
expect(mcpMarketplaceCatalog.length).toBeGreaterThan(0)
7+
})
8+
9+
it("should have unique names for each item", () => {
10+
const names = mcpMarketplaceCatalog.map((item) => item.name)
11+
const uniqueNames = new Set(names)
12+
expect(uniqueNames.size).toBe(names.length)
13+
})
14+
15+
it("each item should have required fields", () => {
16+
for (const item of mcpMarketplaceCatalog) {
17+
expect(item.name).toBeTruthy()
18+
expect(item.displayName).toBeTruthy()
19+
expect(item.description).toBeTruthy()
20+
expect(item.category).toBeTruthy()
21+
expect(item.config).toBeDefined()
22+
expect(item.config.command).toBeTruthy()
23+
expect(item.config.args).toBeInstanceOf(Array)
24+
}
25+
})
26+
27+
it("items with requiresSetup should have setupEnvKeys", () => {
28+
const setupItems = mcpMarketplaceCatalog.filter((item) => item.requiresSetup)
29+
for (const item of setupItems) {
30+
expect(item.setupEnvKeys).toBeDefined()
31+
expect(item.setupEnvKeys!.length).toBeGreaterThan(0)
32+
}
33+
})
34+
35+
it("should include DuckDuckGo search server", () => {
36+
const ddg = mcpMarketplaceCatalog.find((item) => item.name === "ddg-search")
37+
expect(ddg).toBeDefined()
38+
expect(ddg!.config.command).toBe("uvx")
39+
expect(ddg!.config.args).toContain("duckduckgo-mcp-server")
40+
expect(ddg!.requiresSetup).toBeFalsy()
41+
})
42+
43+
it("should include SearXNG server with setup required", () => {
44+
const searxng = mcpMarketplaceCatalog.find((item) => item.name === "searxng")
45+
expect(searxng).toBeDefined()
46+
expect(searxng!.requiresSetup).toBe(true)
47+
expect(searxng!.setupEnvKeys).toContain("SEARXNG_URL")
48+
})
49+
})
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Pre-configured MCP server templates for one-click installation from the MCP Marketplace.
3+
* Each entry defines a server configuration that can be written to the user's MCP settings file.
4+
*/
5+
6+
export interface McpMarketplaceItem {
7+
/** Unique key used as the server name in mcpServers config */
8+
name: string
9+
/** Human-readable display name */
10+
displayName: string
11+
/** Short description of what the server does */
12+
description: string
13+
/** Category tag for grouping */
14+
category: "search" | "tools" | "data"
15+
/** The MCP server configuration to write */
16+
config: {
17+
command: string
18+
args: string[]
19+
env?: Record<string, string>
20+
alwaysAllow?: string[]
21+
}
22+
/** Whether the server requires user-provided env variables before installation */
23+
requiresSetup?: boolean
24+
/** Keys in env that need user customization (e.g. instance URLs) */
25+
setupEnvKeys?: string[]
26+
/** URL for more info / docs */
27+
url?: string
28+
}
29+
30+
export const mcpMarketplaceCatalog: McpMarketplaceItem[] = [
31+
{
32+
name: "ddg-search",
33+
displayName: "DuckDuckGo Search",
34+
description: "Free web search via DuckDuckGo. No API key required.",
35+
category: "search",
36+
config: {
37+
command: "uvx",
38+
args: ["duckduckgo-mcp-server"],
39+
env: {
40+
DDG_SAFE_SEARCH: "OFF",
41+
DDG_REGION: "wt-wt",
42+
},
43+
alwaysAllow: ["search", "fetch_content"],
44+
},
45+
url: "https://pypi.org/project/duckduckgo-mcp-server/",
46+
},
47+
{
48+
name: "searxng",
49+
displayName: "SearXNG",
50+
description: "Privacy-focused metasearch engine. Requires a self-hosted SearXNG instance URL.",
51+
category: "search",
52+
config: {
53+
command: "npx",
54+
args: ["-y", "mcp-searxng"],
55+
env: {
56+
SEARXNG_URL: "https://searxng.example.com",
57+
},
58+
alwaysAllow: ["searxng_web_search", "web_url_read"],
59+
},
60+
requiresSetup: true,
61+
setupEnvKeys: ["SEARXNG_URL"],
62+
url: "https://www.npmjs.com/package/mcp-searxng",
63+
},
64+
{
65+
name: "web-search",
66+
displayName: "Web Search (DuckDuckGo)",
67+
description: "Lightweight web search via DuckDuckGo. No API key required. Uses npx.",
68+
category: "search",
69+
config: {
70+
command: "npx",
71+
args: ["-y", "github:tiagohanna123/web-search-mcp"],
72+
alwaysAllow: [],
73+
},
74+
url: "https://github.com/tiagohanna123/web-search-mcp",
75+
},
76+
]
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import React, { useState } from "react"
2+
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
3+
4+
import { vscode } from "@src/utils/vscode"
5+
import { useExtensionState } from "@src/context/ExtensionStateContext"
6+
import { useAppTranslation } from "@src/i18n/TranslationContext"
7+
import { Button } from "@src/components/ui"
8+
9+
import { mcpMarketplaceCatalog, type McpMarketplaceItem } from "../../../../src/shared/mcpMarketplaceCatalog"
10+
11+
const McpMarketplace = () => {
12+
const { mcpServers: servers } = useExtensionState()
13+
const { t } = useAppTranslation()
14+
15+
const installedServerNames = new Set(servers.map((s) => s.name))
16+
17+
return (
18+
<div style={{ marginTop: "15px" }}>
19+
<div
20+
style={{
21+
fontWeight: 500,
22+
fontSize: "13px",
23+
color: "var(--vscode-foreground)",
24+
marginBottom: "6px",
25+
}}>
26+
{t("mcp:marketplace.title")}
27+
</div>
28+
<div
29+
style={{
30+
fontSize: "12px",
31+
color: "var(--vscode-descriptionForeground)",
32+
marginBottom: "10px",
33+
}}>
34+
{t("mcp:marketplace.description")}
35+
</div>
36+
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
37+
{mcpMarketplaceCatalog.map((item) => (
38+
<MarketplaceRow key={item.name} item={item} isInstalled={installedServerNames.has(item.name)} />
39+
))}
40+
</div>
41+
</div>
42+
)
43+
}
44+
45+
const MarketplaceRow = ({ item, isInstalled }: { item: McpMarketplaceItem; isInstalled: boolean }) => {
46+
const { t } = useAppTranslation()
47+
const [envValues, setEnvValues] = useState<Record<string, string>>(() => {
48+
const initial: Record<string, string> = {}
49+
if (item.setupEnvKeys) {
50+
for (const key of item.setupEnvKeys) {
51+
initial[key] = item.config.env?.[key] ?? ""
52+
}
53+
}
54+
return initial
55+
})
56+
57+
const handleInstall = () => {
58+
const config = { ...item.config }
59+
if (item.requiresSetup && item.setupEnvKeys) {
60+
config.env = { ...config.env }
61+
for (const key of item.setupEnvKeys) {
62+
if (envValues[key]) {
63+
config.env[key] = envValues[key]
64+
}
65+
}
66+
}
67+
vscode.postMessage({
68+
type: "installMcpServer",
69+
serverName: item.name,
70+
config,
71+
})
72+
}
73+
74+
return (
75+
<div
76+
className="rounded bg-vscode-textCodeBlock-background p-2"
77+
style={{
78+
opacity: isInstalled ? 0.6 : 1,
79+
}}>
80+
<div className="flex items-center justify-between gap-2">
81+
<div className="flex-1 min-w-0">
82+
<div className="flex items-center gap-1.5">
83+
<span className="codicon codicon-search text-xs" />
84+
<span className="font-medium text-[13px] text-vscode-foreground">{item.displayName}</span>
85+
{item.requiresSetup && (
86+
<span
87+
className="text-[10px] px-1 py-0.5 rounded"
88+
style={{
89+
background: "var(--vscode-editorWarning-foreground)",
90+
color: "var(--vscode-editor-background)",
91+
}}
92+
title={t("mcp:marketplace.requiresSetupHint", {
93+
keys: item.setupEnvKeys?.join(", ") ?? "",
94+
})}>
95+
{t("mcp:marketplace.requiresSetup")}
96+
</span>
97+
)}
98+
</div>
99+
<div className="text-xs text-vscode-descriptionForeground mt-0.5">{item.description}</div>
100+
</div>
101+
<div className="flex items-center gap-2 shrink-0">
102+
{item.url && (
103+
<VSCodeLink href={item.url} style={{ fontSize: "11px" }}>
104+
{t("mcp:marketplace.learnMore")}
105+
</VSCodeLink>
106+
)}
107+
<Button
108+
variant="secondary"
109+
disabled={isInstalled}
110+
onClick={handleInstall}
111+
style={{ minWidth: "60px", fontSize: "12px" }}>
112+
{isInstalled ? (
113+
<>
114+
<span className="codicon codicon-check mr-1" />
115+
Added
116+
</>
117+
) : (
118+
<>
119+
<span className="codicon codicon-add mr-1" />
120+
{t("mcp:marketplace.install")}
121+
</>
122+
)}
123+
</Button>
124+
</div>
125+
</div>
126+
{item.requiresSetup && item.setupEnvKeys && !isInstalled && (
127+
<div className="mt-2 flex flex-col gap-1.5">
128+
{item.setupEnvKeys.map((key) => (
129+
<div key={key} className="flex items-center gap-2">
130+
<label
131+
className="text-[11px] text-vscode-descriptionForeground shrink-0"
132+
style={{ minWidth: "90px" }}>
133+
{key}:
134+
</label>
135+
<input
136+
type="text"
137+
value={envValues[key] ?? ""}
138+
onChange={(e) =>
139+
setEnvValues((prev) => ({
140+
...prev,
141+
[key]: e.target.value,
142+
}))
143+
}
144+
placeholder={item.config.env?.[key] ?? ""}
145+
className="flex-1 rounded px-1.5 py-0.5 text-xs"
146+
style={{
147+
background: "var(--vscode-input-background)",
148+
color: "var(--vscode-input-foreground)",
149+
border: "1px solid var(--vscode-input-border, transparent)",
150+
}}
151+
/>
152+
</div>
153+
))}
154+
</div>
155+
)}
156+
</div>
157+
)
158+
}
159+
160+
export default McpMarketplace

0 commit comments

Comments
 (0)