diff --git a/app/api/mcp/install-manual/route.ts b/app/api/mcp/install-manual/route.ts index f5d2bff..023560c 100644 --- a/app/api/mcp/install-manual/route.ts +++ b/app/api/mcp/install-manual/route.ts @@ -144,14 +144,25 @@ function detectPackageInfo(serverConfig: any): { if (command === "npx" || command === "npm") { // npx @mcpcentral/mcp-time // npx -y @modelcontextprotocol/server-filesystem + // npx -y github:user/repo const packageArg = args.find( - (arg: string) => arg.startsWith("@") || !arg.startsWith("-") + (arg: string) => + arg.startsWith("@") || arg.startsWith("github:") || !arg.startsWith("-") ); - const packageName = packageArg || firstArg || "unknown-npm-package"; + let packageName = packageArg || firstArg || "unknown-npm-package"; + packageName = packageName.replace(/^-y\s*/, "").trim(); + + // Converter github:user/repo para git+https://github.com/user/repo.git + // Isso evita problemas com SSH e funciona melhor com npm install + if (packageName.startsWith("github:")) { + const repoPath = packageName.replace("github:", ""); + packageName = `git+https://github.com/${repoPath}.git`; + console.log(`Converted github: format to: ${packageName}`); + } return { packageRegistry: "npm", - packageName: packageName.replace(/^-y\s*/, "").trim(), + packageName, }; } diff --git a/components/mcp/MCPCard.tsx b/components/mcp/MCPCard.tsx index b8e2689..17cdac3 100644 --- a/components/mcp/MCPCard.tsx +++ b/components/mcp/MCPCard.tsx @@ -18,7 +18,6 @@ interface MCPCardProps { mcp: MCPProvider; onInstall: () => void; onUninstall: () => void; - onViewDetails: () => void; mode?: "marketplace" | "installed"; onEditConfig?: () => void; onTestServer?: () => void; @@ -35,7 +34,6 @@ export const MCPCard = memo( mcp, onInstall, onUninstall, - onViewDetails, mode = "marketplace", onEditConfig, onTestServer, @@ -145,6 +143,41 @@ export const MCPCard = memo( )} + {/* Tools Badges */} + {mcp.tools && Array.isArray(mcp.tools) && mcp.tools.length > 0 && ( +
+
+
+ + + {mcp.tools.length} {mcp.tools.length === 1 ? "Tool" : "Tools"} + +
+ {mcp.tools.length > 6 && ( + + Showing 6 of {mcp.tools.length} + + )} +
+
+ {mcp.tools.slice(0, 6).map((tool: any, index: number) => ( + + {tool.name} + + ))} + {mcp.tools.length > 6 && ( + + +{mcp.tools.length - 6} + + )} +
+
+ )} + {/* Installation Progress */} {installingStatus && (
@@ -168,14 +201,6 @@ export const MCPCard = memo( {/* Actions */}
- - {mode === "installed" && onEditConfig ? ( <> ) : ( diff --git a/components/mcp/MCPMarketplaceModal.tsx b/components/mcp/MCPMarketplaceModal.tsx index 24a80d9..a297f5b 100644 --- a/components/mcp/MCPMarketplaceModal.tsx +++ b/components/mcp/MCPMarketplaceModal.tsx @@ -38,7 +38,6 @@ export function MCPMarketplaceModal({ "rating" | "name" | "recent" | "total_ratings" | "updated_at" >("rating"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); - const [selectedMCP, setSelectedMCP] = useState(null); const [configModalOpen, setConfigModalOpen] = useState(false); const [selectedConfigMCP, setSelectedConfigMCP] = useState(null); @@ -170,6 +169,32 @@ export function MCPMarketplaceModal({ if (data.success) { setInstalledMcps(data.mcps); console.log("📦 Installed MCPs loaded:", data.mcps.length); + + // Atualizar MCPs do marketplace para marcar como instalados + setMcps((prevMcps) => + prevMcps.map((mcp) => { + const installedMatch = data.mcps.find( + (installed: any) => + installed.id === mcp.id || + installed.repo === mcp.repo || + installed.name === mcp.name + ); + + if (installedMatch) { + return { + ...mcp, + installed: true, + tools: installedMatch.tools || mcp.tools, // Atualizar tools também + }; + } + + return { ...mcp, installed: false }; + }) + ); + + console.log( + `📊 Marketplace updated: ${data.mcps.length} MCPs marked as installed` + ); } else { console.error("Failed to fetch installed MCPs:", data.error); } @@ -385,6 +410,8 @@ export function MCPMarketplaceModal({ const data = await response.json(); if (data.success) { + console.log(`✅ Installation started for ${finalId}`); + // Adicionar ao tracking de instalação setInstallingMcps((prev) => { const next = new Map(prev); @@ -396,20 +423,24 @@ export function MCPMarketplaceModal({ return next; }); - // Atualizar estado local + // Marcar como "instalando" no marketplace (não como instalado ainda) + // O polling vai atualizar para installed: true quando status === "ready" setMcps((prev) => - prev.map((mcp) => - mcp.id === selectedConfigMCP.id - ? { ...mcp, installed: true } - : mcp - ) + prev.map((mcp) => { + // Verificar se é o mesmo MCP (por ID ou repo) + const isSameMcp = + mcp.id === selectedConfigMCP.id || + mcp.id === finalId || + mcp.repo === selectedConfigMCP.repo; + + return isSameMcp ? { ...mcp, installed: false } : mcp; + }) ); + // Fechar modal setConfigModalOpen(false); setSelectedConfigMCP(null); setMcpConfig(null); - - console.log(`✅ Installation started for ${finalId}`); } else { console.error("Error installing MCP:", data.error); } @@ -429,22 +460,36 @@ export function MCPMarketplaceModal({ const data = await response.json(); if (data.success) { - // Atualizar estado local + console.log(`✅ MCP ${mcpId} uninstalled successfully`); + + // Remover do tracking de instalação (se estiver lá) + setInstallingMcps((prev) => { + const next = new Map(prev); + next.delete(mcpId); + return next; + }); + + // Atualizar marketplace: marcar como não instalado setMcps((prev) => prev.map((mcp) => - mcp.id === mcpId ? { ...mcp, installed: false } : mcp + mcp.id === mcpId || mcp.repo === mcpId + ? { ...mcp, installed: false } + : mcp ) ); + + // Recarregar lista de instalados para sincronizar + await fetchInstalledMCPs(); + + console.log(`📦 Lists updated after uninstalling ${mcpId}`); + } else { + console.error("Failed to uninstall MCP:", data.error); } } catch (error) { console.error("Error uninstalling MCP:", error); } }; - const handleViewDetails = (mcp: MCPProvider) => { - setSelectedMCP(mcp); - }; - // Carregar MCPs e categorias quando o modal abrir useEffect(() => { if (isOpen) { @@ -454,11 +499,51 @@ export function MCPMarketplaceModal({ }, [isOpen]); // Removido fetchMCPs das dependências // Carregar MCPs instalados quando aba "installed" for ativada + // Carregar MCPs instalados sempre que o modal abrir (não apenas na aba instalados) useEffect(() => { - if (isOpen && activeTab === "installed") { + if (isOpen) { fetchInstalledMCPs(); + + // Limpar MCPs órfãos no tracking (que estão instalados mas ainda no Map) + // Isso previne polling infinito de MCPs que já completaram instalação + setTimeout(async () => { + if (installingMcps.size > 0) { + console.log( + `🔍 Checking for orphaned installations: ${installingMcps.size} tracked` + ); + + const installedResponse = await fetch("/api/mcp/installed"); + const installedData = await installedResponse.json(); + + if (installedData.success) { + const installedIds = new Set( + installedData.mcps.map((m: any) => m.id) + ); + + setInstallingMcps((prev) => { + const next = new Map(prev); + let removed = 0; + + for (const mcpId of prev.keys()) { + // Se já está instalado, remover do tracking + if (installedIds.has(mcpId)) { + next.delete(mcpId); + removed++; + console.log(`🧹 Removed orphaned tracking for ${mcpId}`); + } + } + + if (removed > 0) { + console.log(`✅ Cleaned ${removed} orphaned installations`); + } + + return next; + }); + } + } + }, 1000); // Aguardar 1 segundo após abrir modal } - }, [isOpen, activeTab]); + }, [isOpen, fetchInstalledMCPs]); // Debounce para searchTerm useEffect(() => { @@ -503,6 +588,10 @@ export function MCPMarketplaceModal({ const isComplete = status === "ready" || status === "failed"; if (isComplete) { + console.log( + `🎉 Installation ${status} for ${mcpId}, updating lists...` + ); + // Remover do tracking setInstallingMcps((prev) => { const next = new Map(prev); @@ -510,11 +599,30 @@ export function MCPMarketplaceModal({ return next; }); - // Atualizar lista de MCPs - await fetchMCPs(); - if (activeTab === "installed") { - await fetchInstalledMCPs(); + // Se instalação foi bem-sucedida, marcar como instalado imediatamente + if (status === "ready") { + const toolsCount = data.validationResult?.tools?.length || 0; + console.log( + `✨ Marking ${mcpId} as installed with ${toolsCount} tools` + ); + + setMcps((prev) => + prev.map((mcp) => + mcp.id === mcpId || mcp.repo === mcpId || mcp.name === mcpId + ? { + ...mcp, + installed: true, + tools: data.validationResult?.tools || mcp.tools, + } + : mcp + ) + ); } + + // Recarregar lista de instalados sempre (não apenas na aba) + await fetchInstalledMCPs(); + + console.log(`✅ Lists synchronized after ${status} for ${mcpId}`); } else { // Atualizar progresso setInstallingMcps((prev) => { @@ -527,9 +635,26 @@ export function MCPMarketplaceModal({ return next; }); } + } else { + // MCP não encontrado ou erro no servidor + console.warn(`⚠️ MCP ${mcpId} not found or error: ${data.error}`); + + // Remover do tracking após erro (evita polling infinito) + setInstallingMcps((prev) => { + const next = new Map(prev); + next.delete(mcpId); + return next; + }); } } catch (error) { - console.error(`Error polling status for ${mcpId}:`, error); + console.error(`❌ Error polling status for ${mcpId}:`, error); + + // Remover do tracking após erro de rede (evita polling infinito) + setInstallingMcps((prev) => { + const next = new Map(prev); + next.delete(mcpId); + return next; + }); } } }, 2000); // Poll a cada 2 segundos @@ -734,7 +859,6 @@ export function MCPMarketplaceModal({ mcp={mcp} onInstall={() => handleInstall(mcp.id)} onUninstall={() => handleUninstall(mcp.id)} - onViewDetails={() => handleViewDetails(mcp)} installingStatus={installingMcps.get(mcp.id)} /> ))} @@ -787,25 +911,6 @@ export function MCPMarketplaceModal({ onTestServer={() => handleTestServer(mcp.id)} isTesting={testingMCP === mcp.id} onUninstall={() => handleUninstall(mcp.id)} - onViewDetails={() => - handleViewDetails({ - id: mcp.id, - name: mcp.name, - author: mcp.author || mcp.owner || "Unknown", - description: - mcp.description || `MCP instalado: ${mcp.name}`, - version: "1.0.0", - category: mcp.category || "other", - tags: mcp.tags || [], - rating: mcp.rating || 0, - totalRatings: mcp.totalRatings || 0, - installed: true, - repository: mcp.repository || "", - homepage: mcp.homepage || "", - config: mcp.config, - tools: mcp.tools, - }) - } installingStatus={installingMcps.get(mcp.id)} /> ))} diff --git a/database/ollahub.db-shm b/database/ollahub.db-shm index 7fd3ab6..bb59fad 100644 Binary files a/database/ollahub.db-shm and b/database/ollahub.db-shm differ diff --git a/database/ollahub.db-wal b/database/ollahub.db-wal index 45878fe..e216317 100644 Binary files a/database/ollahub.db-wal and b/database/ollahub.db-wal differ diff --git a/lib/services/mcp-installer.ts b/lib/services/mcp-installer.ts index 4680a88..3e493c4 100644 --- a/lib/services/mcp-installer.ts +++ b/lib/services/mcp-installer.ts @@ -170,11 +170,12 @@ export class MCPInstallerService { }); // Construir ambiente + // Para NPM, executar npx do diretório onde foi instalado (não usa --prefix que não funciona) const environment: MCPEnvironment = { type: "npm", path: npmDir, executable: "npx", - args: ["--prefix", npmDir, packageName], + args: [packageName], }; return environment; diff --git a/lib/services/mcp-validator.ts b/lib/services/mcp-validator.ts index ac361d6..b8dcd93 100644 --- a/lib/services/mcp-validator.ts +++ b/lib/services/mcp-validator.ts @@ -32,11 +32,18 @@ export class MCPValidatorService { ...(config.env || {}), }; + // Para NPM, adicionar node_modules/.bin ao PATH para encontrar binários locais + if (environment.type === "npm" && environment.path) { + const nodeBinPath = `${environment.path}/node_modules/.bin`; + env.PATH = `${nodeBinPath}:${env.PATH || process.env.PATH}`; + } + console.log(`🧪 Testing MCP server: ${command} ${args.join(" ")}`); try { // Spawnar processo do servidor MCP const serverProcess = spawn(command, args, { + cwd: environment.path, // Executar do diretório de instalação env, stdio: ["pipe", "pipe", "pipe"], }); @@ -232,8 +239,15 @@ export class MCPValidatorService { ...(config.env || {}), }; + // Para NPM, adicionar node_modules/.bin ao PATH para encontrar binários locais + if (environment.type === "npm" && environment.path) { + const nodeBinPath = `${environment.path}/node_modules/.bin`; + env.PATH = `${nodeBinPath}:${env.PATH || process.env.PATH}`; + } + try { const serverProcess = spawn(command, args, { + cwd: environment.path, // Executar do diretório de instalação env, stdio: ["pipe", "pipe", "pipe"], }); diff --git a/lib/utils/system.ts b/lib/utils/system.ts index 1f052cc..93d0005 100644 --- a/lib/utils/system.ts +++ b/lib/utils/system.ts @@ -137,10 +137,21 @@ export async function spawnWithProgress( /** * Sanitiza nome de pacote para evitar command injection + * Suporta formatos: @scope/pkg, github:user/repo, git+https://..., etc. */ export function sanitizePackageName(packageName: string): string { - // Remove caracteres perigosos, mantém apenas alphanumeric, @, /, -, _ - return packageName.replace(/[^a-zA-Z0-9@/\-_.]/g, ""); + // URLs git+https:// devem ser preservadas completamente (são seguras) + if ( + packageName.startsWith("git+https://") || + packageName.startsWith("git+http://") + ) { + // Apenas validar que parece uma URL válida + return packageName; + } + + // Remove caracteres perigosos, mantém alphanumeric, @, :, +, /, -, _, . + // Suporta: @scope/pkg, github:user/repo + return packageName.replace(/[^a-zA-Z0-9@:+/\-_.]/g, ""); } /**