import { Dialog } from "@kobalte/core/dialog" import { ChevronDown, ExternalLink, Plus, RefreshCw, Search, Settings } from "lucide-solid" import { Component, For, Show, createEffect, createMemo, createSignal } from "solid-js" import { serverApi } from "../lib/api-client" import { getLogger } from "../lib/logger" import InstanceServiceStatus from "./instance-service-status" import { useOptionalInstanceMetadataContext } from "../lib/contexts/instance-metadata-context" type McpServerConfig = { command?: string args?: string[] env?: Record // Remote MCP server support type?: "remote" | "http" | "sse" | "streamable-http" url?: string headers?: Record } type McpConfig = { mcpServers?: Record } type McpMarketplaceEntry = { id: string name: string description: string config: McpServerConfig tags?: string[] source?: string requiresApiKey?: boolean } interface McpManagerProps { instanceId: string } const log = getLogger("mcp-manager") const MCP_LINKER_RELEASES = "https://github.com/milisp/mcp-linker/releases" const MCP_LINKER_MARKET = "https://github.com/milisp/mcp-linker" const MARKETPLACE_ENTRIES: McpMarketplaceEntry[] = [ { id: "zread", name: "Zread (Z.AI)", description: "Search GitHub repos, read code, analyze structure. Powered by Z.AI - requires API key from z.ai/manage-apikey.", config: { type: "remote", url: "https://api.z.ai/api/mcp/zread/mcp", headers: { "Authorization": "Bearer YOUR_ZAI_API_KEY" } }, tags: ["github", "code", "search", "z.ai"], source: "z.ai", requiresApiKey: true, }, { id: "sequential-thinking", name: "Sequential Thinking", description: "Step-by-step reasoning scratchpad for complex tasks.", config: { command: "npx", args: ["-y", "@modelcontextprotocol/server-sequential-thinking"] }, tags: ["reasoning", "planning"], source: "curated", }, { id: "desktop-commander", name: "Desktop Commander", description: "Control local desktop actions and automation.", config: { command: "npx", args: ["-y", "@modelcontextprotocol/server-desktop-commander"] }, tags: ["automation", "local"], source: "curated", }, { id: "web-reader", name: "Web Reader", description: "Fetch and summarize web pages with structured metadata.", config: { command: "npx", args: ["-y", "@modelcontextprotocol/server-web-reader"] }, tags: ["web", "search"], source: "curated", }, { id: "github", name: "GitHub", description: "Query GitHub repos, issues, and pull requests.", config: { command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] }, tags: ["git", "productivity"], source: "curated", }, { id: "postgres", name: "PostgreSQL", description: "Inspect PostgreSQL schemas and run safe queries.", config: { command: "npx", args: ["-y", "@modelcontextprotocol/server-postgres"] }, tags: ["database"], source: "curated", }, ] const McpManager: Component = (props) => { const [config, setConfig] = createSignal({ mcpServers: {} }) const [isLoading, setIsLoading] = createSignal(false) const [error, setError] = createSignal(null) const [menuOpen, setMenuOpen] = createSignal(false) const [showManual, setShowManual] = createSignal(false) const [showMarketplace, setShowMarketplace] = createSignal(false) const [marketplaceQuery, setMarketplaceQuery] = createSignal("") const [marketplaceLoading, setMarketplaceLoading] = createSignal(false) const [marketplaceEntries, setMarketplaceEntries] = createSignal([]) const [rawMode, setRawMode] = createSignal(false) const [serverName, setServerName] = createSignal("") const [serverJson, setServerJson] = createSignal("") const [saving, setSaving] = createSignal(false) const metadataContext = useOptionalInstanceMetadataContext() const metadata = createMemo(() => metadataContext?.metadata?.() ?? null) const mcpStatus = createMemo(() => metadata()?.mcpStatus ?? {}) const servers = createMemo(() => Object.entries(config().mcpServers ?? {})) const filteredMarketplace = createMemo(() => { const combined = [...MARKETPLACE_ENTRIES, ...marketplaceEntries()] const query = marketplaceQuery().trim().toLowerCase() if (!query) return combined return combined.filter((entry) => { const haystack = `${entry.name} ${entry.description} ${entry.id} ${(entry.tags || []).join(" ")}`.toLowerCase() return haystack.includes(query) }) }) const loadConfig = async () => { setIsLoading(true) setError(null) try { const data = await serverApi.fetchWorkspaceMcpConfig(props.instanceId) setConfig(data.config ?? { mcpServers: {} }) } catch (err) { log.error("Failed to load MCP config", err) setError("Failed to load MCP configuration.") } finally { setIsLoading(false) } } createEffect(() => { void loadConfig() }) const openExternal = (url: string) => { window.open(url, "_blank", "noopener") } const resetManualForm = () => { setServerName("") setServerJson("") setRawMode(false) } const handleManualSave = async () => { if (saving()) return setSaving(true) setError(null) try { const parsed = JSON.parse(serverJson() || "{}") const nextConfig: McpConfig = { ...(config() ?? {}) } const mcpServers = { ...(nextConfig.mcpServers ?? {}) } if (rawMode()) { if (!parsed || typeof parsed !== "object") { throw new Error("Raw config must be a JSON object.") } setConfig(parsed as McpConfig) await serverApi.updateWorkspaceMcpConfig(props.instanceId, parsed) } else { const name = serverName().trim() if (!name) { throw new Error("Server name is required.") } if (!parsed || typeof parsed !== "object") { throw new Error("Server config must be a JSON object.") } mcpServers[name] = parsed as McpServerConfig nextConfig.mcpServers = mcpServers setConfig(nextConfig) await serverApi.updateWorkspaceMcpConfig(props.instanceId, nextConfig) } resetManualForm() setShowManual(false) } catch (err) { const message = err instanceof Error ? err.message : "Invalid MCP configuration." setError(message) } finally { setSaving(false) } } const handleMarketplaceInstall = async (entry: McpMarketplaceEntry) => { if (saving()) return setSaving(true) setError(null) try { const nextConfig: McpConfig = { ...(config() ?? {}) } const mcpServers = { ...(nextConfig.mcpServers ?? {}) } mcpServers[entry.id] = entry.config nextConfig.mcpServers = mcpServers setConfig(nextConfig) await serverApi.updateWorkspaceMcpConfig(props.instanceId, nextConfig) } catch (err) { const message = err instanceof Error ? err.message : "Failed to install MCP server." setError(message) } finally { setSaving(false) } } const fetchNpmEntries = async (query: string, sourceLabel: string): Promise => { const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=50` const response = await fetch(url) if (!response.ok) { throw new Error(`Failed to fetch ${sourceLabel} MCP entries`) } const data = await response.json() as { objects?: Array<{ package?: { name?: string; description?: string; keywords?: string[] } }> } const objects = Array.isArray(data.objects) ? data.objects : [] return objects .map((entry) => entry.package) .filter((pkg): pkg is { name: string; description?: string; keywords?: string[] } => Boolean(pkg?.name)) .map((pkg) => ({ id: pkg.name, name: pkg.name.replace(/^@modelcontextprotocol\/server-/, ""), description: pkg.description || "Community MCP server package", config: { command: "npx", args: ["-y", pkg.name] }, tags: pkg.keywords, source: sourceLabel, })) } const loadMarketplace = async () => { if (marketplaceLoading()) return setMarketplaceLoading(true) try { const [official, community] = await Promise.allSettled([ fetchNpmEntries("@modelcontextprotocol/server", "npm:official"), fetchNpmEntries("mcp server", "npm:community"), ]) const next: McpMarketplaceEntry[] = [] if (official.status === "fulfilled") next.push(...official.value) if (community.status === "fulfilled") next.push(...community.value) const deduped = new Map() for (const entry of next) { if (!deduped.has(entry.id)) deduped.set(entry.id, entry) } setMarketplaceEntries(Array.from(deduped.values())) } catch (err) { log.error("Failed to load marketplace", err) setError("Failed to load marketplace sources.") } finally { setMarketplaceLoading(false) } } return (
MCP Servers
{(err) =>
{err()}
}
0} fallback={
{isLoading() ? "Loading MCP servers..." : "No MCP servers configured."}
} >
{([name, server]) => (
{name} {server.command ? `${server.command} ${(server.args ?? []).join(" ")}` : "Custom config"}
{mcpStatus()?.[name]?.status} error
)}
Configure MCP Server Paste the MCP server config JSON. Use marketplace via MCP Linker for curated servers.