diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js
index a3b20bcc..07bba260 100644
--- a/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js
+++ b/src/app/(dashboard)/dashboard/cli-tools/components/CoworkToolCard.js
@@ -48,7 +48,7 @@ export default function CoworkToolCard({
const [modelSelectOpen, setModelSelectOpen] = useState(false);
const [marketplaceOpen, setMarketplaceOpen] = useState(false);
const [addMcpOpen, setAddMcpOpen] = useState(false);
- const [addMcpForm, setAddMcpForm] = useState({ type: "url", name: "", url: "", command: "", args: "" });
+ const [addMcpForm, setAddMcpForm] = useState({ name: "", url: "" });
useEffect(() => {
if (apiKeys?.length > 0 && !selectedApiKey) {
@@ -374,7 +374,7 @@ export default function CoworkToolCard({
{p.name}
custom
-
{p.url || p.command}
+
{p.url}
@@ -388,7 +388,7 @@ export default function CoworkToolCard({
-
-
- setAddMcpForm((f) => ({ ...f, type: "url" }))}
- className={`flex-1 py-1.5 rounded border text-xs font-medium transition-colors ${addMcpForm.type === "url" ? "bg-primary/10 border-primary/40 text-primary" : "border-border text-text-muted hover:border-primary/40"}`}
- >URL (SSE)
- setAddMcpForm((f) => ({ ...f, type: "cmd" }))}
- className={`flex-1 py-1.5 rounded border text-xs font-medium transition-colors ${addMcpForm.type === "cmd" ? "bg-primary/10 border-primary/40 text-primary" : "border-border text-text-muted hover:border-primary/40"}`}
- >Command (stdio)
-
-
@@ -618,15 +582,8 @@ export default function CoworkToolCard({
{
const name = addMcpForm.name.trim();
- if (!name) return;
- if (addMcpForm.type === "url") {
- if (!addMcpForm.url.trim()) return;
- setCustomPlugins((prev) => [...prev.filter((x) => x.name !== name), { name, url: addMcpForm.url.trim(), transport: "sse", custom: true }]);
- } else {
- if (!addMcpForm.command.trim()) return;
- const args = addMcpForm.args.split(",").map((a) => a.trim()).filter(Boolean);
- setCustomPlugins((prev) => [...prev.filter((x) => x.name !== name), { name, command: addMcpForm.command.trim(), args, custom: true }]);
- }
+ if (!name || !addMcpForm.url.trim()) return;
+ setCustomPlugins((prev) => [...prev.filter((x) => x.name !== name), { name, url: addMcpForm.url.trim(), transport: "sse", custom: true }]);
setAddMcpOpen(false);
}}
className="px-3 py-1.5 rounded bg-primary text-white text-xs font-medium hover:opacity-90 cursor-pointer"
diff --git a/src/app/api/cli-tools/cowork-settings/route.js b/src/app/api/cli-tools/cowork-settings/route.js
index b599981c..e09ec283 100644
--- a/src/app/api/cli-tools/cowork-settings/route.js
+++ b/src/app/api/cli-tools/cowork-settings/route.js
@@ -5,9 +5,8 @@ import fs from "fs/promises";
import path from "path";
import os from "os";
import crypto from "crypto";
-import { DEFAULT_PLUGINS, LOCAL_STDIO_PLUGINS, ALLOWED_MCP_COMMANDS, buildManagedMcpServers } from "@/shared/constants/coworkPlugins";
+import { DEFAULT_PLUGINS, LOCAL_STDIO_PLUGINS, buildManagedMcpServers } from "@/shared/constants/coworkPlugins";
import { UPDATER_CONFIG } from "@/shared/constants/config";
-import { DATA_DIR } from "@/lib/dataDir";
import { getConsistentMachineId } from "@/shared/utils/machineId";
const APP_PORT = UPDATER_CONFIG.appPort;
@@ -180,17 +179,8 @@ const buildCustomEntries = (customPlugins) => {
if (!Array.isArray(customPlugins)) return [];
const out = [];
for (const p of customPlugins) {
- if (!p?.name) continue;
- if (p.url) {
- out.push({ name: p.name, url: p.url, transport: p.transport || "sse", custom: true });
- } else if (p.command) {
- out.push({
- name: p.name,
- url: `http://localhost:${APP_PORT}/api/mcp/${encodeURIComponent(p.name)}/sse`,
- transport: "sse",
- custom: true,
- });
- }
+ if (!p?.name || !p?.url) continue;
+ out.push({ name: p.name, url: p.url, transport: p.transport || "sse", custom: true });
}
return out;
};
@@ -311,8 +301,6 @@ export async function GET() {
}
export async function POST(request) {
- // Cowork disabled: spawns arbitrary processes (RCE risk).
- return NextResponse.json({ error: "Cowork is disabled" }, { status: 403 });
try {
const { baseUrl, apiKey, models, plugins, localPlugins, customPlugins } = await request.json();
@@ -327,29 +315,8 @@ export async function POST(request) {
// Respect empty array (user toggled all off); fallback to defaults only when undefined.
const pluginsArray = Array.isArray(plugins) ? plugins : DEFAULT_PLUGINS;
const localPluginNames = Array.isArray(localPlugins) ? localPlugins : [];
- const customPluginsArray = Array.isArray(customPlugins) ? customPlugins : [];
-
- // Register custom stdio plugins into bridge + persist for restart survival.
- if (customPluginsArray.length > 0) {
- const { registerCustomPlugin } = require("@/lib/mcp/stdioSseBridge");
- const stdioCustoms = customPluginsArray
- .filter((p) => p && typeof p.command === "string" && p.command.trim())
- .filter((p) => ALLOWED_MCP_COMMANDS.has(path.basename(p.command)))
- .map((p) => ({
- name: String(p.name || "").replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 64),
- command: p.command,
- args: Array.isArray(p.args) ? p.args.map(String) : [],
- }))
- .filter((p) => p.name);
- for (const p of stdioCustoms) {
- try { registerCustomPlugin(p); } catch { /* skip invalid */ }
- }
- try {
- const dir = path.join(DATA_DIR, "mcp");
- await fs.mkdir(dir, { recursive: true });
- await fs.writeFile(path.join(dir, "customPlugins.json"), JSON.stringify(stdioCustoms, null, 2));
- } catch { /* ignore */ }
- }
+ // Only URL-based custom plugins allowed (no stdio command spawning).
+ const customPluginsArray = (Array.isArray(customPlugins) ? customPlugins : []).filter((p) => p?.url);
const bridgeEntries = await injectAuthHeaders(buildLocalBridgeEntries(localPluginNames));
const customEntries = await injectAuthHeaders(buildCustomEntries(customPluginsArray));
diff --git a/src/app/api/mcp/[plugin]/message/route.js b/src/app/api/mcp/[plugin]/message/route.js
index 8e935e4f..86df051d 100644
--- a/src/app/api/mcp/[plugin]/message/route.js
+++ b/src/app/api/mcp/[plugin]/message/route.js
@@ -5,8 +5,6 @@ export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function POST(request, { params }) {
- // Cowork disabled: MCP stdio bridge spawns arbitrary processes (RCE risk).
- return NextResponse.json({ error: "Cowork is disabled" }, { status: 403 });
const { plugin } = await params;
if (!findPlugin(plugin)) {
return NextResponse.json({ error: `Unknown plugin: ${plugin}` }, { status: 404 });
diff --git a/src/app/api/mcp/[plugin]/sse/route.js b/src/app/api/mcp/[plugin]/sse/route.js
index 090d148a..8f9d67d4 100644
--- a/src/app/api/mcp/[plugin]/sse/route.js
+++ b/src/app/api/mcp/[plugin]/sse/route.js
@@ -4,8 +4,6 @@ export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function GET(request, { params }) {
- // Cowork disabled: MCP stdio bridge spawns arbitrary processes (RCE risk).
- return new Response("Cowork is disabled", { status: 403 });
const { plugin } = await params;
if (!findPlugin(plugin)) {
return new Response(`Unknown plugin: ${plugin}`, { status: 404 });
diff --git a/src/lib/mcp/stdioSseBridge.js b/src/lib/mcp/stdioSseBridge.js
index d68d0b1f..c0e07b33 100644
--- a/src/lib/mcp/stdioSseBridge.js
+++ b/src/lib/mcp/stdioSseBridge.js
@@ -2,13 +2,8 @@
// broadcasts JSON-RPC frames over SSE, accepts client messages via HTTP POST.
const { spawn } = require("child_process");
-const fs = require("fs");
-const path = require("path");
const crypto = require("crypto");
-const { LOCAL_STDIO_PLUGINS, ALLOWED_MCP_COMMANDS } = require("@/shared/constants/coworkPlugins");
-const { DATA_DIR } = require("@/lib/dataDir");
-
-const CUSTOM_FILE = path.join(DATA_DIR, "mcp", "customPlugins.json");
+const { LOCAL_STDIO_PLUGINS } = require("@/shared/constants/coworkPlugins");
const G_KEY = "__9routerMcpBridges";
const MAX_TEXT_CHARS = 50000;
@@ -106,33 +101,9 @@ const getStore = () => {
return globalThis[G_KEY];
};
-const getCustomStore = () => {
- if (!globalThis.__9routerCustomPlugins) globalThis.__9routerCustomPlugins = new Map();
- return globalThis.__9routerCustomPlugins;
-};
-
-function isAllowedCommand(cmd) {
- const bin = path.basename(String(cmd || ""));
- return ALLOWED_MCP_COMMANDS.has(bin);
-}
-
-function registerCustomPlugin(def) {
- if (!isAllowedCommand(def?.command)) {
- throw new Error(`Blocked: command '${def?.command}' not in MCP allowlist`);
- }
- getCustomStore().set(def.name, def);
-}
-
+// Only preset stdio plugins may spawn. No user-defined commands (RCE prevention).
function findPlugin(name) {
- const fromMem = getCustomStore().get(name) || LOCAL_STDIO_PLUGINS.find((p) => p.name === name);
- if (fromMem) return fromMem;
- // Lazy-load custom plugins from disk (survives app restart); re-validate allowlist.
- try {
- const list = JSON.parse(fs.readFileSync(CUSTOM_FILE, "utf-8"));
- const def = Array.isArray(list) ? list.find((p) => p.name === name && p.command) : null;
- if (def && isAllowedCommand(def.command)) { getCustomStore().set(def.name, def); return def; }
- } catch { /* file missing or invalid */ }
- return null;
+ return LOCAL_STDIO_PLUGINS.find((p) => p.name === name) || null;
}
function getOrSpawn(name) {
@@ -195,4 +166,4 @@ function isRunning(name) {
return !!(entry?.proc && !entry.proc.killed && entry.proc.exitCode === null);
}
-module.exports = { getOrSpawn, registerSession, unregisterSession, sendToChild, isRunning, findPlugin, registerCustomPlugin };
+module.exports = { getOrSpawn, registerSession, unregisterSession, sendToChild, isRunning, findPlugin };
diff --git a/src/shared/constants/cliTools.js b/src/shared/constants/cliTools.js
index b3e1d7da..18ecab72 100644
--- a/src/shared/constants/cliTools.js
+++ b/src/shared/constants/cliTools.js
@@ -128,15 +128,14 @@ export const CLI_TOOLS = {
description: "OpenCode AI Terminal Assistant",
configType: "custom",
},
- // Cowork disabled: spawns arbitrary processes (RCE risk). Hidden from CLI tools UI.
- // cowork: {
- // id: "cowork",
- // name: "Claude Cowork",
- // image: "/providers/claude.png",
- // color: "#D97757",
- // description: "Claude Desktop Cowork (third-party inference)",
- // configType: "custom",
- // },
+ cowork: {
+ id: "cowork",
+ name: "Claude Cowork",
+ image: "/providers/claude.png",
+ color: "#D97757",
+ description: "Claude Desktop Cowork (third-party inference)",
+ configType: "custom",
+ },
hermes: {
id: "hermes",
name: "Hermes Agent",
diff --git a/src/shared/constants/coworkPlugins.js b/src/shared/constants/coworkPlugins.js
index 592d8569..b32f1208 100644
--- a/src/shared/constants/coworkPlugins.js
+++ b/src/shared/constants/coworkPlugins.js
@@ -69,7 +69,4 @@ function buildManagedMcpServers(plugins) {
return out;
}
-// Allowlist of executables that may be spawned for custom stdio MCP plugins.
-const ALLOWED_MCP_COMMANDS = new Set(["npx", "node", "uvx", "python", "python3", "bunx", "bun"]);
-
-module.exports = { DEFAULT_PLUGINS, LOCAL_STDIO_PLUGINS, ALLOWED_MCP_COMMANDS, buildManagedMcpServers };
+module.exports = { DEFAULT_PLUGINS, LOCAL_STDIO_PLUGINS, buildManagedMcpServers };