diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js index 1122f02..61168a1 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect } from "react"; -import { Card, Button, ManualConfigModal, ComboFormModal } from "@/shared/components"; +import { Card, Button, ManualConfigModal, ComboFormModal, McpMarketplaceModal } from "@/shared/components"; import Image from "next/image"; import BaseUrlSelect from "./BaseUrlSelect"; @@ -39,9 +39,9 @@ export default function CoworkToolCard({ const [selectedModels, setSelectedModels] = useState([]); const [showManualConfigModal, setShowManualConfigModal] = useState(false); const [customBaseUrl, setCustomBaseUrl] = useState(""); - const [selectedPlugins, setSelectedPlugins] = useState([]); - const [pluginsExpanded, setPluginsExpanded] = useState(false); + const [plugins, setPlugins] = useState([]); const [comboModalOpen, setComboModalOpen] = useState(false); + const [marketplaceOpen, setMarketplaceOpen] = useState(false); useEffect(() => { if (apiKeys?.length > 0 && !selectedApiKey) { @@ -64,8 +64,11 @@ export default function CoworkToolCard({ if (status?.cowork?.baseUrl && !customBaseUrl) { setCustomBaseUrl(stripV1(status.cowork.baseUrl)); } - if (Array.isArray(status?.cowork?.selectedPlugins)) { - setSelectedPlugins(status.cowork.selectedPlugins); + // Initialize plugins: from current config, fallback to defaultPlugins + if (Array.isArray(status?.cowork?.plugins) && status.cowork.plugins.length > 0) { + setPlugins(status.cowork.plugins); + } else if (plugins.length === 0 && Array.isArray(status?.defaultPlugins)) { + setPlugins(status.defaultPlugins); } }, [status]); @@ -116,7 +119,7 @@ export default function CoworkToolCard({ baseUrl: effectiveUrl, apiKey: keyToUse, models: selectedModels, - plugins: selectedPlugins, + plugins, }), }); const data = await res.json(); @@ -145,7 +148,6 @@ export default function CoworkToolCard({ setMessage({ type: "error", text: err.error || "Failed to create combo" }); return; } - // Add combo name into selected models for Cowork if (!selectedModels.includes(name)) { setSelectedModels([...selectedModels, name]); } @@ -165,6 +167,7 @@ export default function CoworkToolCard({ if (res.ok) { setMessage({ type: "success", text: "Settings reset successfully" }); setSelectedModels([]); + setPlugins(status?.defaultPlugins || []); checkStatus(); } else { setMessage({ type: "error", text: data.error || "Failed to reset" }); @@ -176,6 +179,15 @@ export default function CoworkToolCard({ } }; + const addPlugin = (p) => { + if (plugins.some((x) => x.name === p.name)) return; + setPlugins([...plugins, p]); + }; + + const removePlugin = (name) => { + setPlugins(plugins.filter((p) => p.name !== name)); + }; + const getManualConfigs = () => { const keyToUse = (selectedApiKey && selectedApiKey.trim()) ? selectedApiKey @@ -307,51 +319,33 @@ export default function CoworkToolCard({ - {false && (
- Connectors +
+ Plugins arrow_forward
-
- {selectedPlugins.length} of {(status?.availablePlugins || []).length} selected - -
- {pluginsExpanded && ( -
- {(status?.availablePlugins || []).map((p) => { - const checked = selectedPlugins.includes(p.name); - return ( - - ); - })} -
- )} - {!pluginsExpanded && selectedPlugins.length > 0 && ( -
- {selectedPlugins.map((name) => ( - - {name} - - ))} -
- )} + )) + )} +
+ +

+ ๐Ÿ’ก Exa is auto-installed. Prefer web_search_exa for web search and web_fetch_exa for reading pages. +

-
)} + {message && ( @@ -372,7 +366,6 @@ export default function CoworkToolCard({ content_copyManual Config - )} @@ -394,6 +387,13 @@ export default function CoworkToolCard({ forcePrefix="claude-" title="Create Cowork Combo" /> + + setMarketplaceOpen(false)} + onAdd={addPlugin} + addedNames={plugins.map((p) => p.name)} + /> ); } diff --git a/src/app/api/cli-tools/cowork-mcp-registry/route.js b/src/app/api/cli-tools/cowork-mcp-registry/route.js index b511e12..62275f8 100644 --- a/src/app/api/cli-tools/cowork-mcp-registry/route.js +++ b/src/app/api/cli-tools/cowork-mcp-registry/route.js @@ -4,78 +4,58 @@ import { NextResponse } from "next/server"; const REGISTRY_URL = "https://api.anthropic.com/mcp-registry/v0/servers"; const VISIBILITY = "commercial,gsuite,gsuite-google"; -const PLUGINS_REPO = "anthropics/knowledge-work-plugins"; -const GH_API = "https://api.github.com"; -const GH_RAW = "https://raw.githubusercontent.com"; -const CACHE_TTL_MS = 60 * 60 * 1000; // 1h - +const CACHE_TTL_MS = 60 * 60 * 1000; const G_KEY = "__9routerCoworkMcpRegistryCache"; + function gcache() { if (!globalThis[G_KEY]) globalThis[G_KEY] = { ts: 0, data: null }; return globalThis[G_KEY]; } -// Fetch full registry across pagination -async function fetchRegistry() { +// Filter out claude.ai-mediated servers (broken in 3p) and tenant-required entries. +function isDirectConnect(url) { + if (!url || typeof url !== "string") return false; + if (/^https?:\/\/[^/]*\bmcp\.claude\.com\b/i.test(url)) return false; + if (/^https?:\/\/api\.anthropic\.com\/mcp\b/i.test(url)) return false; + if (/[<{]/.test(url)) return false; + return /^https:\/\//i.test(url); +} + +async function fetchAll() { const out = []; let cursor = ""; for (let i = 0; i < 20; i++) { const url = `${REGISTRY_URL}?limit=500&visibility=${VISIBILITY}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`; - const r = await fetch(url, { headers: { "accept": "application/json" } }); + const r = await fetch(url, { headers: { accept: "application/json" } }); if (!r.ok) break; const j = await r.json(); for (const item of j.servers || []) { const s = item.server || {}; + const meta = item._meta?.["com.anthropic.api/mcp-registry"] || {}; const remote = (s.remotes || [])[0]; - if (!remote?.url) continue; - const transport = remote.type === "streamable-http" ? "http" : (remote.type === "sse" ? "sse" : "http"); + if (!remote?.url || !isDirectConnect(remote.url)) continue; + if (meta.requiredFields?.length) continue; + const transport = remote.type === "sse" ? "sse" : "http"; + const toolNames = Array.isArray(meta.toolNames) ? meta.toolNames : []; out.push({ - source: "registry", name: s.name, - title: s.title || s.name, - description: s.description || "", + slug: meta.slug || s.name, + title: s.title || meta.displayName || s.name, + description: s.description || meta.oneLiner || "", url: remote.url, transport, + oauth: !meta.isAuthless, + toolNames, + toolCount: toolNames.length, + iconUrl: meta.iconUrl || null, }); } cursor = j.metadata?.nextCursor; if (!cursor) break; } - return out; -} - -// Fetch plugins from anthropics/knowledge-work-plugins. Each plugin folder contains -// .claude-plugin/plugin.json with mcp_servers map. -async function fetchPlugins() { - const r = await fetch(`${GH_API}/repos/${PLUGINS_REPO}/contents/`, { headers: { "accept": "application/vnd.github.v3+json" } }); - if (!r.ok) return []; - const items = await r.json(); - const dirs = items.filter((i) => i.type === "dir" && !i.name.startsWith(".") && i.name !== "partner-built"); - const out = []; - await Promise.all(dirs.map(async (d) => { - try { - const url = `${GH_RAW}/${PLUGINS_REPO}/main/${d.name}/.claude-plugin/plugin.json`; - const pr = await fetch(url); - if (!pr.ok) return; - const pj = await pr.json(); - const servers = pj.mcp_servers || pj.mcpServers || {}; - for (const [key, srv] of Object.entries(servers)) { - if (!srv?.url || typeof srv.url !== "string") continue; - if (!/^https?:\/\//i.test(srv.url)) continue; - const transport = /\/sse(\b|\/)/i.test(srv.url) ? "sse" : (srv.type === "sse" ? "sse" : "http"); - out.push({ - source: "plugins", - plugin: d.name, - name: `${d.name}-${key}`, - title: pj.name || d.name, - description: pj.description || "", - url: srv.url, - transport, - }); - } - } catch { /* skip */ } - })); - return out; + // Dedupe by url + const seen = new Set(); + return out.filter((s) => (seen.has(s.url) ? false : (seen.add(s.url), true))); } export async function GET(request) { @@ -86,19 +66,12 @@ export async function GET(request) { return NextResponse.json({ cached: true, ...cache.data }); } try { - const [registry, plugins] = await Promise.all([fetchRegistry(), fetchPlugins()]); - // Deduplicate by url - const seen = new Set(); - const merged = [...registry, ...plugins].filter((s) => { - if (seen.has(s.url)) return false; - seen.add(s.url); - return true; - }); - const data = { servers: merged, counts: { registry: registry.length, plugins: plugins.length, total: merged.length } }; + const servers = await fetchAll(); + const data = { servers, total: servers.length }; cache.ts = Date.now(); cache.data = data; return NextResponse.json({ cached: false, ...data }); } catch (e) { - return NextResponse.json({ error: e.message, servers: [], counts: { total: 0 } }, { status: 500 }); + return NextResponse.json({ error: e.message, servers: [], total: 0 }, { status: 500 }); } } diff --git a/src/app/api/cli-tools/cowork-mcp-tools/route.js b/src/app/api/cli-tools/cowork-mcp-tools/route.js new file mode 100644 index 0000000..5cc3d3a --- /dev/null +++ b/src/app/api/cli-tools/cowork-mcp-tools/route.js @@ -0,0 +1,95 @@ +"use server"; + +import { NextResponse } from "next/server"; + +const TIMEOUT_MS = 8000; + +// Probe MCP server: initialize + tools/list. No auth header โ€” works for authless servers. +// OAuth servers return 401, signal client to skip tool listing. +async function probeMcp(url) { + const headers = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + "MCP-Protocol-Version": "2025-06-18", + }; + const ac = new AbortController(); + const timer = setTimeout(() => ac.abort(), TIMEOUT_MS); + try { + // Step 1: initialize + const initRes = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify({ + jsonrpc: "2.0", id: 1, method: "initialize", + params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "9router", version: "1" } }, + }), + signal: ac.signal, + }); + if (initRes.status === 401 || initRes.status === 403) { + return { requiresAuth: true, tools: [] }; + } + if (!initRes.ok) { + return { error: `init ${initRes.status}`, tools: [] }; + } + const sessionId = initRes.headers.get("mcp-session-id") || ""; + await initRes.text().catch(() => {}); + + const listHeaders = { ...headers }; + if (sessionId) listHeaders["mcp-session-id"] = sessionId; + + // Step 2: notifications/initialized (required by spec before tools/list) + await fetch(url, { + method: "POST", + headers: listHeaders, + body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} }), + signal: ac.signal, + }).catch(() => {}); + + // Step 3: tools/list + const listRes = await fetch(url, { + method: "POST", + headers: listHeaders, + body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }), + signal: ac.signal, + }); + if (listRes.status === 401 || listRes.status === 403) { + return { requiresAuth: true, tools: [] }; + } + const ct = listRes.headers.get("content-type") || ""; + let parsed; + if (ct.includes("text/event-stream")) { + // Parse SSE: each "data: {...}" line is a JSON-RPC message + const text = await listRes.text(); + const dataLines = text.split("\n").filter((l) => l.startsWith("data:")); + for (const line of dataLines) { + try { + const obj = JSON.parse(line.replace(/^data:\s*/, "")); + if (obj?.id === 2 && obj.result) { parsed = obj; break; } + } catch { /* skip */ } + } + } else { + parsed = await listRes.json().catch(() => null); + } + const tools = parsed?.result?.tools || []; + return { + tools: tools.map((t) => ({ name: t.name, description: t.description || "" })), + }; + } catch (e) { + return { error: e.name === "AbortError" ? "timeout" : e.message, tools: [] }; + } finally { + clearTimeout(timer); + } +} + +export async function POST(request) { + try { + const { url } = await request.json(); + if (!url || typeof url !== "string") { + return NextResponse.json({ error: "url required" }, { status: 400 }); + } + const result = await probeMcp(url); + return NextResponse.json(result); + } catch (e) { + return NextResponse.json({ error: e.message, tools: [] }, { status: 500 }); + } +} diff --git a/src/app/api/cli-tools/cowork-settings/route.js b/src/app/api/cli-tools/cowork-settings/route.js index 2b00861..68313c3 100644 --- a/src/app/api/cli-tools/cowork-settings/route.js +++ b/src/app/api/cli-tools/cowork-settings/route.js @@ -5,116 +5,27 @@ import fs from "fs/promises"; import path from "path"; import os from "os"; import crypto from "crypto"; -import { COWORK_PLUGINS, buildManagedMcpServers } from "@/shared/constants/coworkPlugins"; +import { DEFAULT_PLUGINS, buildManagedMcpServers } from "@/shared/constants/coworkPlugins"; const PROVIDER = "gateway"; -// Plugin folder mount location. -// Claude Cowork 3p actually launches with --user-data-dir=Claude-3p, so plugins -// must live there (not the system /Library path which requires admin & isn't read in 3p). -const getOrgPluginsCandidates = () => { - if (os.platform() === "darwin") { - const home = os.homedir(); - return [ - path.join(home, "Library", "Application Support", "Claude-3p", "org-plugins"), - path.join(home, "Library", "Application Support", "Claude", "org-plugins"), - "/Library/Application Support/Claude/org-plugins", - ]; - } - if (os.platform() === "win32") { - const localApp = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"); - const programData = process.env.ProgramData || "C:\\ProgramData"; - return [ - path.join(localApp, "Claude-3p", "org-plugins"), - path.join(localApp, "Claude", "org-plugins"), - path.join(programData, "Claude", "org-plugins"), - ]; - } - return [path.join(os.homedir(), ".config", "Claude-3p", "org-plugins"), "/etc/Claude/org-plugins"]; +// Hardcoded relax-security profile applied on every Apply. +const SECURITY_RELAX = { + coworkEgressAllowedHosts: ["*"], + disabledBuiltinTools: [], + isLocalDevMcpEnabled: true, + isDesktopExtensionEnabled: true, + isDesktopExtensionDirectoryEnabled: true, + isDesktopExtensionSignatureRequired: false, + isClaudeCodeForDesktopEnabled: true, + disableEssentialTelemetry: true, + disableNonessentialTelemetry: true, + disableNonessentialServices: true, }; -// Pick first writable candidate for org-plugins -async function pickPluginsRoot() { - for (const dir of getOrgPluginsCandidates()) { - try { - await fs.mkdir(dir, { recursive: true }); - // Probe write - const probe = path.join(dir, ".__9router_probe"); - await fs.writeFile(probe, "ok"); - await fs.unlink(probe); - return dir; - } catch { /* try next */ } - } - return null; -} +// Tools auto-allow per server via toolPolicy["*"] = "allow" semantics. +// 3p schema requires explicit tool names; we mark "*" via operonSkipMcpApprovals instead. -// Create plugin folder mount: org-plugins//claude-plugin/{plugin.json, version.json, .mcp.json} -async function writeOrgPluginsFolder(selectedPluginNames) { - const root = await pickPluginsRoot(); - if (!root) return { error: "no_writable_plugins_dir", written: [] }; - const set = new Set(selectedPluginNames || []); - const selectedPlugins = COWORK_PLUGINS.filter((p) => set.has(p.name)); - // Remove previously-managed plugin subfolders (best-effort) - for (const p of COWORK_PLUGINS) { - try { await fs.rm(path.join(root, p.name), { recursive: true, force: true }); } catch { /* ignore */ } - } - const written = []; - for (const p of selectedPlugins) { - const pluginRoot = path.join(root, p.name); - const metaDir = path.join(pluginRoot, ".claude-plugin"); - try { - await fs.mkdir(metaDir, { recursive: true }); - const manifest = { name: p.name, version: "1.0.0", description: p.description || p.name, author: { name: "9router" } }; - await fs.writeFile(path.join(metaDir, "plugin.json"), JSON.stringify(manifest, null, 2)); - // .mcp.json at plugin root, schema: {mcpServers: {name: {type, url, oauth?}}} - const mcpServers = {}; - for (const s of p.servers) { - const key = p.servers.length === 1 ? p.name : `${p.name}-${s.key}`; - mcpServers[key] = { - type: /\/sse(\b|\/)/i.test(s.url) ? "sse" : "http", - url: s.url, - }; - } - await fs.writeFile(path.join(pluginRoot, ".mcp.json"), JSON.stringify({ mcpServers }, null, 2)); - written.push(p.name); - } catch (e) { - return { error: e.code || e.message, written, root }; - } - } - return { written, root }; -} - -// Set operonSkipMcpApprovals[serverName]=true in Claude-3p/config.json so user -// is not prompted for every tool call. Mirrors mcpToolAccessProvider.setSkipApprovals. -async function writeSkipApprovals(managedServers) { - const cfgPath = path.join(getWriteRoot(), "config.json"); - let cfg = {}; - try { - cfg = JSON.parse(await fs.readFile(cfgPath, "utf-8")) || {}; - } catch (e) { - if (e.code !== "ENOENT") return { error: e.code }; - } - // Reset previous managed entries (those we own == COWORK_PLUGINS server names) - const ownedNames = new Set(); - for (const p of COWORK_PLUGINS) { - for (const s of p.servers) { - ownedNames.add(p.servers.length === 1 ? p.name : `${p.name}-${s.key}`); - } - } - const skip = (cfg.operonSkipMcpApprovals && typeof cfg.operonSkipMcpApprovals === "object") ? cfg.operonSkipMcpApprovals : {}; - for (const k of Object.keys(skip)) { - if (ownedNames.has(k)) delete skip[k]; - } - for (const srv of managedServers) { - if (srv?.name) skip[srv.name] = true; - } - cfg.operonSkipMcpApprovals = skip; - await fs.mkdir(getWriteRoot(), { recursive: true }); - await fs.writeFile(cfgPath, JSON.stringify(cfg, null, 2)); - return { written: Object.keys(skip).length }; -} - -// Candidate user-data roots โ€” Cowork can run from either Claude-3p (3p mode) or Claude (1p mode w/ cowork features) const getCandidateRoots = () => { if (os.platform() === "darwin") { const base = path.join(os.homedir(), "Library", "Application Support"); @@ -136,7 +47,6 @@ const getCandidateRoots = () => { ]; }; -// Claude.app/exe install paths โ€” fallback detect when no user-data folder yet const getAppInstallPaths = () => { if (os.platform() === "darwin") { return ["/Applications/Claude.app", path.join(os.homedir(), "Applications", "Claude.app")]; @@ -153,7 +63,6 @@ const getAppInstallPaths = () => { return []; }; -// For READ: prefer existing configLibrary (any root). For WRITE: always Claude-3p (first candidate). const resolveAppRootForRead = async () => { const candidates = getCandidateRoots(); for (const dir of candidates) { @@ -165,70 +74,55 @@ const resolveAppRootForRead = async () => { return candidates[0]; }; -const getWriteRoot = () => getCandidateRoots()[0]; // always Claude-3p - +const getWriteRoot = () => getCandidateRoots()[0]; const getConfigDir = async () => path.join(await resolveAppRootForRead(), "configLibrary"); const getWriteConfigDir = () => path.join(getWriteRoot(), "configLibrary"); const getMetaPath = async () => path.join(await getConfigDir(), "_meta.json"); const getWriteMetaPath = () => path.join(getWriteConfigDir(), "_meta.json"); -// Locate Claude (1p) folder for claude_desktop_config.json bootstrap const get1pRoot = () => { - if (os.platform() === "darwin") { - return path.join(os.homedir(), "Library", "Application Support", "Claude"); - } + if (os.platform() === "darwin") return path.join(os.homedir(), "Library", "Application Support", "Claude"); if (os.platform() === "win32") { - const localApp = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"); const roaming = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"); - return path.join(roaming, "Claude"); // 1p uses roaming on Win + return path.join(roaming, "Claude"); } return path.join(os.homedir(), ".config", "Claude"); }; -// Set deploymentMode="3p" in Claude/claude_desktop_config.json (preserve existing keys) const bootstrapDeploymentMode = async () => { const cfgPath = path.join(get1pRoot(), "claude_desktop_config.json"); let cfg = {}; try { - const content = await fs.readFile(cfgPath, "utf-8"); - cfg = JSON.parse(content); + cfg = JSON.parse(await fs.readFile(cfgPath, "utf-8")); } catch (error) { if (error.code !== "ENOENT") throw error; } - if (cfg.deploymentMode === "3p") return false; // no change + if (cfg.deploymentMode === "3p") return false; cfg.deploymentMode = "3p"; await fs.mkdir(get1pRoot(), { recursive: true }); await fs.writeFile(cfgPath, JSON.stringify(cfg, null, 2)); return true; }; -// Cowork is available if either (a) any user-data root exists or (b) Claude app is installed const checkInstalled = async () => { for (const dir of [...getCandidateRoots(), ...getAppInstallPaths()]) { - try { - await fs.access(dir); - return true; - } catch { /* try next */ } + try { await fs.access(dir); return true; } catch { /* try next */ } } return false; }; const readJson = async (filePath) => { - try { - const content = await fs.readFile(filePath, "utf-8"); - return JSON.parse(content); - } catch (error) { + try { return JSON.parse(await fs.readFile(filePath, "utf-8")); } + catch (error) { if (error.code === "ENOENT") return null; throw error; } }; -// Ensure meta exists in Claude-3p/configLibrary (write target). If meta already exists in Claude/ (1p), copy appliedId. const ensureMeta = async () => { const writeMetaPath = getWriteMetaPath(); let meta = await readJson(writeMetaPath); if (!meta || !meta.appliedId) { - // Try to inherit from any existing root const existingRead = await readJson(await getMetaPath()); if (existingRead?.appliedId) { meta = existingRead; @@ -242,17 +136,28 @@ const ensureMeta = async () => { return meta; }; +// Auto-skip approvals for every managed server (no per-tool prompts). +async function writeSkipApprovals(managedServers) { + const cfgPath = path.join(getWriteRoot(), "config.json"); + let cfg = {}; + try { cfg = JSON.parse(await fs.readFile(cfgPath, "utf-8")) || {}; } + catch (e) { if (e.code !== "ENOENT") return { error: e.code }; } + const skip = {}; + for (const srv of managedServers) { + if (srv?.name) skip[srv.name] = true; + } + cfg.operonSkipMcpApprovals = skip; + await fs.mkdir(getWriteRoot(), { recursive: true }); + await fs.writeFile(cfgPath, JSON.stringify(cfg, null, 2)); + return { written: Object.keys(skip).length }; +} + export async function GET() { try { const installed = await checkInstalled(); if (!installed) { - return NextResponse.json({ - installed: false, - config: null, - message: "Claude Desktop (Cowork mode) not detected", - }); + return NextResponse.json({ installed: false, config: null, message: "Claude Desktop (Cowork mode) not detected" }); } - const meta = await readJson(await getMetaPath()); const appliedId = meta?.appliedId || null; const configDir = await getConfigDir(); @@ -263,13 +168,7 @@ export async function GET() { const models = Array.isArray(config?.inferenceModels) ? config.inferenceModels.map((m) => (typeof m === "string" ? m : m?.name)).filter(Boolean) : []; - - // managedMcpServers stored as native array in configLibrary .json - const managedMcpArr = Array.isArray(config?.managedMcpServers) ? config.managedMcpServers : []; - const selectedPlugins = COWORK_PLUGINS - .filter((p) => p.servers.some((s) => managedMcpArr.some((v) => v?.url === s.url))) - .map((p) => p.name); - + const managedMcp = Array.isArray(config?.managedMcpServers) ? config.managedMcpServers : []; const has9Router = !!(config?.inferenceProvider === PROVIDER && baseUrl); return NextResponse.json({ @@ -282,9 +181,23 @@ export async function GET() { baseUrl, models, provider: config?.inferenceProvider || null, - selectedPlugins, + plugins: managedMcp.map((m) => { + // Strip "{name}-" prefix and dedupe so re-applies don't multiply entries. + const keys = m.toolPolicy ? Object.keys(m.toolPolicy) : []; + const prefix = `${m.name}-`; + const bare = new Set(); + for (const k of keys) { + let t = k; + while (t.startsWith(prefix)) t = t.slice(prefix.length); + bare.add(t); + } + // If plugin matches a default, prefer default toolNames (curated/correct). + const def = DEFAULT_PLUGINS.find((d) => d.name === m.name); + const toolNames = def && Array.isArray(def.toolNames) ? def.toolNames : Array.from(bare); + return { name: m.name, url: m.url, transport: m.transport, oauth: !!m.oauth, toolNames }; + }), }, - availablePlugins: COWORK_PLUGINS.map((p) => ({ name: p.name, description: p.description })), + defaultPlugins: DEFAULT_PLUGINS, }); } catch (error) { console.log("Error reading cowork settings:", error); @@ -299,13 +212,13 @@ export async function POST(request) { if (!baseUrl || !apiKey) { return NextResponse.json({ error: "baseUrl and apiKey are required" }, { status: 400 }); } - const modelsArray = Array.isArray(models) ? models.filter((m) => typeof m === "string" && m.trim()) : []; if (modelsArray.length === 0) { return NextResponse.json({ error: "At least one model is required" }, { status: 400 }); } - const pluginsArray = Array.isArray(plugins) ? plugins.filter((p) => typeof p === "string") : []; + // Plugins: array of {name, url, transport?, oauth?}. Default to DEFAULT_PLUGINS if absent. + const pluginsArray = Array.isArray(plugins) && plugins.length > 0 ? plugins : DEFAULT_PLUGINS; const managedMcpServers = buildManagedMcpServers(pluginsArray); const bootstrapped = await bootstrapDeploymentMode(); @@ -313,22 +226,16 @@ export async function POST(request) { const configPath = path.join(getWriteConfigDir(), `${meta.appliedId}.json`); const newConfig = { + ...SECURITY_RELAX, inferenceProvider: PROVIDER, inferenceGatewayBaseUrl: baseUrl, inferenceGatewayApiKey: apiKey, inferenceModels: modelsArray.map((name) => ({ name })), - isLocalDevMcpEnabled: true, - isDesktopExtensionEnabled: true, }; - if (managedMcpServers.length > 0) { - newConfig.managedMcpServers = managedMcpServers; - } + if (managedMcpServers.length > 0) newConfig.managedMcpServers = managedMcpServers; await fs.writeFile(configPath, JSON.stringify(newConfig, null, 2)); - // Plugin folder mount (best-effort, doesn't fail the request) - const pluginsResult = await writeOrgPluginsFolder(pluginsArray); - // Auto-skip approvals for managed servers let skipResult = null; try { skipResult = await writeSkipApprovals(managedMcpServers); } catch (e) { skipResult = { error: e.message }; } @@ -339,7 +246,6 @@ export async function POST(request) { ? "Cowork enabled (3p mode set). Quit & reopen Claude Desktop." : "Cowork settings applied. Quit & reopen Claude Desktop.", configPath, - plugins: pluginsResult, skipApprovals: skipResult, }); } catch (error) { @@ -355,12 +261,8 @@ export async function DELETE() { return NextResponse.json({ success: true, message: "No active config to reset" }); } const configPath = path.join(await getConfigDir(), `${meta.appliedId}.json`); - try { - await fs.writeFile(configPath, JSON.stringify({}, null, 2)); - } catch (error) { - if (error.code !== "ENOENT") throw error; - } - await writeOrgPluginsFolder([]); + try { await fs.writeFile(configPath, JSON.stringify({}, null, 2)); } + catch (error) { if (error.code !== "ENOENT") throw error; } try { await writeSkipApprovals([]); } catch { /* ignore */ } return NextResponse.json({ success: true, message: "Cowork config reset" }); } catch (error) { diff --git a/src/shared/components/McpMarketplaceModal.js b/src/shared/components/McpMarketplaceModal.js new file mode 100644 index 0000000..5b18dfb --- /dev/null +++ b/src/shared/components/McpMarketplaceModal.js @@ -0,0 +1,255 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import Modal from "./Modal"; + +const REGISTRY_ENDPOINT = "/api/cli-tools/cowork-mcp-registry"; +const TOOLS_ENDPOINT = "/api/cli-tools/cowork-mcp-tools"; + +export default function McpMarketplaceModal({ isOpen, onClose, onAdd, addedNames = [] }) { + const [servers, setServers] = useState([]); + const [loading, setLoading] = useState(false); + const [search, setSearch] = useState(""); + const [filter, setFilter] = useState("all"); + const [error, setError] = useState(null); + const [expandedUrl, setExpandedUrl] = useState(null); + const [toolsCache, setToolsCache] = useState({}); + const [toolsLoading, setToolsLoading] = useState({}); + const [toolSelection, setToolSelection] = useState({}); + + useEffect(() => { + if (!isOpen) return; + if (servers.length > 0) return; + setLoading(true); + fetch(REGISTRY_ENDPOINT) + .then((r) => r.json()) + .then((d) => { + if (d.error) setError(d.error); + else setServers(d.servers || []); + }) + .catch((e) => setError(e.message)) + .finally(() => setLoading(false)); + }, [isOpen]); + + const addedSet = useMemo(() => new Set(addedNames), [addedNames]); + + const filtered = useMemo(() => { + const q = search.trim().toLowerCase(); + return servers.filter((s) => { + if (filter === "authless" && s.oauth) return false; + if (filter === "oauth" && !s.oauth) return false; + if (!q) return true; + return ( + (s.title || "").toLowerCase().includes(q) || + (s.description || "").toLowerCase().includes(q) || + (s.name || "").toLowerCase().includes(q) + ); + }); + }, [servers, search, filter]); + + const fetchTools = async (server) => { + if (toolsCache[server.url]) return; + setToolsLoading((p) => ({ ...p, [server.url]: true })); + try { + const r = await fetch(TOOLS_ENDPOINT, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url: server.url }), + }); + const d = await r.json(); + const tools = d.tools || []; + const fallback = Array.isArray(server.toolNames) ? server.toolNames : []; + const toolNames = tools.length > 0 ? tools.map((t) => t.name) : fallback; + setToolsCache((p) => ({ ...p, [server.url]: { tools, requiresAuth: !!d.requiresAuth, error: d.error } })); + // Default: all checked + setToolSelection((p) => ({ ...p, [server.url]: Object.fromEntries(toolNames.map((t) => [t, true])) })); + } catch (e) { + setToolsCache((p) => ({ ...p, [server.url]: { tools: [], error: e.message } })); + } finally { + setToolsLoading((p) => ({ ...p, [server.url]: false })); + } + }; + + const expandServer = (server) => { + if (expandedUrl === server.url) { + setExpandedUrl(null); + return; + } + setExpandedUrl(server.url); + fetchTools(server); + }; + + const toggleTool = (url, tool) => { + setToolSelection((prev) => ({ ...prev, [url]: { ...prev[url], [tool]: !prev[url]?.[tool] } })); + }; + + const setAllTools = (url, value) => { + const sel = toolSelection[url] || {}; + setToolSelection((prev) => ({ ...prev, [url]: Object.fromEntries(Object.keys(sel).map((t) => [t, value])) })); + }; + + const confirmAdd = (server) => { + const sel = toolSelection[server.url] || {}; + const enabled = Object.keys(sel).filter((t) => sel[t]); + onAdd?.({ + name: server.slug || server.name, + title: server.title, + description: server.description, + url: server.url, + transport: server.transport, + oauth: server.oauth, + toolNames: enabled, + }); + setExpandedUrl(null); + }; + + return ( + +
+
+ setSearch(e.target.value)} + placeholder="Search by name or description..." + className="flex-1 px-2 py-1.5 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50" + /> + +
+ + {error && ( +
{error}
+ )} + + {loading && ( +
+ progress_activity + Loading registry... +
+ )} + + {!loading && ( +
+ {filtered.length === 0 && ( +
No servers match filter
+ )} + {filtered.map((s) => { + const added = addedSet.has(s.slug || s.name); + const expanded = expandedUrl === s.url; + const cache = toolsCache[s.url]; + const isLoadingTools = toolsLoading[s.url]; + const sel = toolSelection[s.url] || {}; + const toolKeys = Object.keys(sel); + const selectedCount = Object.values(sel).filter(Boolean).length; + return ( +
+
+ {s.iconUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + { e.target.style.display = "none"; }} /> + ) : ( +
+ )} +
+
+ {s.title} + {s.oauth ? ( + OAuth + ) : ( + Authless + )} + {s.toolCount > 0 && ( + {s.toolCount} tools + )} +
+ {s.description && ( +

{s.description}

+ )} +
+ +
+ {expanded && ( +
+ {isLoadingTools && ( +
+ progress_activity + Probing server for tools... +
+ )} + {!isLoadingTools && cache?.requiresAuth && ( +

+ ๐Ÿ” OAuth required. Add now and authenticate after Apply; tool list will be discovered after first connect. +

+ )} + {!isLoadingTools && cache?.error && !cache?.requiresAuth && ( +

Probe failed: {cache.error}

+ )} + {!isLoadingTools && toolKeys.length === 0 && !cache?.requiresAuth && !cache?.error && ( +

No tools advertised by server.

+ )} + {!isLoadingTools && toolKeys.length > 0 && ( + <> +
+ {selectedCount}/{toolKeys.length} tools enabled +
+ + ยท + +
+
+
+ {toolKeys.map((t) => ( + + ))} +
+ + )} + +
+ )} +
+ ); + })} +
+ )} + +
+ {filtered.length} of {servers.length} servers +
+
+ + ); +} diff --git a/src/shared/components/index.js b/src/shared/components/index.js index b7e7dcc..af8ab32 100644 --- a/src/shared/components/index.js +++ b/src/shared/components/index.js @@ -17,6 +17,7 @@ export { default as OAuthModal } from "./OAuthModal"; export { default as ModelSelectModal } from "./ModelSelectModal"; export { default as ManualConfigModal } from "./ManualConfigModal"; export { default as ComboFormModal } from "./ComboFormModal"; +export { default as McpMarketplaceModal } from "./McpMarketplaceModal"; export { default as UsageStats } from "./UsageStats"; export { default as LanguageSwitcher } from "./LanguageSwitcher"; export { default as NineRemoteButton } from "./NineRemoteButton"; diff --git a/src/shared/constants/cliTools.js b/src/shared/constants/cliTools.js index 71d6e39..220829c 100644 --- a/src/shared/constants/cliTools.js +++ b/src/shared/constants/cliTools.js @@ -100,7 +100,7 @@ export const CLI_TOOLS = { }, codex: { id: "codex", - name: "OpenAI Codex CLI", + name: "OpenAI Codex CLI / App", image: "/providers/codex.png", color: "#10A37F", description: "OpenAI Codex CLI", diff --git a/src/shared/constants/coworkPlugins.js b/src/shared/constants/coworkPlugins.js index b67d1ab..1e09389 100644 --- a/src/shared/constants/coworkPlugins.js +++ b/src/shared/constants/coworkPlugins.js @@ -1,955 +1,69 @@ -// Cowork plugins extracted from anthropics/knowledge-work-plugins marketplace. -// Used to inject managedMcpServers into Claude Cowork (3p mode) configLibrary entries. - -const COWORK_PLUGINS = [ +// Default plugins auto-installed for Claude Cowork (3p mode). +// Exa works without auth; Tavily uses OAuth (DCR auto-flow). +const DEFAULT_PLUGINS = [ { - "name": "tavily", - "description": "Tavily - Real-time web search API optimized for LLM agents. Search and extract content from the web.", - "servers": [ - { - "key": "tavily", - "url": "https://mcp.tavily.com/mcp", - "type": "http" - } - ] + name: "exa", + title: "Exa", + description: "Real-time web search and code documentation", + url: "https://mcp.exa.ai/mcp", + transport: "http", + oauth: false, + toolNames: ["web_search_exa", "web_fetch_exa"], }, { - "name": "lseg", - "description": "Price bonds, analyze yield curves, evaluate FX carry trades, value options, and build macro dashboards using LSEG financial data and analytics.", - "servers": [ - { - "key": "lseg", - "url": "https://api.analytics.lseg.com/lfa/mcp", - "type": "http" - } - ] + name: "tavily", + title: "Tavily", + description: "Real-time web search optimized for LLM agents", + url: "https://mcp.tavily.com/mcp", + transport: "http", + oauth: true, + toolNames: ["tavily_search", "tavily_extract", "tavily_crawl", "tavily_map"], }, - { - "name": "sp-global", - "description": "S&P Global - Financial data and analytics skills including company tearsheets, earnings previews, and transaction summaries", - "servers": [ - { - "key": "spglobal", - "url": "https://kfinance.kensho.com/integrations/mcp", - "type": "http" - } - ] - }, - { - "name": "adobe-for-creativity", - "description": "Brings together Adobe Creative Cloud tools for images, vectors, design, and video. Edit multiple assets at once, adapt for different platforms, and complete multi-step creative workflows for polished ", - "servers": [ - { - "key": "Adobe for creativity", - "url": "https://adobe-creativity.adobe.io/mcp", - "type": "http" - } - ] - }, - { - "name": "figma", - "description": "Figma design platform integration. Access design files, extract component information, read design tokens, and translate designs into code. Bridge the gap between design and development workflows.", - "servers": [ - { - "key": "figma", - "url": "https://mcp.figma.com/mcp", - "type": "http" - } - ] - }, - { - "name": "atlan", - "description": "Atlan data catalog plugin for Claude Code. Search, explore, govern, and manage your data assets through natural language. Powered by the Atlan MCP server with semantic search, lineage traversal, gloss", - "servers": [ - { - "key": "atlan", - "url": "https://mcp.atlan.com/mcp", - "type": "http" - } - ] - }, - { - "name": "cloudinary", - "description": "Use Cloudinary directly in Claude. Manage assets, apply transformations, optimize media, and more through natural conversation.", - "servers": [ - { - "key": "cloudinary-asset-mgmt", - "url": "https://asset-management.mcp.cloudinary.com/mcp", - "type": "http" - }, - { - "key": "cloudinary-env-config", - "url": "https://environment-config.mcp.cloudinary.com/mcp", - "type": "http" - }, - { - "key": "cloudinary-smd", - "url": "https://structured-metadata.mcp.cloudinary.com/mcp", - "type": "http" - }, - { - "key": "cloudinary-analysis", - "url": "https://analysis.mcp.cloudinary.com/sse", - "type": "http" - }, - { - "key": "cloudinary-mediaflows", - "url": "https://mediaflows.mcp.cloudinary.com/v2/mcp", - "type": "http" - } - ] - }, - { - "name": "prisma", - "description": "Prisma MCP integration for Postgres database management, schema migrations, SQL queries, and connection string management. Provision Prisma Postgres databases, run migrations, and interact with your d", - "servers": [ - { - "key": "Prisma-Remote", - "url": "https://mcp.prisma.io/mcp", - "type": "http" - } - ] - }, - { - "name": "cockroachdb", - "description": "CockroachDB plugin for Claude Code โ€” explore schemas, write optimized SQL, debug queries, and manage distributed database clusters directly from your AI coding agent.", - "servers": [ - { - "key": "cockroachdb-toolbox-http", - "url": "http://127.0.0.1:5000/mcp", - "type": "http" - }, - { - "key": "cockroachdb-cloud", - "url": "https://cockroachlabs.cloud/mcp", - "type": "http" - } - ] - }, - { - "name": "daloopa", - "description": "Financial analysis skills powered by Daloopa's institutional-grade data", - "servers": [ - { - "key": "daloopa", - "url": "https://mcp.daloopa.com/server/mcp", - "type": "http" - }, - { - "key": "daloopa-docs", - "url": "https://docs.daloopa.com/mcp", - "type": "http" - } - ] - }, - { - "name": "intercom", - "description": "Intercom integration for Claude Code. Search conversations, analyze customer support patterns, look up contacts and companies, and install the Intercom Messenger. Connect your Intercom workspace to ge", - "servers": [ - { - "key": "intercom", - "url": "https://mcp.intercom.com/mcp", - "type": "http" - } - ] - }, - { - "name": "zoominfo", - "description": "Search companies and contacts, enrich leads, find lookalikes, and get AI-ranked contact recommendations. Pre-built skills chain multiple ZoomInfo tools into complete B2B sales workflows.", - "servers": [ - { - "key": "zoominfo", - "url": "https://mcp.zoominfo.com/mcp", - "type": "http" - } - ] - }, - { - "name": "sanity-plugin", - "description": "Sanity content platform integration with MCP server, agent skills, and slash commands. Query and author content, build and optimize GROQ queries, design schemas, and set up Visual Editing.", - "servers": [ - { - "key": "Sanity", - "url": "https://mcp.sanity.io", - "type": "http" - } - ] - }, - { - "name": "adspirer-ads-agent", - "description": "Cross-platform ad management for Google Ads, Meta Ads, TikTok Ads, and LinkedIn Ads. 91 tools for keyword research, campaign creation, performance analysis, and budget optimization.", - "servers": [ - { - "key": "adspirer", - "url": "https://mcp.adspirer.com/mcp", - "type": "http" - } - ] - }, - { - "name": "planetscale", - "description": "An authenticated hosted MCP server that accesses your PlanetScale organizations, databases, branches, schema, and Insights data. Query against your data, surface slow queries, and get organizational a", - "servers": [ - { - "key": "planetscale", - "url": "https://mcp.pscale.dev/mcp/planetscale", - "type": "http" - } - ] - }, - { - "name": "miro", - "description": "Secure access to Miro boards. Enables AI to read board context, create diagrams, and generate code with enterprise-grade security.", - "servers": [ - { - "key": "miro", - "url": "https://mcp.miro.com/", - "type": "http" - } - ] - }, - { - "name": "zoom-plugin", - "description": "Plan, build, and debug Zoom integrations across REST APIs, Meeting SDK, Video SDK, webhooks, bots, and MCP workflows. Search meetings, retrieve recordings, access transcripts, and design AI-powered Zo", - "servers": [ - { - "key": "zoom-mcp", - "url": "https://mcp-us.zoom.us/mcp/zoom/streamable", - "type": "http" - }, - { - "key": "zoom-docs-mcp", - "url": "https://mcp.zoom.us/mcp/docs/streamable", - "type": "http" - }, - { - "key": "zoom-whiteboard-mcp", - "url": "https://mcp-us.zoom.us/mcp/whiteboard/streamable", - "type": "http" - } - ] - }, - { - "name": "bigdata-com", - "description": "Official Bigdata.com plugin providing financial research, analytics, and intelligence tools powered by Bigdata MCP.", - "servers": [ - { - "key": "bigdata.com", - "url": "https://mcp.bigdata.com", - "type": "http" - } - ] - }, - { - "name": "operations", - "description": "Optimize business operations โ€” vendor management, process documentation, change management, capacity planning, and compliance tracking. Keep your organization running efficiently.", - "servers": [ - { - "key": "slack", - "url": "https://mcp.slack.com/mcp", - "type": "http" - }, - { - "key": "notion", - "url": "https://mcp.notion.com/mcp", - "type": "http" - }, - { - "key": "atlassian", - "url": "https://mcp.atlassian.com/v1/mcp", - "type": "http" - }, - { - "key": "asana", - "url": "https://mcp.asana.com/v2/mcp", - "type": "http" - }, - { - "key": "servicenow", - "url": "https://mcp.servicenow.com/mcp", - "type": "http" - }, - { - "key": "ms365", - "url": "https://microsoft365.mcp.claude.com/mcp", - "type": "http" - } - ] - }, - { - "name": "brand-voice", - "description": "Discover your brand voice from existing documents and conversations, generate enforceable guidelines, and validate AI-generated content against your established tone and positioning.", - "servers": [ - { - "key": "notion", - "url": "https://mcp.notion.com/mcp", - "type": "http" - }, - { - "key": "atlassian", - "url": "https://mcp.atlassian.com/v1/mcp", - "type": "http" - }, - { - "key": "box", - "url": "https://mcp.box.com", - "type": "http" - }, - { - "key": "figma", - "url": "https://mcp.figma.com/mcp", - "type": "http" - }, - { - "key": "gong", - "url": "https://mcp.gong.io/mcp", - "type": "http" - }, - { - "key": "microsoft-365", - "url": "https://microsoft365.mcp.claude.com/mcp", - "type": "http" - }, - { - "key": "granola", - "url": "https://mcp.granola.ai/mcp", - "type": "http" - } - ] - }, - { - "name": "human-resources", - "description": "Streamline people operations โ€” recruiting, onboarding, performance reviews, compensation analysis, and policy guidance. Maintain compliance and keep your team running smoothly.", - "servers": [ - { - "key": "slack", - "url": "https://mcp.slack.com/mcp", - "type": "http" - }, - { - "key": "notion", - "url": "https://mcp.notion.com/mcp", - "type": "http" - }, - { - "key": "atlassian", - "url": "https://mcp.atlassian.com/v1/mcp", - "type": "http" - }, - { - "key": "ms365", - "url": "https://microsoft365.mcp.claude.com/mcp", - "type": "http" - } - ] - }, - { - "name": "design", - "description": "Accelerate design workflows โ€” critique, design system management, UX writing, accessibility audits, research synthesis, and dev handoff. From exploration to pixel-perfect specs.", - "servers": [ - { - "key": "slack", - "url": "https://mcp.slack.com/mcp", - "type": "http" - }, - { - "key": "figma", - "url": "https://mcp.figma.com/mcp", - "type": "http" - }, - { - "key": "linear", - "url": "https://mcp.linear.app/mcp", - "type": "http" - }, - { - "key": "asana", - "url": "https://mcp.asana.com/v2/mcp", - "type": "http" - }, - { - "key": "atlassian", - "url": "https://mcp.atlassian.com/v1/mcp", - "type": "http" - }, - { - "key": "notion", - "url": "https://mcp.notion.com/mcp", - "type": "http" - }, - { - "key": "intercom", - "url": "https://mcp.intercom.com/mcp", - "type": "http" - } - ] - }, - { - "name": "engineering", - "description": "Streamline engineering workflows โ€” standups, code review, architecture decisions, incident response, and technical documentation. Works with your existing tools or standalone.", - "servers": [ - { - "key": "slack", - "url": "https://mcp.slack.com/mcp", - "type": "http" - }, - { - "key": "linear", - "url": "https://mcp.linear.app/mcp", - "type": "http" - }, - { - "key": "asana", - "url": "https://mcp.asana.com/v2/mcp", - "type": "http" - }, - { - "key": "atlassian", - "url": "https://mcp.atlassian.com/v1/mcp", - "type": "http" - }, - { - "key": "notion", - "url": "https://mcp.notion.com/mcp", - "type": "http" - }, - { - "key": "github", - "url": "https://api.githubcopilot.com/mcp/", - "type": "http" - }, - { - "key": "pagerduty", - "url": "https://mcp.pagerduty.com/mcp", - "type": "http" - }, - { - "key": "datadog", - "url": "https://mcp.datadoghq.com/mcp", - "type": "http" - } - ] - }, - { - "name": "common-room", - "description": "Turn Common Room into your GTM copilot. Research accounts and contacts, prep for calls with attendee profiles and talking points, and draft personalized outreach across email, LinkedIn, and phone.", - "servers": [ - { - "key": "common-room", - "url": "https://mcp.commonroom.io/mcp", - "type": "http" - } - ] - }, - { - "name": "apollo", - "description": "Prospect, enrich leads, and load outreach sequences with Apollo.io โ€” one-click MCP server integration for Claude Code and Cowork.", - "servers": [ - { - "key": "apollo", - "url": "https://mcp.apollo.io/mcp", - "type": "http" - } - ] - }, - { - "name": "slack-by-salesforce", - "description": "Slack integration for searching messages, sending communications, managing canvases, and more", - "servers": [ - { - "key": "slack", - "url": "https://mcp.slack.com/mcp", - "type": "http" - } - ] - }, - { - "name": "bio-research", - "description": "Connect to preclinical research tools and databases (literature search, genomics analysis, target prioritization) to accelerate early-stage life sciences R&D", - "servers": [ - { - "key": "pubmed", - "url": "https://pubmed.mcp.claude.com/mcp", - "type": "http" - }, - { - "key": "biorender", - "url": "https://mcp.services.biorender.com/mcp", - "type": "http" - }, - { - "key": "biorxiv", - "url": "https://mcp.deepsense.ai/biorxiv/mcp", - "type": "http" - }, - { - "key": "c-trials", - "url": "https://mcp.deepsense.ai/clinical_trials/mcp", - "type": "http" - }, - { - "key": "chembl", - "url": "https://mcp.deepsense.ai/chembl/mcp", - "type": "http" - }, - { - "key": "synapse", - "url": "https://mcp.synapse.org/mcp", - "type": "http" - }, - { - "key": "wiley", - "url": "https://connector.scholargateway.ai/mcp", - "type": "http" - }, - { - "key": "owkin", - "url": "https://mcp.k.owkin.com/mcp", - "type": "http" - }, - { - "key": "ot", - "url": "https://mcp.platform.opentargets.org/mcp", - "type": "http" - } - ] - }, - { - "name": "sales", - "description": "Prospect, craft outreach, and build deal strategy faster. Prep for calls, manage your pipeline, and write personalized messaging that moves deals forward.", - "servers": [ - { - "key": "slack", - "url": "https://mcp.slack.com/mcp", - "type": "http" - }, - { - "key": "hubspot", - "url": "https://mcp.hubspot.com/anthropic", - "type": "http" - }, - { - "key": "close", - "url": "https://mcp.close.com/mcp", - "type": "http" - }, - { - "key": "clay", - "url": "https://api.clay.com/v3/mcp", - "type": "http" - }, - { - "key": "zoominfo", - "url": "https://mcp.zoominfo.com/mcp", - "type": "http" - }, - { - "key": "notion", - "url": "https://mcp.notion.com/mcp", - "type": "http" - }, - { - "key": "atlassian", - "url": "https://mcp.atlassian.com/v1/mcp", - "type": "http" - }, - { - "key": "fireflies", - "url": "https://api.fireflies.ai/mcp", - "type": "http" - }, - { - "key": "ms365", - "url": "https://microsoft365.mcp.claude.com/mcp", - "type": "http" - }, - { - "key": "apollo", - "url": "https://api.apollo.io/mcp", - "type": "http" - }, - { - "key": "outreach", - "url": "https://mcp.outreach.io/mcp", - "type": "http" - }, - { - "key": "similarweb", - "url": "https://mcp.similarweb.com/mcp", - "type": "http" - } - ] - }, - { - "name": "legal", - "description": "Speed up contract review, NDA triage, and compliance workflows for in-house legal teams. Draft legal briefs, organize precedent research, and manage institutional knowledge.", - "servers": [ - { - "key": "slack", - "url": "https://mcp.slack.com/mcp", - "type": "http" - }, - { - "key": "box", - "url": "https://mcp.box.com", - "type": "http" - }, - { - "key": "egnyte", - "url": "https://mcp-server.egnyte.com/mcp", - "type": "http" - }, - { - "key": "atlassian", - "url": "https://mcp.atlassian.com/v1/mcp", - "type": "http" - }, - { - "key": "ms365", - "url": "https://microsoft365.mcp.claude.com/mcp", - "type": "http" - }, - { - "key": "docusign", - "url": "https://mcp.docusign.com/mcp", - "type": "http" - } - ] - }, - { - "name": "product-management", - "description": "Write feature specs, plan roadmaps, and synthesize user research faster. Keep stakeholders updated and stay ahead of the competitive landscape.", - "servers": [ - { - "key": "slack", - "url": "https://mcp.slack.com/mcp", - "type": "http" - }, - { - "key": "linear", - "url": "https://mcp.linear.app/mcp", - "type": "http" - }, - { - "key": "asana", - "url": "https://mcp.asana.com/v2/mcp", - "type": "http" - }, - { - "key": "monday", - "url": "https://mcp.monday.com/mcp", - "type": "http" - }, - { - "key": "clickup", - "url": "https://mcp.clickup.com/mcp", - "type": "http" - }, - { - "key": "atlassian", - "url": "https://mcp.atlassian.com/v1/mcp", - "type": "http" - }, - { - "key": "notion", - "url": "https://mcp.notion.com/mcp", - "type": "http" - }, - { - "key": "figma", - "url": "https://mcp.figma.com/mcp", - "type": "http" - }, - { - "key": "amplitude", - "url": "https://mcp.amplitude.com/mcp", - "type": "http" - }, - { - "key": "amplitude-eu", - "url": "https://mcp.eu.amplitude.com/mcp", - "type": "http" - }, - { - "key": "pendo", - "url": "https://app.pendo.io/mcp/v0/shttp", - "type": "http" - }, - { - "key": "intercom", - "url": "https://mcp.intercom.com/mcp", - "type": "http" - }, - { - "key": "fireflies", - "url": "https://api.fireflies.ai/mcp", - "type": "http" - }, - { - "key": "similarweb", - "url": "https://mcp.similarweb.com/mcp", - "type": "http" - } - ] - }, - { - "name": "productivity", - "description": "Manage tasks, plan your day, and build up memory of important context about your work. Syncs with your calendar, email, and chat to keep everything organized and on track.", - "servers": [ - { - "key": "slack", - "url": "https://mcp.slack.com/mcp", - "type": "http" - }, - { - "key": "notion", - "url": "https://mcp.notion.com/mcp", - "type": "http" - }, - { - "key": "asana", - "url": "https://mcp.asana.com/v2/mcp", - "type": "http" - }, - { - "key": "linear", - "url": "https://mcp.linear.app/mcp", - "type": "http" - }, - { - "key": "atlassian", - "url": "https://mcp.atlassian.com/v1/mcp", - "type": "http" - }, - { - "key": "ms365", - "url": "https://microsoft365.mcp.claude.com/mcp", - "type": "http" - }, - { - "key": "monday", - "url": "https://mcp.monday.com/mcp", - "type": "http" - }, - { - "key": "clickup", - "url": "https://mcp.clickup.com/mcp", - "type": "http" - } - ] - }, - { - "name": "marketing", - "description": "Create content, plan campaigns, and analyze performance across marketing channels. Maintain brand voice consistency, track competitors, and report on what's working.", - "servers": [ - { - "key": "slack", - "url": "https://mcp.slack.com/mcp", - "type": "http" - }, - { - "key": "canva", - "url": "https://mcp.canva.com/mcp", - "type": "http" - }, - { - "key": "figma", - "url": "https://mcp.figma.com/mcp", - "type": "http" - }, - { - "key": "hubspot", - "url": "https://mcp.hubspot.com/anthropic", - "type": "http" - }, - { - "key": "amplitude", - "url": "https://mcp.amplitude.com/mcp", - "type": "http" - }, - { - "key": "amplitude-eu", - "url": "https://mcp.eu.amplitude.com/mcp", - "type": "http" - }, - { - "key": "notion", - "url": "https://mcp.notion.com/mcp", - "type": "http" - }, - { - "key": "ahrefs", - "url": "https://api.ahrefs.com/mcp/mcp", - "type": "http" - }, - { - "key": "similarweb", - "url": "https://mcp.similarweb.com", - "type": "http" - }, - { - "key": "klaviyo", - "url": "https://mcp.klaviyo.com/mcp", - "type": "http" - }, - { - "key": "supermetrics", - "url": "https://mcp.supermetrics.com/mcp", - "type": "http" - } - ] - }, - { - "name": "finance", - "description": "Streamline finance and accounting workflows, from journal entries and reconciliation to financial statements and variance analysis. Speed up audit prep, month-end close, and keeping your books clean.", - "servers": [ - { - "key": "bigquery", - "url": "https://bigquery.googleapis.com/mcp", - "type": "http" - }, - { - "key": "slack", - "url": "https://mcp.slack.com/mcp", - "type": "http" - }, - { - "key": "ms365", - "url": "https://microsoft365.mcp.claude.com/mcp", - "type": "http" - } - ] - }, - { - "name": "enterprise-search", - "description": "Search across all of your company's tools in one place. Find anything across email, chat, documents, and wikis without switching between apps.", - "servers": [ - { - "key": "slack", - "url": "https://mcp.slack.com/mcp", - "type": "http" - }, - { - "key": "notion", - "url": "https://mcp.notion.com/mcp", - "type": "http" - }, - { - "key": "guru", - "url": "https://mcp.api.getguru.com/mcp", - "type": "http" - }, - { - "key": "atlassian", - "url": "https://mcp.atlassian.com/v1/mcp", - "type": "http" - }, - { - "key": "asana", - "url": "https://mcp.asana.com/v2/mcp", - "type": "http" - }, - { - "key": "ms365", - "url": "https://microsoft365.mcp.claude.com/mcp", - "type": "http" - } - ] - }, - { - "name": "data", - "description": "Write SQL, explore datasets, and generate insights faster. Build visualizations and dashboards, and turn raw data into clear stories for stakeholders.", - "servers": [ - { - "key": "bigquery", - "url": "https://bigquery.googleapis.com/mcp", - "type": "http" - }, - { - "key": "hex", - "url": "https://app.hex.tech/mcp", - "type": "http" - }, - { - "key": "amplitude", - "url": "https://mcp.amplitude.com/mcp", - "type": "http" - }, - { - "key": "amplitude-eu", - "url": "https://mcp.eu.amplitude.com/mcp", - "type": "http" - }, - { - "key": "atlassian", - "url": "https://mcp.atlassian.com/v1/mcp", - "type": "http" - }, - { - "key": "definite", - "url": "https://api.definite.app/v3/mcp/http", - "type": "http" - } - ] - }, - { - "name": "customer-support", - "description": "Triage tickets, draft responses, escalate issues, and build your knowledge base. Research customer context and turn resolved issues into self-service content.", - "servers": [ - { - "key": "slack", - "url": "https://mcp.slack.com/mcp", - "type": "http" - }, - { - "key": "intercom", - "url": "https://mcp.intercom.com/mcp", - "type": "http" - }, - { - "key": "hubspot", - "url": "https://mcp.hubspot.com/anthropic", - "type": "http" - }, - { - "key": "guru", - "url": "https://mcp.api.getguru.com/mcp", - "type": "http" - }, - { - "key": "atlassian", - "url": "https://mcp.atlassian.com/v1/mcp", - "type": "http" - }, - { - "key": "notion", - "url": "https://mcp.notion.com/mcp", - "type": "http" - }, - { - "key": "ms365", - "url": "https://microsoft365.mcp.claude.com/mcp", - "type": "http" - } - ] - } ]; -// Build managedMcpServers ARRAY (Anthropic schema) from selected plugin names. -// Schema: [{name, url, transport: "http"|"sse", oauth?: true}] -// Most enterprise SaaS MCPs require OAuth โ†’ enable PKCE auto-flow. -function buildManagedMcpServers(selectedPluginNames) { - const set = new Set(selectedPluginNames || []); +// Build managedMcpServers entries from plugin objects. +// Schema: [{name, url, transport, oauth?, toolPolicy?}] +// toolPolicy maps each tool to "allow" so Claude doesn't prompt. +// Plugin name that's force-installed regardless of user selection. +const ALWAYS_ON = "exa"; + +function buildManagedMcpServers(plugins) { + const list = Array.isArray(plugins) ? plugins : []; + // Force Exa always-on at the front; drop any duplicate from user list. + const exaDefault = DEFAULT_PLUGINS.find((p) => p.name === ALWAYS_ON); + const merged = exaDefault ? [exaDefault, ...list.filter((p) => p?.name !== ALWAYS_ON)] : list; const out = []; - for (const p of COWORK_PLUGINS) { - if (!set.has(p.name)) continue; - for (const s of p.servers) { - const name = p.servers.length === 1 ? p.name : `${p.name}-${s.key}`; - const transport = /\/sse(\b|\/)/i.test(s.url) ? "sse" : "http"; - out.push({ name, url: s.url, transport, oauth: true }); + const seen = new Set(); + for (const p of merged) { + if (!p?.name || !p?.url || seen.has(p.name)) continue; + seen.add(p.name); + const entry = { + name: p.name, + url: p.url, + transport: p.transport || (/\/sse(\b|\/)/i.test(p.url) ? "sse" : "http"), + }; + if (p.oauth) entry.oauth = true; + if (Array.isArray(p.toolNames) && p.toolNames.length > 0) { + // Strip any pre-existing "{name}-" prefixes (idempotent across re-applies), + // then emit both bare + single-prefixed variants to match runtime tool naming. + const prefix = `${p.name}-`; + const bare = new Set(); + for (const raw of p.toolNames) { + if (typeof raw !== "string" || !raw) continue; + let t = raw; + while (t.startsWith(prefix)) t = t.slice(prefix.length); + bare.add(t); + } + const policy = {}; + for (const t of bare) { + policy[t] = "allow"; + policy[`${prefix}${t}`] = "allow"; + } + entry.toolPolicy = policy; } + out.push(entry); } return out; } -module.exports = { COWORK_PLUGINS, buildManagedMcpServers }; +module.exports = { DEFAULT_PLUGINS, buildManagedMcpServers, ALWAYS_ON };