# v0.4.30 (2026-05-11)

## Features
- MCP stdio→SSE bridge: expose local stdio MCP plugins over SSE (api/mcp/[plugin]/sse, /message)
- Dynamic Linux cert resolution + NSS DB injection (Debian/Arch/Fedora/openSUSE, Chrome/Chromium/Firefox incl. snap) (#1010)
- Cowork tool: expanded settings UI & API
- GitBook docs (DocsContent, DocsLayout)
## Fixes
- OAuth callback postMessage scoped to expected origins (CWE-1385) (#998)
- Re-enable TLS verification on DNS-bypass fetch (CWE-295) (#998)
- Normalize `developer` role → `system` for OpenAI-format providers (Deepseek, Groq, …) (#1011, closes #773)
- Respect `PORT` env in internal model-test fetch (#1014)
- Dropdown text readability in dark theme on usage page (#997)
## Improvements
- Refactor Claude CLI spoof headers into shared constant
- Tool deduper utility in open-sse handlers
This commit is contained in:
decolua
2026-05-12 09:19:50 +07:00
parent 76f3d4b74e
commit 8f4d29caa4
23 changed files with 1198 additions and 155 deletions
@@ -41,8 +41,12 @@ export default function CoworkToolCard({
const [showManualConfigModal, setShowManualConfigModal] = useState(false);
const [customBaseUrl, setCustomBaseUrl] = useState("");
const [plugins, setPlugins] = useState([]);
const [localPlugins, setLocalPlugins] = useState([]);
const [customPlugins, setCustomPlugins] = useState([]);
const [comboModalOpen, setComboModalOpen] = useState(false);
const [marketplaceOpen, setMarketplaceOpen] = useState(false);
const [addMcpOpen, setAddMcpOpen] = useState(false);
const [addMcpForm, setAddMcpForm] = useState({ type: "url", name: "", url: "", command: "", args: "" });
useEffect(() => {
if (apiKeys?.length > 0 && !selectedApiKey) {
@@ -71,6 +75,12 @@ export default function CoworkToolCard({
} else if (plugins.length === 0 && Array.isArray(status?.defaultPlugins)) {
setPlugins(status.defaultPlugins);
}
if (Array.isArray(status?.cowork?.localPlugins)) {
setLocalPlugins(status.cowork.localPlugins);
}
if (Array.isArray(status?.cowork?.customPlugins) && status.cowork.customPlugins.length > 0) {
setCustomPlugins(status.cowork.customPlugins);
}
}, [status]);
const checkStatus = async () => {
@@ -120,6 +130,8 @@ export default function CoworkToolCard({
apiKey: keyToUse,
models: selectedModels,
plugins,
localPlugins,
customPlugins,
}),
});
const data = await res.json();
@@ -168,6 +180,8 @@ export default function CoworkToolCard({
setMessage({ type: "success", text: "Settings reset successfully" });
setSelectedModels([]);
setPlugins(status?.defaultPlugins || []);
setLocalPlugins([]);
setCustomPlugins([]);
checkStatus();
} else {
setMessage({ type: "error", text: data.error || "Failed to reset" });
@@ -288,11 +302,11 @@ export default function CoworkToolCard({
<ApiKeySelect value={selectedApiKey} onChange={setSelectedApiKey} apiKeys={apiKeys} cloudEnabled={cloudEnabled} />
</div>
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr] sm:items-start sm:gap-2">
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right pt-1">Models</span>
<span className="material-symbols-outlined text-text-muted text-[14px] mt-1.5">arrow_forward</span>
<div className="flex-1 flex flex-col gap-2">
<div className="flex flex-wrap gap-1.5 min-h-[28px] px-2 py-1.5 bg-surface rounded border border-border">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr] sm:items-center sm:gap-2">
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right">Models</span>
<span className="material-symbols-outlined text-text-muted text-[14px]">arrow_forward</span>
<div className="flex-1 flex items-center gap-2">
<div className="flex-1 flex flex-wrap gap-1.5 min-h-[28px] px-2 py-1.5 bg-surface rounded border border-border">
{selectedModels.length === 0 ? (
<span className="text-xs text-text-muted">No models selected</span>
) : (
@@ -306,37 +320,145 @@ export default function CoworkToolCard({
))
)}
</div>
<button onClick={() => setComboModalOpen(true)} disabled={!hasActiveProviders} className={`self-start px-2 py-1 rounded border text-xs transition-colors ${hasActiveProviders ? "bg-primary/10 border-primary/40 text-primary hover:bg-primary/20 cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>+ Add Combo (claude-)</button>
<button onClick={() => setComboModalOpen(true)} disabled={!hasActiveProviders} className={`shrink-0 px-2 py-1.5 rounded border text-xs whitespace-nowrap transition-colors ${hasActiveProviders ? "bg-primary/10 border-primary/40 text-primary hover:bg-primary/20 cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>+ Combo</button>
</div>
</div>
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr] sm:items-start sm:gap-2">
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right pt-1">Plugins</span>
<span className="material-symbols-outlined text-text-muted text-[14px] mt-1.5">arrow_forward</span>
<div className="flex-1 flex flex-col gap-2">
<div className="flex flex-wrap gap-1.5 min-h-[28px] px-2 py-1.5 bg-surface rounded border border-border">
{plugins.filter((p) => p.name !== "exa").length === 0 ? (
<span className="text-xs text-text-muted">No plugins</span>
) : (
plugins.filter((p) => p.name !== "exa").map((p) => (
<span key={p.name} className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-black/5 dark:bg-white/5 text-text-muted border border-transparent hover:border-border">
{p.title || p.name}
{p.oauth && <span className="text-[8px] text-amber-600">OAuth</span>}
<button onClick={() => removePlugin(p.name)} className="ml-0.5 hover:text-red-500">
<span className="material-symbols-outlined text-[12px]">close</span>
</button>
</span>
))
)}
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right pt-2">MCP</span>
<span className="material-symbols-outlined text-text-muted text-[14px] mt-2">arrow_forward</span>
<div className="flex-1 flex flex-col gap-1">
{/* Preset plugins */}
{plugins.filter((p) => p.name !== "exa").map((p) => (
<div key={p.name} className="flex items-center gap-2 px-2 py-1 bg-surface rounded border border-border">
<span className="text-xs font-medium min-w-0 truncate flex-shrink-0">{p.title || p.name}</span>
{p.oauth && <span className="text-[8px] text-amber-600 shrink-0">OAuth</span>}
<div className="flex-1 flex flex-wrap gap-1 overflow-hidden" style={{ maxHeight: "1.5rem" }}>
{Array.isArray(p.toolNames) && p.toolNames.slice(0, 6).map((t) => (
<span key={t} className="text-[9px] px-1 py-0.5 rounded bg-black/5 dark:bg-white/5 text-text-muted whitespace-nowrap">{t}</span>
))}
{Array.isArray(p.toolNames) && p.toolNames.length > 6 && (
<span className="text-[9px] px-1 py-0.5 rounded bg-black/5 dark:bg-white/5 text-text-muted whitespace-nowrap">+{p.toolNames.length - 6}</span>
)}
</div>
<button onClick={() => removePlugin(p.name)} className="shrink-0 hover:text-red-500 ml-auto">
<span className="material-symbols-outlined text-[12px]">close</span>
</button>
</div>
))}
{/* Custom plugins */}
{customPlugins.map((p) => (
<div key={p.name} className="flex items-center gap-2 px-2 py-1 bg-surface rounded border border-border">
<span className="text-xs font-medium min-w-0 truncate flex-shrink-0">{p.name}</span>
<span className="text-[8px] px-1 py-0.5 rounded bg-blue-500/10 text-blue-500 shrink-0">custom</span>
<span className="flex-1 text-[9px] text-text-muted truncate">{p.url || p.command}</span>
<button onClick={() => setCustomPlugins(customPlugins.filter((x) => x.name !== p.name))} className="shrink-0 hover:text-red-500 ml-auto">
<span className="material-symbols-outlined text-[12px]">close</span>
</button>
</div>
))}
{plugins.filter((p) => p.name !== "exa").length === 0 && customPlugins.length === 0 && (
<div className="px-2 py-1.5 bg-surface rounded border border-border text-xs text-text-muted">No MCPs added</div>
)}
{/* Actions row */}
<div className="flex items-center gap-2 mt-0.5">
<button onClick={() => setMarketplaceOpen(true)} className="px-2 py-1 rounded border text-xs bg-primary/10 border-primary/40 text-primary hover:bg-primary/20 cursor-pointer whitespace-nowrap">
+ Browse
</button>
<button onClick={() => { setAddMcpForm({ type: "url", name: "", url: "", command: "", args: "" }); setAddMcpOpen(true); }} className="px-2 py-1 rounded border text-xs bg-surface border-border text-text-muted hover:border-primary hover:text-primary cursor-pointer whitespace-nowrap">
+ Custom
</button>
<a href="https://mcp.so" target="_blank" rel="noopener noreferrer" className="text-[10px] text-text-muted hover:text-primary underline ml-auto">Find MCPs </a>
</div>
<button onClick={() => 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
</button>
<p className="text-[10px] text-text-muted leading-snug">
💡 Exa is auto-installed. Prefer <code className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/5">web_search_exa</code> for web search and <code className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/5">web_fetch_exa</code> for reading pages.
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr] sm:items-start sm:gap-2">
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right pt-1">Tools</span>
<span className="material-symbols-outlined text-text-muted text-[14px] mt-1.5">arrow_forward</span>
<div className="flex-1 flex flex-col gap-1.5">
{(() => {
const exaEnabled = plugins.some((p) => p.name === "exa");
const exaDef = (status?.defaultPlugins || []).find((d) => d.name === "exa");
return (
<label className="flex items-start gap-2 cursor-pointer px-2 py-1.5 bg-surface rounded border border-border">
<input
type="checkbox"
checked={exaEnabled}
onChange={(e) => {
if (e.target.checked && exaDef) setPlugins([...plugins.filter((p) => p.name !== "exa"), exaDef]);
else setPlugins(plugins.filter((p) => p.name !== "exa"));
}}
className="mt-0.5"
/>
<div className="flex-1 min-w-0">
<div className="text-xs font-medium">Web Search & Fetch (Exa)</div>
<p className="text-[10px] text-text-muted leading-snug">Replaces built-in WebSearch/WebFetch. Auto-strips duplicates from tool list.</p>
</div>
</label>
);
})()}
{(() => {
const browserDef = (status?.localStdioPlugins || []).find((p) => p.name === "browsermcp");
if (!browserDef) return null;
const browserEnabled = localPlugins.includes("browsermcp");
return (
<label className="flex items-start gap-2 cursor-pointer px-2 py-1.5 bg-surface rounded border border-border">
<input
type="checkbox"
checked={browserEnabled}
onChange={(e) => setLocalPlugins(e.target.checked ? [...localPlugins, "browsermcp"] : localPlugins.filter((n) => n !== "browsermcp"))}
className="mt-0.5"
/>
<div className="flex-1 min-w-0">
<div className="text-xs font-medium">Browser Control (Browser MCP)</div>
<p className="text-[10px] text-text-muted leading-snug">
Controls your running Chrome. Auto-strips Cowork&apos;s built-in browser tools.{" "}
<a href={browserDef.extensionUrl} target="_blank" rel="noopener noreferrer" className="text-primary underline">Install Chrome extension</a>
</p>
</div>
</label>
);
})()}
</div>
</div>
{Array.isArray(status?.localStdioPlugins) && status.localStdioPlugins.filter((p) => p.name !== "browsermcp").length > 0 && (
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr] sm:items-start sm:gap-2">
<span className="w-32 shrink-0 text-sm font-semibold text-text-main text-right pt-1">Local Plugins</span>
<span className="material-symbols-outlined text-text-muted text-[14px] mt-1.5">arrow_forward</span>
<div className="flex-1 flex flex-col gap-2">
<div className="flex flex-col gap-1.5 px-2 py-1.5 bg-surface rounded border border-border">
{status.localStdioPlugins.filter((p) => p.name !== "browsermcp").map((p) => {
const enabled = localPlugins.includes(p.name);
return (
<label key={p.name} className="flex items-start gap-2 cursor-pointer">
<input
type="checkbox"
checked={enabled}
onChange={(e) => setLocalPlugins(e.target.checked ? [...localPlugins, p.name] : localPlugins.filter((n) => n !== p.name))}
className="mt-0.5"
/>
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-1.5">
<span className="text-xs font-medium">{p.title}</span>
<span className="text-[8px] text-amber-600">stdio</span>
</div>
<p className="text-[10px] text-text-muted leading-snug">{p.description}</p>
{p.extensionUrl && (
<a href={p.extensionUrl} target="_blank" rel="noopener noreferrer" className="text-[10px] text-primary underline">Install Chrome extension</a>
)}
</div>
</label>
);
})}
</div>
<p className="text-[10px] text-text-muted leading-snug">
Local plugins run as subprocess via <code className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/5">npx</code>. Requires Node.js installed.
</p>
</div>
</div>
)}
</div>
{message && (
@@ -385,6 +507,99 @@ export default function CoworkToolCard({
onAdd={addPlugin}
addedNames={plugins.map((p) => p.name)}
/>
{/* Add Custom MCP modal */}
{addMcpOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={() => setAddMcpOpen(false)}>
<div className="bg-surface border border-border rounded-xl shadow-xl w-full max-w-sm mx-4 p-5 flex flex-col gap-4" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between">
<h3 className="font-semibold text-sm">Add Custom MCP</h3>
<button onClick={() => setAddMcpOpen(false)} className="text-text-muted hover:text-text-main">
<span className="material-symbols-outlined text-[18px]">close</span>
</button>
</div>
<div className="flex gap-2">
<button
onClick={() => 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)</button>
<button
onClick={() => 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)</button>
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1">
<label className="text-[11px] text-text-muted font-medium">Name</label>
<input
type="text"
placeholder="my-mcp"
value={addMcpForm.name}
onChange={(e) => setAddMcpForm((f) => ({ ...f, name: e.target.value.replace(/\s+/g, "-").toLowerCase() }))}
className="px-2 py-1.5 rounded border border-border bg-surface text-xs outline-none focus:border-primary"
/>
</div>
{addMcpForm.type === "url" ? (
<div className="flex flex-col gap-1">
<label className="text-[11px] text-text-muted font-medium">SSE URL</label>
<input
type="text"
placeholder="https://your-mcp-server.com/sse"
value={addMcpForm.url}
onChange={(e) => setAddMcpForm((f) => ({ ...f, url: e.target.value }))}
className="px-2 py-1.5 rounded border border-border bg-surface text-xs outline-none focus:border-primary"
/>
</div>
) : (
<>
<div className="flex flex-col gap-1">
<label className="text-[11px] text-text-muted font-medium">Command</label>
<input
type="text"
placeholder="npx"
value={addMcpForm.command}
onChange={(e) => setAddMcpForm((f) => ({ ...f, command: e.target.value }))}
className="px-2 py-1.5 rounded border border-border bg-surface text-xs outline-none focus:border-primary"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-[11px] text-text-muted font-medium">Args <span className="font-normal">(comma-separated)</span></label>
<input
type="text"
placeholder="-y, @some/mcp-package"
value={addMcpForm.args}
onChange={(e) => setAddMcpForm((f) => ({ ...f, args: e.target.value }))}
className="px-2 py-1.5 rounded border border-border bg-surface text-xs outline-none focus:border-primary"
/>
</div>
</>
)}
</div>
<div className="flex gap-2 justify-end">
<button onClick={() => setAddMcpOpen(false)} className="px-3 py-1.5 rounded border border-border text-xs text-text-muted hover:bg-surface cursor-pointer">Cancel</button>
<button
onClick={() => {
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 }]);
}
setAddMcpOpen(false);
}}
className="px-3 py-1.5 rounded bg-primary text-white text-xs font-medium hover:opacity-90 cursor-pointer"
>Add</button>
</div>
</div>
</div>
)}
</Card>
);
}