From f19bf10ef369306c85e402ce007965dcda2ec8af Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Tue, 28 Oct 2025 21:59:00 -0300 Subject: [PATCH] feat(mcp): enhance MCP installation with status tracking and custom configs - Add /mcp-servers/ to .gitignore for MCP server installations - Extend mcp_installations table with status, logs, and validation fields - Refactor MCP install route to support custom configs, enable logs, and async installation - Improve MCPSyncService with promise-based syncing to prevent concurrent operations This update improves MCP management by adding robust installation tracking, custom configuration support, and better error handling for a more reliable user experience. --- .gitignore | 2 + app/api/mcp/install-manual/route.ts | 348 +++++++++++ app/api/mcp/install-status/route.ts | 58 ++ app/api/mcp/install/route.ts | 237 ++++++- app/api/mcp/installed/route.ts | 74 +++ app/api/mcp/list/route.ts | 48 +- app/api/mcp/server-config/route.ts | 112 ++++ app/api/mcp/test-server/route.ts | 128 ++++ app/api/mcp/uninstall/route.ts | 40 +- app/api/mcp/update-config/route.ts | 52 ++ components/mcp/MCPCard.tsx | 395 +++++++----- components/mcp/MCPInstallConfigModal.tsx | 272 +++++++++ components/mcp/MCPManualInstallModal.tsx | 320 ++++++++++ components/mcp/MCPMarketplaceModal.tsx | 745 ++++++++++++++++++++--- components/mcp/VirtualizedMCPGrid.tsx | 1 + database/migrations.ts | 61 ++ database/ollahub.db-shm | Bin 32768 -> 32768 bytes database/ollahub.db-wal | Bin 4165352 -> 593312 bytes database/repositories/mcp-cache.ts | 158 +++-- database/repositories/mcp.ts | 234 ++++++- database/schema.sql | 6 + lib/config/mcp.ts | 33 + lib/services/mcp-environment.ts | 214 +++++++ lib/services/mcp-installer.ts | 408 +++++++++++++ lib/services/mcp-sync.ts | 115 ++-- lib/services/mcp-validator.ts | 285 +++++++++ lib/services/pulsemcp.ts | 46 +- lib/types/mcp-installer.ts | 65 ++ lib/types/mcp.ts | 16 + lib/utils/system.ts | 155 +++++ package.json | 4 + pnpm-lock.yaml | 56 ++ scripts/add-fts-to-mcp-cache.ts | 66 ++ scripts/benchmark-performance.ts | 187 ++++++ scripts/test-mcp-installation.ts | 231 +++++++ 35 files changed, 4743 insertions(+), 429 deletions(-) create mode 100644 app/api/mcp/install-manual/route.ts create mode 100644 app/api/mcp/install-status/route.ts create mode 100644 app/api/mcp/installed/route.ts create mode 100644 app/api/mcp/server-config/route.ts create mode 100644 app/api/mcp/test-server/route.ts create mode 100644 app/api/mcp/update-config/route.ts create mode 100644 components/mcp/MCPInstallConfigModal.tsx create mode 100644 components/mcp/MCPManualInstallModal.tsx create mode 100644 components/mcp/VirtualizedMCPGrid.tsx create mode 100644 lib/config/mcp.ts create mode 100644 lib/services/mcp-environment.ts create mode 100644 lib/services/mcp-installer.ts create mode 100644 lib/services/mcp-validator.ts create mode 100644 lib/types/mcp-installer.ts create mode 100644 lib/utils/system.ts create mode 100644 scripts/add-fts-to-mcp-cache.ts create mode 100644 scripts/benchmark-performance.ts create mode 100644 scripts/test-mcp-installation.ts diff --git a/.gitignore b/.gitignore index 4e35ea0..6e325fc 100644 --- a/.gitignore +++ b/.gitignore @@ -46,5 +46,7 @@ database/extensions/*.so database/extensions/*.dll database/extensions/*.dylib +# mcp servers installation directory +/mcp-servers/ /.cursor \ No newline at end of file diff --git a/app/api/mcp/install-manual/route.ts b/app/api/mcp/install-manual/route.ts new file mode 100644 index 0000000..f5d2bff --- /dev/null +++ b/app/api/mcp/install-manual/route.ts @@ -0,0 +1,348 @@ +import { NextResponse } from "next/server"; +import { MCPRepository } from "@/database/repositories/mcp"; +import { MCPInstallerService } from "@/lib/services/mcp-installer"; +import { MCPValidatorService } from "@/lib/services/mcp-validator"; +import { + MCPInstallationStatus, + type InstallationConfig, + type InstallationProgress, + type PackageRegistry, +} from "@/lib/types/mcp-installer"; + +/** + * POST /api/mcp/install-manual + * Instala um MCP a partir de uma configuração manual (formato Claude Desktop) + */ +export async function POST(request: Request) { + try { + const body = await request.json(); + const { config, mcpId, enableLogs } = body; + + // Validar estrutura básica + if (!config || typeof config !== "object") { + return NextResponse.json( + { success: false, error: "Invalid configuration format" }, + { status: 400 } + ); + } + + // Extrair primeiro servidor da configuração (se for formato mcpServers) + let serverConfig: any; + let serverId: string; + + if (config.mcpServers) { + // Formato: { "mcpServers": { "server-name": { ... } } } + const serverNames = Object.keys(config.mcpServers); + if (serverNames.length === 0) { + return NextResponse.json( + { success: false, error: "No MCP server found in configuration" }, + { status: 400 } + ); + } + + serverId = mcpId || serverNames[0]; + serverConfig = config.mcpServers[serverNames[0]]; + } else { + // Formato direto: { "command": "...", "args": [...] } + serverId = mcpId || "manual-mcp"; + serverConfig = config; + } + + // Validar campos obrigatórios + if (!serverConfig.command) { + return NextResponse.json( + { success: false, error: "Missing required field: command" }, + { status: 400 } + ); + } + + console.log(`🚀 Starting manual installation for: ${serverId}`); + console.log( + `Command: ${serverConfig.command} ${(serverConfig.args || []).join(" ")}` + ); + + // Detectar tipo de pacote baseado no comando + const { packageRegistry, packageName, repositoryUrl } = + detectPackageInfo(serverConfig); + + console.log(`Detected package type: ${packageRegistry} (${packageName})`); + + // Verificar se já está instalado + if (MCPRepository.isInstalled(serverId)) { + const existing = MCPRepository.getInstalledMCP(serverId); + + if (existing.status === "ready") { + return NextResponse.json( + { + success: false, + error: `MCP ${serverId} is already installed`, + }, + { status: 400 } + ); + } + + console.log( + `Re-installing MCP ${serverId} (current status: ${existing.status})` + ); + } + + // Preparar configuração de instalação + const installConfig: InstallationConfig = { + mcpId: serverId, + packageName, + packageRegistry, + repositoryUrl, + config: serverConfig, + enableLogs: enableLogs === true, + }; + + // Salvar no banco como PENDING + if (!MCPRepository.isInstalled(serverId)) { + MCPRepository.saveInstallation(serverId, serverConfig, []); + } + MCPRepository.updateInstallationStatus( + serverId, + "pending", + "Starting manual installation..." + ); + + // Iniciar instalação em background + installInBackground(installConfig); + + return NextResponse.json({ + success: true, + message: "Manual installation started", + mcpId: serverId, + status: "pending", + detectedType: packageRegistry, + }); + } catch (error: any) { + console.error("Error starting manual installation:", error); + return NextResponse.json( + { + success: false, + error: error.message || "Failed to start manual installation", + }, + { status: 500 } + ); + } +} + +/** + * Detecta informações do pacote baseado no comando + */ +function detectPackageInfo(serverConfig: any): { + packageRegistry: PackageRegistry; + packageName: string; + repositoryUrl?: string; +} { + const command = serverConfig.command.toLowerCase(); + const args = serverConfig.args || []; + const firstArg = args[0] || ""; + + // NPM/NPX + if (command === "npx" || command === "npm") { + // npx @mcpcentral/mcp-time + // npx -y @modelcontextprotocol/server-filesystem + const packageArg = args.find( + (arg: string) => arg.startsWith("@") || !arg.startsWith("-") + ); + const packageName = packageArg || firstArg || "unknown-npm-package"; + + return { + packageRegistry: "npm", + packageName: packageName.replace(/^-y\s*/, "").trim(), + }; + } + + // Node + if (command === "node" || command.endsWith("node")) { + // node dist/index.js -> assume que é local ou já instalado + return { + packageRegistry: "npm", + packageName: "local-node-package", + }; + } + + // Python + if ( + command === "python" || + command === "python3" || + command.includes("python") + ) { + // python -m mcp_server_git + // python /path/to/server.py + const moduleIndex = args.indexOf("-m"); + if (moduleIndex !== -1 && args[moduleIndex + 1]) { + return { + packageRegistry: "pypi", + packageName: args[moduleIndex + 1], + }; + } + + return { + packageRegistry: "pypi", + packageName: "local-python-package", + }; + } + + // UV (Python package manager) + if (command === "uv" || command === "uvx") { + // uvx mcp-server-git + const packageName = args[0] || "unknown-uv-package"; + return { + packageRegistry: "pypi", + packageName, + }; + } + + // Cargo/Rust + if (command === "cargo") { + // cargo run --bin mcp-server + const binIndex = args.indexOf("--bin"); + if (binIndex !== -1 && args[binIndex + 1]) { + return { + packageRegistry: "cargo", + packageName: args[binIndex + 1], + }; + } + + return { + packageRegistry: "cargo", + packageName: "local-rust-package", + }; + } + + // Docker + if (command === "docker") { + return { + packageRegistry: "other", + packageName: "docker-container", + }; + } + + // Caminho absoluto ou relativo (executável local) + if (command.startsWith("/") || command.startsWith("./")) { + return { + packageRegistry: "other", + packageName: "local-executable", + }; + } + + // Padrão: tratar como "other" + return { + packageRegistry: "other", + packageName: command, + }; +} + +/** + * Executa instalação em background (não bloqueia resposta HTTP) + */ +async function installInBackground(config: InstallationConfig): Promise { + const { mcpId, enableLogs } = config; + + try { + console.log(`📦 Installing ${mcpId} in background (manual mode)...`); + + // Callback de progresso + const onProgress = (progress: InstallationProgress) => { + MCPRepository.updateInstallationStatus( + mcpId, + progress.status, + progress.message + ); + + if (enableLogs && progress.logs && progress.logs.length > 0) { + MCPRepository.appendLogs(mcpId, progress.logs); + } + + console.log( + `[${mcpId}] ${progress.status}: ${progress.message} (${progress.percentage}%)` + ); + }; + + // Instalar + onProgress({ + status: MCPInstallationStatus.DOWNLOADING, + message: "Preparing manual installation...", + percentage: 5, + }); + + const environment = await MCPInstallerService.install(config, onProgress); + + // Salvar ambiente + MCPRepository.saveEnvironment(mcpId, environment); + + onProgress({ + status: MCPInstallationStatus.TESTING, + message: "Validating MCP server...", + percentage: 85, + }); + + // Validar servidor MCP + const validation = await MCPValidatorService.validateMCPServer( + environment, + config.config + ); + + // Salvar resultado de validação + MCPRepository.saveValidationResult(mcpId, validation); + + if (validation.success) { + // Salvar ferramentas descobertas + MCPRepository.saveTools(mcpId, validation.tools); + + MCPRepository.updateInstallationStatus( + mcpId, + "ready", + `Installed successfully with ${validation.tools.length} tools` + ); + + console.log( + `✅ MCP ${mcpId} (manual) installed and validated successfully` + ); + } else { + // Validação falhou, mas instalação pode estar OK + console.warn( + `⚠️ MCP ${mcpId} (manual) installed but validation failed: ${validation.error}` + ); + + // Tentar validação simples como fallback + const simpleValidation = await MCPValidatorService.simpleValidation( + environment, + config.config + ); + + if (simpleValidation.success) { + MCPRepository.updateInstallationStatus( + mcpId, + "ready", + "Installed (validation partial)" + ); + console.log( + `✅ MCP ${mcpId} (manual) installed (simple validation passed)` + ); + } else { + throw new Error( + validation.error || simpleValidation.error || "Validation failed" + ); + } + } + } catch (error: any) { + console.error(`❌ Manual installation failed for ${mcpId}:`, error); + + MCPRepository.updateInstallationStatus( + mcpId, + "failed", + error.message || "Manual installation failed" + ); + + if (enableLogs) { + MCPRepository.appendLogs(mcpId, [ + `ERROR: ${error.message}`, + error.stack || "", + ]); + } + } +} diff --git a/app/api/mcp/install-status/route.ts b/app/api/mcp/install-status/route.ts new file mode 100644 index 0000000..02297ff --- /dev/null +++ b/app/api/mcp/install-status/route.ts @@ -0,0 +1,58 @@ +import { NextResponse } from "next/server"; +import { MCPRepository } from "@/database/repositories/mcp"; + +export const dynamic = "force-dynamic"; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const mcpId = searchParams.get("mcpId"); + + if (!mcpId) { + return NextResponse.json( + { success: false, error: "MCP ID is required" }, + { status: 400 } + ); + } + + const installation = MCPRepository.getInstalledMCP(mcpId); + + if (!installation) { + return NextResponse.json( + { success: false, error: "MCP not found" }, + { status: 404 } + ); + } + + // Parsear logs se existirem + let logs: string[] = []; + if (installation.install_logs) { + try { + logs = JSON.parse(installation.install_logs); + } catch { + logs = [installation.install_logs]; + } + } + + return NextResponse.json({ + success: true, + mcpId: installation.id, + status: installation.status || "unknown", + message: installation.status_message || "", + logs, + environment: installation.environment_path, + executableCommand: installation.executable_command, + installedAt: installation.installed_at, + updatedAt: installation.updated_at, + }); + } catch (error: any) { + console.error("Error getting installation status:", error); + return NextResponse.json( + { + success: false, + error: error.message || "Failed to get installation status", + }, + { status: 500 } + ); + } +} diff --git a/app/api/mcp/install/route.ts b/app/api/mcp/install/route.ts index cd1ffe7..63d3956 100644 --- a/app/api/mcp/install/route.ts +++ b/app/api/mcp/install/route.ts @@ -1,11 +1,18 @@ import { NextResponse } from "next/server"; -import { MCPInstallationStatus } from "@/lib/types/mcp"; -import { DeepNLPService } from "@/lib/services/deepnlp"; import { MCPRepository } from "@/database/repositories/mcp"; +import { MCPInstallerService } from "@/lib/services/mcp-installer"; +import { MCPValidatorService } from "@/lib/services/mcp-validator"; +import { DeepNLPService } from "@/lib/services/deepnlp"; +import { PulseMCPService } from "@/lib/services/pulsemcp"; +import { MCPCacheRepository } from "@/database/repositories/mcp-cache"; +import type { + InstallationConfig, + InstallationProgress, +} from "@/lib/types/mcp-installer"; export async function POST(request: Request) { try { - const { mcpId } = await request.json(); + const { mcpId, customConfig, enableLogs } = await request.json(); if (!mcpId) { return NextResponse.json( @@ -14,49 +21,219 @@ export async function POST(request: Request) { ); } - console.log(`Installing MCP: ${mcpId}`); + console.log(`🚀 Starting installation for MCP: ${mcpId}`); // Verificar se já está instalado if (MCPRepository.isInstalled(mcpId)) { - return NextResponse.json({ - success: false, - error: `MCP ${mcpId} is already installed`, - }, { status: 400 }); + const existing = MCPRepository.getInstalledMCP(mcpId); + + // Se já está instalado e pronto, retornar erro + if (existing.status === "ready") { + return NextResponse.json( + { + success: false, + error: `MCP ${mcpId} is already installed`, + }, + { status: 400 } + ); + } + + // Se está em processo de instalação ou falhou, permitir reinstalação + console.log( + `Re-installing MCP ${mcpId} (current status: ${existing.status})` + ); } - // Obter configuração completa do servidor - console.log(`Fetching server config for ${mcpId}`); - const serverConfig = await DeepNLPService.getServerConfig(mcpId); - - if (!serverConfig.items || serverConfig.items.length === 0) { - throw new Error(`No configuration found for MCP ${mcpId}`); + // Obter configuração + let serverConfig: any; + let packageName: string; + let packageRegistry: string | null = null; + let repositoryUrl: string | undefined; + + if (customConfig) { + // Usar configuração personalizada + console.log(`Using custom configuration for ${mcpId}`); + serverConfig = customConfig; + packageName = customConfig.packageName || mcpId; + packageRegistry = customConfig.packageRegistry || null; + repositoryUrl = customConfig.repositoryUrl; + } else { + // Buscar configuração da API + console.log(`Fetching configuration for ${mcpId}`); + + try { + // Tentar DeepNLP primeiro + const deepnlpConfig = await DeepNLPService.getServerConfig(mcpId); + if (deepnlpConfig.items && deepnlpConfig.items.length > 0) { + serverConfig = deepnlpConfig.items[0]; + } + } catch (error) { + console.log(`DeepNLP config not found, trying cache...`); + } + + // Se não encontrou, tentar cache PulseMCP + if (!serverConfig) { + const cachedMCP = MCPCacheRepository.findById(mcpId); + if (cachedMCP) { + serverConfig = PulseMCPService.transformToMCPProvider(cachedMCP); + packageName = cachedMCP.package_name || mcpId; + packageRegistry = cachedMCP.package_registry; + repositoryUrl = cachedMCP.source_code_url; + } + } + + if (!serverConfig) { + throw new Error(`No configuration found for MCP ${mcpId}`); + } + + // Extrair informações do pacote + packageName = serverConfig.packageName || serverConfig.name || mcpId; } - // Obter ferramentas disponíveis - console.log(`Fetching server tools for ${mcpId}`); - const tools = await DeepNLPService.getServerTools(mcpId); - - // Salvar configuração localmente - console.log(`Saving MCP ${mcpId} to local database`); - MCPRepository.saveInstallation(mcpId, serverConfig.items[0], tools); - - const result: MCPInstallationStatus = { - success: true, - message: `MCP ${mcpId} installed successfully`, + // Preparar configuração de instalação + const installConfig: InstallationConfig = { mcpId, + packageName, + packageRegistry: packageRegistry as any, + repositoryUrl, + config: serverConfig, + enableLogs: enableLogs === true, }; + // Salvar como PENDING no banco + if (!MCPRepository.isInstalled(mcpId)) { + MCPRepository.saveInstallation(mcpId, serverConfig, []); + } + MCPRepository.updateInstallationStatus( + mcpId, + "pending", + "Starting installation..." + ); + + // Iniciar instalação em background + installInBackground(installConfig); + return NextResponse.json({ success: true, - installation: result, - config: serverConfig.items[0], - tools: tools, + message: "Installation started", + mcpId, + status: "pending", }); } catch (error: any) { - console.error("Error installing MCP:", error); + console.error("Error starting installation:", error); return NextResponse.json( - { success: false, error: error.message || "Failed to install MCP" }, + { + success: false, + error: error.message || "Failed to start installation", + }, { status: 500 } ); } } + +/** + * Executa instalação em background (não bloqueia resposta HTTP) + */ +async function installInBackground(config: InstallationConfig): Promise { + const { mcpId, enableLogs } = config; + + try { + console.log(`📦 Installing ${mcpId} in background...`); + + // Callback de progresso + const onProgress = (progress: InstallationProgress) => { + MCPRepository.updateInstallationStatus( + mcpId, + progress.status, + progress.message + ); + + if (enableLogs && progress.logs && progress.logs.length > 0) { + MCPRepository.appendLogs(mcpId, progress.logs); + } + + console.log( + `[${mcpId}] ${progress.status}: ${progress.message} (${progress.percentage}%)` + ); + }; + + // Instalar + onProgress({ + status: "downloading", + message: "Preparing installation...", + percentage: 5, + }); + + const environment = await MCPInstallerService.install(config, onProgress); + + // Salvar ambiente + MCPRepository.saveEnvironment(mcpId, environment); + + onProgress({ + status: "testing", + message: "Validating MCP server...", + percentage: 85, + }); + + // Validar servidor MCP + const validation = await MCPValidatorService.validateMCPServer( + environment, + config.config + ); + + // Salvar resultado de validação + MCPRepository.saveValidationResult(mcpId, validation); + + if (validation.success) { + // Salvar ferramentas descobertas + MCPRepository.saveTools(mcpId, validation.tools); + + MCPRepository.updateInstallationStatus( + mcpId, + "ready", + `Installed successfully with ${validation.tools.length} tools` + ); + + console.log(`✅ MCP ${mcpId} installed and validated successfully`); + } else { + // Validação falhou, mas instalação pode estar OK + console.warn( + `⚠️ MCP ${mcpId} installed but validation failed: ${validation.error}` + ); + + // Tentar validação simples como fallback + const simpleValidation = await MCPValidatorService.simpleValidation( + environment, + config.config + ); + + if (simpleValidation.success) { + MCPRepository.updateInstallationStatus( + mcpId, + "ready", + "Installed (validation partial)" + ); + console.log(`✅ MCP ${mcpId} installed (simple validation passed)`); + } else { + throw new Error( + validation.error || simpleValidation.error || "Validation failed" + ); + } + } + } catch (error: any) { + console.error(`❌ Installation failed for ${mcpId}:`, error); + + MCPRepository.updateInstallationStatus( + mcpId, + "failed", + error.message || "Installation failed" + ); + + if (enableLogs) { + MCPRepository.appendLogs(mcpId, [ + `ERROR: ${error.message}`, + error.stack || "", + ]); + } + } +} diff --git a/app/api/mcp/installed/route.ts b/app/api/mcp/installed/route.ts new file mode 100644 index 0000000..5d4e1c1 --- /dev/null +++ b/app/api/mcp/installed/route.ts @@ -0,0 +1,74 @@ +import { NextResponse } from "next/server"; +import { MCPRepository } from "@/database/repositories/mcp"; +import { MCPCacheRepository } from "@/database/repositories/mcp-cache"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + try { + console.log("Fetching installed MCPs..."); + + // Buscar todos os MCPs instalados com detalhes + const installedMcps = MCPRepository.listInstalledWithDetails(); + + // Enriquecer com dados do cache do marketplace + const enrichedMcps = installedMcps.map((mcp) => { + // Tentar encontrar no cache usando diferentes critérios + const cacheEntry = + MCPCacheRepository.findByOwnerRepo(mcp.owner, mcp.repo) || + MCPCacheRepository.findByName(mcp.name) || + MCPCacheRepository.findById(mcp.id); + + if (cacheEntry) { + return { + ...mcp, + // Usar dados do cache quando disponíveis + author: + cacheEntry.publisher_id !== "Unknown" + ? cacheEntry.publisher_id + : mcp.owner, + description: cacheEntry.description || `MCP instalado: ${mcp.name}`, + repository: cacheEntry.detail_url || cacheEntry.website, + homepage: cacheEntry.website, + rating: cacheEntry.rating || 0, + totalRatings: cacheEntry.review_cnt || 0, + category: cacheEntry.category || "other", + tags: cacheEntry.content_tag_list + ? cacheEntry.content_tag_list.split(",") + : [], + }; + } + + return { + ...mcp, + author: mcp.owner !== "unknown" ? mcp.owner : "Unknown", + description: `MCP instalado: ${mcp.name}`, + repository: "", + homepage: "", + rating: 0, + totalRatings: 0, + category: "other", + tags: [], + status: mcp.status || "unknown", + statusMessage: mcp.status_message || "", + }; + }); + + console.log(`Found ${enrichedMcps.length} installed MCPs`); + + return NextResponse.json({ + success: true, + mcps: enrichedMcps, + total: enrichedMcps.length, + }); + } catch (error: any) { + console.error("Error fetching installed MCPs:", error); + return NextResponse.json( + { + success: false, + error: error.message || "Failed to fetch installed MCPs", + }, + { status: 500 } + ); + } +} diff --git a/app/api/mcp/list/route.ts b/app/api/mcp/list/route.ts index d1fe336..5bf4a2d 100644 --- a/app/api/mcp/list/route.ts +++ b/app/api/mcp/list/route.ts @@ -58,24 +58,36 @@ export async function GET(request: Request) { console.log(`Found ${cachedMcps.length} MCPs in cache`); // Os dados já estão no formato interno após transformação no saveBatch - const mcps = cachedMcps.map((item) => ({ - id: item.id, - name: item.content_name, - author: item.publisher_id, - description: item.description || "", - version: "1.0.0", - category: item.category as MCPProvider["category"], - tags: item.content_tag_list ? item.content_tag_list.split(",") : [], - rating: item.rating, - totalRatings: item.review_cnt, - repository: item.detail_url || "", - homepage: item.website || "", - installed: false, // Será definido abaixo - subfield: item.subfield || "", - field: item.field || "", - config: [], - tools: [], - })); + const mcps = cachedMcps.map((item) => { + // Construir ID no formato owner/repo a partir dos campos do cache + // Se owner estiver vazio, usar apenas o repo + const correctId = + item.owner && item.owner.trim() !== "" + ? `${item.owner}/${item.repo}` + : item.repo; + + return { + id: correctId, // Usar owner/repo como ID principal ou apenas repo se owner vazio + originalId: item.id, // Manter ID original para referência + owner: item.owner || "", + repo: item.repo, + name: item.content_name, + author: item.publisher_id, + description: item.description || "", + version: "1.0.0", + category: item.category as MCPProvider["category"], + tags: item.content_tag_list ? item.content_tag_list.split(",") : [], + rating: item.rating, + totalRatings: item.review_cnt, + repository: item.detail_url || "", + homepage: item.website || "", + installed: false, // Será definido abaixo + subfield: item.subfield || "", + field: item.field || "", + config: [], + tools: [], + }; + }); // Adicionar status de instalação const installedMCPs = MCPRepository.listInstalled(); diff --git a/app/api/mcp/server-config/route.ts b/app/api/mcp/server-config/route.ts new file mode 100644 index 0000000..945175d --- /dev/null +++ b/app/api/mcp/server-config/route.ts @@ -0,0 +1,112 @@ +import { NextResponse } from "next/server"; +import { DeepNLPService } from "@/lib/services/deepnlp"; +import { MCPCacheRepository } from "@/database/repositories/mcp-cache"; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const mcpId = searchParams.get("mcpId"); + + if (!mcpId) { + return NextResponse.json( + { success: false, error: "MCP ID is required" }, + { status: 400 } + ); + } + + console.log(`Fetching server config for: ${mcpId}`); + + // Primeiro, tentar buscar do cache local (PulseMCP) + const [owner, repo] = mcpId.includes("/") ? mcpId.split("/") : ["", mcpId]; + + // Buscar no cache usando ID completo ou apenas repo + const cachedMcps = MCPCacheRepository.search({ + query: repo, + limit: 1, + }); + + if (cachedMcps.length > 0) { + const cachedMCP = cachedMcps[0]; + + // Verificar se tem configuração no ext_info + if (cachedMCP.ext_info) { + try { + const extInfo = + typeof cachedMCP.ext_info === "string" + ? JSON.parse(cachedMCP.ext_info) + : cachedMCP.ext_info; + + if ( + extInfo.config && + Array.isArray(extInfo.config) && + extInfo.config.length > 0 + ) { + console.log(`Found config in cache for ${mcpId}`); + // Converter formato PulseMCP para formato esperado pelo modal + const serverConfig = extInfo.config.find( + (c: any) => c.type === "server" + ); + const envConfig = extInfo.config.find((c: any) => c.type === "env"); + + if (serverConfig) { + // Usar o nome do servidor config como chave + const serverName = + serverConfig.name || extInfo.package_name || cachedMCP.id; + + const formattedConfig = { + mcpServers: { + [serverName]: { + command: serverConfig.command || "npx", + args: serverConfig.args || [], + env: envConfig?.variables || {}, + }, + }, + }; + + return NextResponse.json({ + success: true, + config: formattedConfig, + source: "pulsemcp", + }); + } + } + } catch (error) { + console.error("Error parsing ext_info:", error); + } + } + } + + // Se não encontrou no cache, tentar DeepNLP como fallback + try { + const serverConfig = await DeepNLPService.getServerConfig(mcpId); + + if (serverConfig.items && serverConfig.items.length > 0) { + console.log(`Found config from DeepNLP for ${mcpId}`); + return NextResponse.json({ + success: true, + config: serverConfig.items[0], + source: "deepnlp", + }); + } + } catch (error) { + console.log(`DeepNLP lookup failed for ${mcpId}, continuing...`); + } + + // Se não encontrou configuração em nenhum lugar, retornar config vazio + console.log(`No config found for ${mcpId}, returning empty config`); + return NextResponse.json({ + success: true, + config: null, + source: "none", + }); + } catch (error: any) { + console.error("Error fetching server config:", error); + return NextResponse.json( + { + success: false, + error: error.message || "Failed to fetch server config", + }, + { status: 500 } + ); + } +} diff --git a/app/api/mcp/test-server/route.ts b/app/api/mcp/test-server/route.ts new file mode 100644 index 0000000..b50187e --- /dev/null +++ b/app/api/mcp/test-server/route.ts @@ -0,0 +1,128 @@ +import { NextResponse } from "next/server"; +import { MCPRepository } from "@/database/repositories/mcp"; +import { MCPValidatorService } from "@/lib/services/mcp-validator"; + +export const dynamic = "force-dynamic"; + +export async function POST(request: Request) { + try { + const { mcpId } = await request.json(); + + if (!mcpId) { + return NextResponse.json( + { success: false, error: "MCP ID é obrigatório" }, + { status: 400 } + ); + } + + console.log(`🧪 Testing MCP server: ${mcpId}`); + + // Buscar dados do MCP no banco + const mcpData = MCPRepository.getInstalledMCP(mcpId); + if (!mcpData) { + return NextResponse.json( + { success: false, error: "MCP não encontrado" }, + { status: 404 } + ); + } + + // Obter ambiente de instalação + const environment = MCPRepository.getEnvironment(mcpId); + + if (!environment) { + return NextResponse.json( + { + success: false, + error: + "MCP não está instalado corretamente. Ambiente não encontrado.", + }, + { status: 400 } + ); + } + + console.log( + `Testing environment: ${environment.executable} ${environment.args?.join( + " " + )}` + ); + + // Validar servidor usando protocolo JSON-RPC MCP + const validationResult = await MCPValidatorService.validateMCPServer( + environment, + mcpData.config + ); + + // Salvar resultado da validação + MCPRepository.saveValidationResult(mcpId, validationResult); + + if (validationResult.success) { + // Atualizar ferramentas se validação foi bem-sucedida + if (validationResult.tools && validationResult.tools.length > 0) { + MCPRepository.saveTools(mcpId, validationResult.tools); + } + + console.log(`✅ Test successful for ${mcpId}:`, { + protocol: validationResult.protocol, + tools: validationResult.tools.length, + }); + + return NextResponse.json({ + success: true, + result: { + success: true, + protocol: validationResult.protocol, + toolsCount: validationResult.tools.length, + tools: validationResult.tools, + capabilities: validationResult.capabilities, + }, + }); + } + + // Se validação falhou, tentar validação simples + console.warn(`⚠️ JSON-RPC validation failed, trying simple validation...`); + + const simpleResult = await MCPValidatorService.simpleValidation( + environment, + mcpData.config, + 5000 + ); + + if (simpleResult.success) { + console.log(`✅ Simple validation passed for ${mcpId}`); + + return NextResponse.json({ + success: true, + result: { + success: true, + protocol: "unknown", + message: "Server starts but JSON-RPC validation failed", + toolsCount: 0, + tools: [], + }, + }); + } + + // Ambas validações falharam + console.error(`❌ All validations failed for ${mcpId}`); + + return NextResponse.json({ + success: false, + error: + validationResult.error || simpleResult.error || "Validation failed", + result: { + success: false, + protocol: validationResult.protocol, + error: validationResult.error, + }, + }); + } catch (error: any) { + console.error("Error testing MCP server:", error); + return NextResponse.json( + { + success: false, + error: error.message || "Internal server error", + }, + { status: 500 } + ); + } +} diff --git a/app/api/mcp/uninstall/route.ts b/app/api/mcp/uninstall/route.ts index d4d9f1b..73880a1 100644 --- a/app/api/mcp/uninstall/route.ts +++ b/app/api/mcp/uninstall/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; -import { MCPInstallationStatus } from "@/lib/types/mcp"; import { MCPRepository } from "@/database/repositories/mcp"; +import { MCPEnvironmentService } from "@/lib/services/mcp-environment"; export async function DELETE(request: Request) { try { @@ -13,28 +13,44 @@ export async function DELETE(request: Request) { ); } - console.log(`Uninstalling MCP: ${mcpId}`); + console.log(`🗑️ Uninstalling MCP: ${mcpId}`); // Verificar se está instalado if (!MCPRepository.isInstalled(mcpId)) { - return NextResponse.json({ - success: false, - error: `MCP ${mcpId} is not installed`, - }, { status: 400 }); + return NextResponse.json( + { + success: false, + error: `MCP ${mcpId} is not installed`, + }, + { status: 400 } + ); } + // Obter ambiente antes de remover do banco + const environment = MCPRepository.getEnvironment(mcpId); + // Remover do banco local MCPRepository.removeInstallation(mcpId); - const result: MCPInstallationStatus = { - success: true, - message: `MCP ${mcpId} uninstalled successfully`, - mcpId, - }; + console.log(`✓ MCP ${mcpId} removed from database`); + + // Limpar arquivos de instalação se ambiente existe + if (environment) { + try { + await MCPEnvironmentService.cleanupEnvironment(environment); + console.log(`✓ MCP ${mcpId} environment cleaned up`); + } catch (error: any) { + console.warn( + `⚠️ Could not clean up environment for ${mcpId}: ${error.message}` + ); + // Não falhar a desinstalação se limpeza falhar + } + } return NextResponse.json({ success: true, - installation: result, + message: `MCP ${mcpId} uninstalled successfully`, + mcpId, }); } catch (error: any) { console.error("Error uninstalling MCP:", error); diff --git a/app/api/mcp/update-config/route.ts b/app/api/mcp/update-config/route.ts new file mode 100644 index 0000000..9b65b62 --- /dev/null +++ b/app/api/mcp/update-config/route.ts @@ -0,0 +1,52 @@ +import { NextResponse } from "next/server"; +import { MCPRepository } from "@/database/repositories/mcp"; + +export async function PUT(request: Request) { + try { + const { mcpId, newConfig } = await request.json(); + + if (!mcpId) { + return NextResponse.json( + { success: false, error: "MCP ID is required" }, + { status: 400 } + ); + } + + if (!newConfig) { + return NextResponse.json( + { success: false, error: "New configuration is required" }, + { status: 400 } + ); + } + + console.log(`Updating config for MCP: ${mcpId}`); + + // Verificar se o MCP está instalado + if (!MCPRepository.isInstalled(mcpId)) { + return NextResponse.json( + { + success: false, + error: `MCP ${mcpId} is not installed`, + }, + { status: 404 } + ); + } + + // Atualizar configuração + MCPRepository.updateConfig(mcpId, newConfig); + + console.log(`MCP ${mcpId} configuration updated successfully`); + + return NextResponse.json({ + success: true, + message: `MCP ${mcpId} configuration updated successfully`, + mcpId, + }); + } catch (error: any) { + console.error("Error updating MCP configuration:", error); + return NextResponse.json( + { success: false, error: error.message || "Failed to update MCP configuration" }, + { status: 500 } + ); + } +} diff --git a/components/mcp/MCPCard.tsx b/components/mcp/MCPCard.tsx index 42de537..b8e2689 100644 --- a/components/mcp/MCPCard.tsx +++ b/components/mcp/MCPCard.tsx @@ -1,5 +1,6 @@ "use client"; +import { memo } from "react"; import { Package, Star, @@ -7,6 +8,9 @@ import { ExternalLink, CheckCircle, Zap, + Settings, + PlayCircle, + Loader2, } from "lucide-react"; import { MCPProvider } from "@/lib/types/mcp"; @@ -15,171 +19,254 @@ interface MCPCardProps { onInstall: () => void; onUninstall: () => void; onViewDetails: () => void; + mode?: "marketplace" | "installed"; + onEditConfig?: () => void; + onTestServer?: () => void; + isTesting?: boolean; + installingStatus?: { + status: string; + message: string; + percentage: number; + }; } -export function MCPCard({ - mcp, - onInstall, - onUninstall, - onViewDetails, -}: MCPCardProps) { - const getCategoryColor = (category: string) => { - switch (category) { - case "map": - return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"; - case "browser": - return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"; - case "office": - return "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"; - case "search": - return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"; - case "database": - return "bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200"; - case "finance": - return "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200"; - case "code": - return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"; - case "chart": - return "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200"; - case "payment": - return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"; - default: - return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200"; - } - }; +export const MCPCard = memo( + function MCPCard({ + mcp, + onInstall, + onUninstall, + onViewDetails, + mode = "marketplace", + onEditConfig, + onTestServer, + isTesting = false, + installingStatus, + }: MCPCardProps) { + const getCategoryColor = (category: string) => { + switch (category) { + case "map": + return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"; + case "browser": + return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"; + case "office": + return "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"; + case "search": + return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"; + case "database": + return "bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200"; + case "finance": + return "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200"; + case "code": + return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"; + case "chart": + return "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200"; + case "payment": + return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"; + default: + return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200"; + } + }; - const formatDownloads = (downloads: number) => { - if (downloads >= 1000000) return `${(downloads / 1000000).toFixed(1)}M`; - if (downloads >= 1000) return `${(downloads / 1000).toFixed(1)}K`; - return downloads.toString(); - }; + const formatDownloads = (downloads: number) => { + if (downloads >= 1000000) return `${(downloads / 1000000).toFixed(1)}M`; + if (downloads >= 1000) return `${(downloads / 1000).toFixed(1)}K`; + return downloads.toString(); + }; - const isOfficial = mcp.tags?.includes("official") || false; - const toolCount = mcp.tools?.length || 0; + const isOfficial = mcp.tags?.includes("official") || false; + const toolCount = mcp.tools?.length || 0; - return ( -
- {/* Header */} -
-
-
- -
-
-
-

{mcp.name}

- {isOfficial && } + return ( +
+ {/* Header */} +
+
+
+
-

- by {mcp.author} +

+
+

{mcp.name}

+ {isOfficial && ( + + )} +
+

+ by {mcp.author} +

+
+
+ + {mcp.category} + +
+ + {/* Description */} +

+ {mcp.description} +

+ + {/* Stats */} +
+
+ + {mcp.rating} + ({mcp.totalRatings}) +
+ {toolCount > 0 && ( +
+ + {toolCount} tools +
+ )} + + v{mcp.version} + +
+ + {/* Tags */} +
+ {mcp.tags.slice(0, 3).map((tag) => ( + + {tag} + + ))} + {mcp.tags.length > 3 && ( + + +{mcp.tags.length - 3} + + )} +
+ + {/* Installation Progress */} + {installingStatus && ( +
+
+ + + Installing... + +
+
+
+
+

+ {installingStatus.message}

-
- - {mcp.category} - -
+ )} - {/* Description */} -

- {mcp.description} -

+ {/* Actions */} +
+ - {/* Stats */} -
-
- - {mcp.rating} - ({mcp.totalRatings}) + {mode === "installed" && onEditConfig ? ( + <> + + {onTestServer && ( + + )} + + + ) : mcp.installed ? ( + + ) : ( + + )}
- {toolCount > 0 && ( -
- - {toolCount} tools + + {/* Links */} + {(mcp.repository || mcp.homepage) && ( +
+
+ {mcp.repository && ( + + + Repository + + )} + {mcp.homepage && ( + + + Homepage + + )} +
)} - - v{mcp.version} -
- - {/* Tags */} -
- {mcp.tags.slice(0, 3).map((tag) => ( - - {tag} - - ))} - {mcp.tags.length > 3 && ( - - +{mcp.tags.length - 3} - - )} -
- - {/* Actions */} -
- - - {mcp.installed ? ( - - ) : ( - - )} -
- - {/* Links */} - {(mcp.repository || mcp.homepage) && ( -
-
- {mcp.repository && ( - - - Repository - - )} - {mcp.homepage && ( - - - Homepage - - )} -
-
- )} -
- ); -} + ); + }, + (prevProps, nextProps) => { + // Custom comparison para evitar re-renders desnecessários + return ( + prevProps.mcp.id === nextProps.mcp.id && + prevProps.mcp.installed === nextProps.mcp.installed && + prevProps.mcp.rating === nextProps.mcp.rating && + prevProps.mcp.totalRatings === nextProps.mcp.totalRatings && + prevProps.mcp.name === nextProps.mcp.name && + prevProps.mcp.description === nextProps.mcp.description && + prevProps.mode === nextProps.mode && + prevProps.isTesting === nextProps.isTesting + ); + } +); diff --git a/components/mcp/MCPInstallConfigModal.tsx b/components/mcp/MCPInstallConfigModal.tsx new file mode 100644 index 0000000..f7c533a --- /dev/null +++ b/components/mcp/MCPInstallConfigModal.tsx @@ -0,0 +1,272 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + X, + Package, + AlertCircle, + CheckCircle, + Info, + ExternalLink, +} from "lucide-react"; +import { MCPProvider } from "@/lib/types/mcp"; + +interface MCPInstallConfigModalProps { + mcp: MCPProvider; + config: any; // Configuração inicial do servidor + isOpen: boolean; + onClose: () => void; + onConfirm: (editedConfig: any) => void; + isEditMode?: boolean; // Novo prop para indicar modo de edição +} + +export function MCPInstallConfigModal({ + mcp, + config, + isOpen, + onClose, + onConfirm, + isEditMode = false, +}: MCPInstallConfigModalProps) { + const [jsonString, setJsonString] = useState(""); + const [isValidJson, setIsValidJson] = useState(true); + const [jsonError, setJsonError] = useState(""); + const [hasEnvVars, setHasEnvVars] = useState(false); + + // Inicializar JSON quando config mudar + useEffect(() => { + if (config) { + try { + const formattedJson = JSON.stringify(config, null, 2); + setJsonString(formattedJson); + setIsValidJson(true); + setJsonError(""); + + // Verificar se tem variáveis de ambiente + const hasEnv = + JSON.stringify(config).includes('"env"') || + JSON.stringify(config).includes("env:"); + setHasEnvVars(hasEnv); + } catch (error) { + console.error("Error formatting config:", error); + setJsonString("{}"); + setIsValidJson(false); + setJsonError("Erro ao formatar configuração"); + } + } else { + // Config vazio - criar template baseado no MCP + // Usar npm ou nome do repositório se disponível + const serverName = mcp.id || mcp.name.toLowerCase().replace(/\s+/g, "-"); + const command = mcp.repository?.includes("npm") + ? "npx" + : mcp.repository + ? "node" + : "npm"; + + const template = { + mcpServers: { + [serverName]: { + command: command, + args: command === "npx" ? ["-y", serverName] : [], + env: {}, + }, + }, + }; + + setJsonString(JSON.stringify(template, null, 2)); + setIsValidJson(true); + setJsonError(""); + setHasEnvVars(false); + } + }, [config]); + + // Validar JSON em tempo real + const validateJson = (value: string) => { + try { + JSON.parse(value); + setIsValidJson(true); + setJsonError(""); + return true; + } catch (error) { + setIsValidJson(false); + setJsonError(error instanceof Error ? error.message : "JSON inválido"); + return false; + } + }; + + const handleJsonChange = (value: string) => { + setJsonString(value); + validateJson(value); + }; + + const handleConfirm = () => { + if (!isValidJson) { + return; + } + + try { + const parsedConfig = JSON.parse(jsonString); + onConfirm(parsedConfig); + } catch (error) { + console.error("Error parsing config:", error); + } + }; + + if (!isOpen) return null; + + return ( +
+
+ +
+ {/* Header */} +
+
+
+ +
+
+

+ {isEditMode ? "Editar Configuração" : "Configurar Instalação"} +

+
+

+ {mcp.name} por {mcp.author} +

+ {(mcp.repository || mcp.homepage) && ( + + + GitHub + + )} +
+
+
+ +
+ + {/* Instructions */} +
+
+ +
+

+ Instruções de Configuração +

+
    +
  • + • Edite apenas os campos necessários, + especialmente variáveis de ambiente (env) +
  • +
  • + • Adicione suas API keys,{" "} + tokens e credenciais nos + campos env +
  • +
  • + • Mantenha a estrutura JSON válida - use aspas duplas e + vírgulas corretamente +
  • +
  • + • Campos como "command" geralmente não precisam ser alterados +
  • +
+
+
+
+ + {/* JSON Editor */} +
+
+

Configuração do Servidor

+
+ {isValidJson ? ( +
+ + JSON válido +
+ ) : ( +
+ + JSON inválido +
+ )} +
+
+ + {/* Error Message */} + {!isValidJson && jsonError && ( +
+
+ +
+

Erro no JSON:

+

{jsonError}

+
+
+
+ )} + + {/* JSON Textarea - Área maior */} +
+