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}
-
+
setMarketplaceOpen(true)} className="self-start px-2 py-1 rounded border text-xs bg-primary/10 border-primary/40 text-primary hover:bg-primary/20 cursor-pointer">
+ + Browse MCP marketplace
+
+
+ ๐ก 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}
+ )}
+
+
added ? null : expandServer(s)}
+ disabled={added}
+ className={`shrink-0 px-2 py-1 rounded text-[10px] font-medium transition-colors ${
+ added
+ ? "bg-green-500/10 text-green-600 cursor-default"
+ : expanded
+ ? "bg-surface border border-border text-text-muted hover:bg-black/5"
+ : "bg-primary/10 border border-primary/40 text-primary hover:bg-primary/20"
+ }`}
+ >
+ {added ? "Added" : expanded ? "Cancel" : "+ Add"}
+
+
+ {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
+
+ setAllTools(s.url, true)} className="text-[10px] text-primary hover:underline">All
+ ยท
+ setAllTools(s.url, false)} className="text-[10px] text-primary hover:underline">None
+
+
+
+ {toolKeys.map((t) => (
+
+ ))}
+
+ >
+ )}
+
confirmAdd(s)}
+ className="self-end px-2 py-1 rounded text-[10px] font-medium bg-primary text-white hover:bg-primary/90"
+ >
+ โ Confirm Add
+
+
+ )}
+
+ );
+ })}
+
+ )}
+
+
+ {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 };