"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
); }