diff --git a/app/api/mcp/available-for-chat/route.ts b/app/api/mcp/available-for-chat/route.ts index 837a3fd..fe65f60 100644 --- a/app/api/mcp/available-for-chat/route.ts +++ b/app/api/mcp/available-for-chat/route.ts @@ -15,7 +15,7 @@ export async function GET() { const allMcps = MCPRepository.getAllInstalled(); const availableMcps = allMcps - .filter((mcp: any) => mcp.status === "ready") + .filter((mcp: any) => !mcp.status || mcp.status === "ready") .map((mcp: any) => { const tools = MCPExecutor.getAvailableTools(mcp.id); diff --git a/app/api/mcp/categories/route.ts b/app/api/mcp/categories/route.ts deleted file mode 100644 index 0d51000..0000000 --- a/app/api/mcp/categories/route.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { NextResponse } from "next/server"; -import { MCPCacheRepository } from "@/database/repositories/mcp-cache"; - -export const dynamic = "force-dynamic"; - -/** - * GET /api/mcp/categories - Obter categorias dinâmicas do cache - */ -export async function GET(request: Request) { - try { - console.log("📊 Fetching dynamic categories from cache..."); - - // Verificar se cache existe e tem dados - const cacheExists = MCPCacheRepository.tableExists("mcp_marketplace_cache"); - if (!cacheExists) { - return NextResponse.json({ - success: false, - error: "Cache not initialized", - categories: { - primary: [], - others: [], - }, - }); - } - - // Obter categorias dinâmicas - const categories = MCPCacheRepository.getCategories(); - const stats = MCPCacheRepository.getCategoryStats(); - - // Verificar se há dados no cache - if (stats.totalMCPs === 0) { - return NextResponse.json({ - success: false, - error: "No MCPs found in cache", - categories: { - primary: [], - others: [], - }, - stats, - }); - } - - console.log( - `📊 Categories extracted: ${categories.primary.length} primary, ${categories.others.length} others` - ); - - return NextResponse.json({ - success: true, - categories, - stats, - lastSync: MCPCacheRepository.getLastSync(), - cacheValid: MCPCacheRepository.isCacheValid(), - }); - } catch (error: any) { - console.error("Error fetching categories:", error); - return NextResponse.json( - { - success: false, - error: error.message || "Failed to fetch categories", - categories: { - primary: [], - others: [], - }, - }, - { status: 500 } - ); - } -} diff --git a/app/api/mcp/install/route.ts b/app/api/mcp/install/route.ts index 63d3956..a4dfd04 100644 --- a/app/api/mcp/install/route.ts +++ b/app/api/mcp/install/route.ts @@ -3,8 +3,6 @@ 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, @@ -71,17 +69,6 @@ export async function POST(request: Request) { 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}`); } diff --git a/app/api/mcp/installed/route.ts b/app/api/mcp/installed/route.ts index 5d4e1c1..69cbdc2 100644 --- a/app/api/mcp/installed/route.ts +++ b/app/api/mcp/installed/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { MCPRepository } from "@/database/repositories/mcp"; -import { MCPCacheRepository } from "@/database/repositories/mcp-cache"; +// Marketplace cache removido export const dynamic = "force-dynamic"; @@ -11,48 +11,20 @@ export async function GET() { // 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 || "", - }; - }); + // Retornar dados de instalados sem enriquecimento de marketplace + const enrichedMcps = installedMcps.map((mcp) => ({ + ...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`); diff --git a/app/api/mcp/list/route.ts b/app/api/mcp/list/route.ts deleted file mode 100644 index 5bf4a2d..0000000 --- a/app/api/mcp/list/route.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { NextResponse } from "next/server"; -import { - MCPProvider, - MCPSearchParams, - PulseMCPSearchParams, -} from "@/lib/types/mcp"; -import { PulseMCPService } from "@/lib/services/pulsemcp"; -import { MCPRepository } from "@/database/repositories/mcp"; -import { MCPCacheRepository } from "@/database/repositories/mcp-cache"; -import { MCPSyncService } from "@/lib/services/mcp-sync"; - -const PULSEMCP_API_BASE = "https://api.pulsemcp.com/v0beta"; - -export const dynamic = "force-dynamic"; - -export async function GET(request: Request) { - try { - const { searchParams } = new URL(request.url); - const category = searchParams.get("category") || undefined; - const search = searchParams.get("search") || undefined; - const sort = - (searchParams.get("sort") as MCPSearchParams["sort"]) || "rating"; - const order = (searchParams.get("order") as "asc" | "desc") || "desc"; - const page = parseInt(searchParams.get("page") || "0"); - const limit = parseInt(searchParams.get("limit") || "2000"); // Aumentado para 2000 - - console.log("MCP List API called with params:", { - category, - search, - sort, - order, - page, - limit, - }); - - // 1. Verificar se cache do DB é válido - const cacheValid = MCPCacheRepository.isCacheValid(); - - if (!cacheValid) { - // Iniciar sincronização em background se necessário - if (!MCPSyncService.isSyncing()) { - console.log("Cache invalid, starting background sync..."); - MCPSyncService.backgroundSync().catch(console.error); - } - } - - // 2. Tentar buscar do banco de dados primeiro - const cachedMcps = MCPCacheRepository.search({ - query: search, - category: category !== "all" ? category : undefined, - limit, - offset: page * limit, - sortBy: sort, - order: order, - }); - - if (cachedMcps.length > 0 || cacheValid) { - 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) => { - // 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(); - const mcpsWithStatus = mcps.map((mcp) => ({ - ...mcp, - installed: installedMCPs.includes(mcp.id), - })); - - const total = MCPCacheRepository.count({ - category: category !== "all" ? category : undefined, - }); - - return NextResponse.json({ - success: true, - mcps: mcpsWithStatus, - total, - hasMore: (page + 1) * limit < total, - cached: true, - lastSync: MCPCacheRepository.getLastSync(), - }); - } - - // 3. Fallback: buscar da API PulseMCP se cache vazio - console.log("Cache empty, falling back to PulseMCP API"); - - const pulseMCPParams: PulseMCPSearchParams = { - query: search, - count_per_page: limit, - offset: page * limit, - }; - - console.log("Calling PulseMCP API with params:", pulseMCPParams); - - const response = await fetch( - `${PULSEMCP_API_BASE}/servers?${new URLSearchParams({ - ...(search && { query: search }), - count_per_page: limit.toString(), - offset: (page * limit).toString(), - })}`, - { - method: "GET", - headers: { - Accept: "application/json", - "User-Agent": "Ollahub-MCP-Client/1.0 (https://ollahub.com)", - }, - } - ); - - if (!response.ok) { - throw new Error( - `PulseMCP API error: ${response.status} ${response.statusText}` - ); - } - - const data = await response.json(); - const mcps = data.servers.map((item: any) => - PulseMCPService.transformToMCPProvider(item) - ); - - // Adicionar status de instalação - const installedMCPs = MCPRepository.listInstalled(); - const mcpsWithStatus = mcps.map((mcp: MCPProvider) => ({ - ...mcp, - installed: installedMCPs.includes(mcp.id), - })); - - return NextResponse.json({ - success: true, - mcps: mcpsWithStatus, - total: data.total_count, - hasMore: data.total_count > (page + 1) * limit, - cached: false, - }); - } catch (error: any) { - console.error("Error fetching MCPs from PulseMCP:", error); - - // Fallback para dados mock em caso de erro - console.log("Falling back to mock data due to API error"); - - const mockMCPs: MCPProvider[] = [ - { - id: "google-maps/google-maps", - name: "Google Maps", - author: "Google", - description: - "Google Maps Location services, directions, and place details", - version: "1.0.0", - category: "map", - tags: ["maps", "location", "directions"], - rating: 4.5, - totalRatings: 2, - repository: - "https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps", - installed: false, - subfield: "MAP", - field: "MCP SERVER", - config: [], - tools: [], - }, - ]; - - return NextResponse.json({ - success: true, - mcps: mockMCPs, - total: mockMCPs.length, - hasMore: false, - fallback: true, - error: error.message, - }); - } -} diff --git a/app/api/mcp/server-config/route.ts b/app/api/mcp/server-config/route.ts index 945175d..4416836 100644 --- a/app/api/mcp/server-config/route.ts +++ b/app/api/mcp/server-config/route.ts @@ -1,6 +1,5 @@ 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 { @@ -16,67 +15,9 @@ export async function GET(request: Request) { console.log(`Fetching server config for: ${mcpId}`); - // Primeiro, tentar buscar do cache local (PulseMCP) - const [owner, repo] = mcpId.includes("/") ? mcpId.split("/") : ["", mcpId]; + // Marketplace removido: pular cache local - // 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 + // Tentar DeepNLP try { const serverConfig = await DeepNLPService.getServerConfig(mcpId); diff --git a/app/api/mcp/sync/route.ts b/app/api/mcp/sync/route.ts deleted file mode 100644 index e8a6bcc..0000000 --- a/app/api/mcp/sync/route.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { NextResponse } from "next/server"; -import { MCPSyncService } from "@/lib/services/mcp-sync"; -import { MCPCacheRepository } from "@/database/repositories/mcp-cache"; - -export const dynamic = "force-dynamic"; - -/** - * POST /api/mcp/sync - Iniciar sincronização manual - */ -export async function POST(request: Request) { - try { - if (MCPSyncService.isSyncing()) { - return NextResponse.json({ - success: false, - error: "Sync already in progress" - }, { status: 409 }); - } - - console.log("🔄 Starting manual MCP sync..."); - - const result = await MCPSyncService.syncAll((progress) => { - console.log(`📥 Progress: ${progress.current}/${progress.total} (${progress.page}/${progress.totalPages} pages)`); - }); - - return NextResponse.json({ - success: result.success, - totalDownloaded: result.totalDownloaded, - errors: result.errors, - duration: result.duration - }); - } catch (error: any) { - console.error("Manual sync failed:", error); - return NextResponse.json( - { success: false, error: error.message || "Failed to sync MCPs" }, - { status: 500 } - ); - } -} - -/** - * GET /api/mcp/sync - Obter status da sincronização - */ -export async function GET(request: Request) { - try { - const stats = MCPCacheRepository.getStats(); - const healthCheck = await MCPSyncService.healthCheck(); - - return NextResponse.json({ - isSyncing: MCPSyncService.isSyncing(), - lastSync: stats.lastSync, - totalCached: stats.totalItems, - needsSync: MCPCacheRepository.needsSync(), - cacheValid: stats.isValid, - categories: stats.categories, - healthCheck - }); - } catch (error: any) { - console.error("Error getting sync status:", error); - return NextResponse.json( - { success: false, error: error.message || "Failed to get sync status" }, - { status: 500 } - ); - } -} - -/** - * DELETE /api/mcp/sync - Cancelar sincronização atual - */ -export async function DELETE(request: Request) { - try { - if (!MCPSyncService.isSyncing()) { - return NextResponse.json({ - success: false, - error: "No sync in progress" - }, { status: 400 }); - } - - MCPSyncService.cancelSync(); - - return NextResponse.json({ - success: true, - message: "Sync cancelled successfully" - }); - } catch (error: any) { - console.error("Error cancelling sync:", error); - return NextResponse.json( - { success: false, error: error.message || "Failed to cancel sync" }, - { status: 500 } - ); - } -} diff --git a/app/api/mcp/test-pulsemcp/route.ts b/app/api/mcp/test-pulsemcp/route.ts deleted file mode 100644 index c3d8028..0000000 --- a/app/api/mcp/test-pulsemcp/route.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { NextResponse } from "next/server"; -import { PulseMCPService } from "@/lib/services/pulsemcp"; - -const PULSEMCP_API_BASE = "https://api.pulsemcp.com/v0beta"; - -export const dynamic = "force-dynamic"; - -export async function GET(request: Request) { - try { - const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get("limit") || "20"); - const offset = parseInt(searchParams.get("offset") || "0"); - - console.log("🔍 Testando PulseMCP API diretamente..."); - - // Chamar API PulseMCP diretamente - const response = await fetch( - `${PULSEMCP_API_BASE}/servers?${new URLSearchParams({ - count_per_page: limit.toString(), - offset: offset.toString(), - })}`, - { - method: "GET", - headers: { - Accept: "application/json", - "User-Agent": "Ollahub-MCP-Client/1.0 (https://ollahub.com)", - }, - } - ); - - if (!response.ok) { - throw new Error( - `PulseMCP API error: ${response.status} ${response.statusText}` - ); - } - - const data = await response.json(); - console.log( - `✅ PulseMCP API respondeu com ${data.servers.length} servidores` - ); - - // Transformar para formato interno - const mcps = data.servers.map((item: any) => - PulseMCPService.transformToMCPProvider(item) - ); - - return NextResponse.json({ - success: true, - mcps: mcps, - total: data.total_count, - hasMore: data.total_count > offset + limit, - source: "pulsemcp-direct", - message: `Listando ${mcps.length} servidores diretamente da API PulseMCP`, - }); - } catch (error: any) { - console.error("❌ Erro ao chamar PulseMCP API:", error); - - return NextResponse.json( - { - success: false, - error: error.message, - mcps: [], - total: 0, - source: "pulsemcp-direct-error", - }, - { status: 500 } - ); - } -} diff --git a/app/api/ollama/chat/route.ts b/app/api/ollama/chat/route.ts index b21292e..1297ba3 100644 --- a/app/api/ollama/chat/route.ts +++ b/app/api/ollama/chat/route.ts @@ -70,6 +70,9 @@ export async function POST(req: NextRequest) { async start(controller) { let isClosed = false; let accumulatedResponse = ""; + let toolExecuted = false; + let lastExtractedToolCall: { name: string; parameters: any } | null = + null; const safeEnqueue = (data: string) => { if (!isClosed) { @@ -95,14 +98,24 @@ export async function POST(req: NextRequest) { try { const ollamaMessages = convertToOllamaMessages(messages); + console.log("🗣️ [CHAT] Messages ->", ollamaMessages); // Merge system prompt com tools prompt const finalSystemPrompt = toolsPrompt ? `${system || ""}\n\n${toolsPrompt}` : system; + if (toolsPrompt) console.log("🧩 [CHAT] Tools prompt injected"); // Preparar parâmetros para chat const chatParams = capabilities.supportsNativeTools ? { tools } : {}; + if (capabilities.supportsNativeTools) { + console.log( + "🧰 [CHAT] Native tools enabled:", + tools?.map((t: any) => t.function?.name) + ); + } else { + console.log("🧰 [CHAT] Prompt-engineering tools mode"); + } const response = await chatWithStream( model, @@ -111,28 +124,72 @@ export async function POST(req: NextRequest) { finalSystemPrompt, capabilities.supportsNativeTools ? tools : undefined ); + console.log("🌊 [CHAT] Streaming started for model:", model); + + const normalizeParameters = (params: any) => { + try { + if (typeof params === "string") { + params = JSON.parse(params); + } + } catch { + // ignora erro de parse e mantém original + } + if (params && typeof params === "object") { + const cleaned: Record = {}; + for (const [k, v] of Object.entries(params)) { + if (v && typeof v === "object" && Object.keys(v).length === 0) { + // remove objetos vazios que costumam vir de UIs (ex.: count: {}) + continue; + } + cleaned[k] = v; + } + return cleaned; + } + return params ?? {}; + }; for await (const chunk of response) { + console.log("📦 [CHAT stream chunk]", JSON.stringify(chunk)); // Verificar se há tool calls (modelos com suporte nativo) if ( - chunk.message?.tool_calls && - chunk.message.tool_calls.length > 0 + (chunk.message?.tool_calls && + chunk.message.tool_calls.length > 0) || + (Array.isArray((chunk as any).tool_calls) && + (chunk as any).tool_calls.length > 0) ) { console.log("🔧 Model requested tool calls (native)"); - for (const toolCall of chunk.message.tool_calls) { + const nativeCalls = + chunk.message?.tool_calls || (chunk as any).tool_calls || []; + for (const toolCall of nativeCalls) { const toolKey = toolCall.function.name; const toolInfo = mcpToolsMap.get(toolKey); + console.log( + "🧭 [CHAT] Native tool call:", + toolKey, + "->", + toolInfo + ); if (toolInfo) { // Executar tool const result = await MCPExecutor.executeMCPTool( toolInfo.mcpId, toolInfo.toolName, - toolCall.function.arguments || {} + normalizeParameters( + toolCall.function.arguments || + toolCall.function.parameters || + {} + ) ); if (result.success) { + toolExecuted = true; + console.log("✅ [CHAT] Tool executed successfully", { + mcpId: toolInfo.mcpId, + toolName: toolInfo.toolName, + executionTime: result.executionTime, + }); // Enviar indicador de execução de tool safeEnqueue( JSON.stringify({ @@ -149,6 +206,10 @@ export async function POST(req: NextRequest) { toolInfo.toolName, result.result ); + console.log( + "🧩 [CHAT] Injecting tool result into context (length)", + String(resultMessage).length + ); // Fazer nova chamada ao modelo com o resultado const updatedMessages = [ @@ -189,13 +250,23 @@ export async function POST(req: NextRequest) { } } - // Se não há suporte nativo, verificar se resposta contém tool call via prompt engineering - if (!capabilities.supportsNativeTools && accumulatedResponse) { + // Verificar se resposta contém tool call via prompt engineering (fallback também para nativo) + if (accumulatedResponse) { + console.log( + "🧾 [CHAT] Accumulated response length:", + accumulatedResponse.length + ); + console.log( + "🧾 [CHAT] Accumulated preview:", + accumulatedResponse.slice(0, 400) + ); const extractedToolCall = extractToolCallFromResponse(accumulatedResponse); if (extractedToolCall) { console.log("🔧 Model requested tool call (prompt engineering)"); + lastExtractedToolCall = extractedToolCall; + console.log("🧭 [CHAT] Extracted tool call:", extractedToolCall); // Encontrar MCP e tool let foundToolInfo: { mcpId: string; toolName: string } | null = @@ -216,10 +287,11 @@ export async function POST(req: NextRequest) { const result = await MCPExecutor.executeMCPTool( foundToolInfo.mcpId, foundToolInfo.toolName, - extractedToolCall.parameters + normalizeParameters(extractedToolCall.parameters) ); if (result.success) { + toolExecuted = true; // Limpar resposta anterior (que continha o JSON) safeEnqueue(JSON.stringify({ clearPrevious: true }) + "\n"); @@ -266,6 +338,10 @@ export async function POST(req: NextRequest) { } } } + } else { + console.log( + "🕵️ [CHAT] No tool call extracted from accumulated response" + ); } } diff --git a/components/ClientHome.tsx b/components/ClientHome.tsx index dc88f5d..510c44e 100644 --- a/components/ClientHome.tsx +++ b/components/ClientHome.tsx @@ -8,7 +8,7 @@ import { SettingsButton, SettingsModal, } from "@/components/settings/SettingsModal"; -import { MCPMarketplaceModal } from "@/components/mcp/MCPMarketplaceModal"; +import { MCPManualInstallModal } from "@/components/mcp/MCPManualInstallModal"; import { Store } from "lucide-react"; interface ClientHomeProps { @@ -22,7 +22,7 @@ export function ClientHome({ offline, models }: ClientHomeProps) { const [currentConversationId, setCurrentConversationId] = useState< string | null >(null); - const [showMCPMarketplace, setShowMCPMarketplace] = useState(false); + const [showManualInstall, setShowManualInstall] = useState(false); const handleConversationCreated = () => { // Incrementar trigger para forçar atualização da sidebar @@ -49,11 +49,11 @@ export function ClientHome({ offline, models }: ClientHomeProps) { )} setOpen(true)} /> @@ -77,9 +77,9 @@ export function ClientHome({ offline, models }: ClientHomeProps) { setOpen(false)} /> - setShowMCPMarketplace(false)} + setShowManualInstall(false)} /> ); diff --git a/components/mcp/MCPCategoryFilter.tsx b/components/mcp/MCPCategoryFilter.tsx deleted file mode 100644 index d5d01fc..0000000 --- a/components/mcp/MCPCategoryFilter.tsx +++ /dev/null @@ -1,136 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { ChevronDown, ChevronRight } from "lucide-react"; - -interface MCPCategory { - id: string; - name: string; - icon: string; - count: number; -} - -interface MCPCategoryFilterProps { - primaryCategories: MCPCategory[]; - otherCategories: MCPCategory[]; - selectedCategory: string; - onSelectCategory: (categoryId: string) => void; -} - -export function MCPCategoryFilter({ - primaryCategories, - otherCategories, - selectedCategory, - onSelectCategory, -}: MCPCategoryFilterProps) { - const [showOthers, setShowOthers] = useState(false); - - const getCategoryIcon = (categoryName: string) => { - const iconMap: Record = { - MAP: "🗺️", - BROWSER: "🌐", - OFFICE: "📄", - CODE: "💻", - DATABASE: "🗄️", - SEARCH: "🔍", - PAYMENT: "💳", - CHART: "📊", - FINANCE: "💰", - OTHER: "📦", - }; - return iconMap[categoryName.toUpperCase()] || "📦"; - }; - - return ( -
-

- Categories -

- - {/* All Categories */} - - - {/* Primary Categories */} -
-

- Principais -

- {primaryCategories.map((category) => ( - - ))} -
- - {/* Other Categories */} - {otherCategories.length > 0 && ( -
- - - {showOthers && ( -
- {otherCategories.map((category) => ( - - ))} -
- )} -
- )} -
- ); -} diff --git a/components/mcp/MCPManualInstallModal.tsx b/components/mcp/MCPManualInstallModal.tsx index 023aaed..6c38bb4 100644 --- a/components/mcp/MCPManualInstallModal.tsx +++ b/components/mcp/MCPManualInstallModal.tsx @@ -154,9 +154,9 @@ export function MCPManualInstallModal({ How to use

- Paste your MCP server configuration in Claude Desktop format - (claude_desktop_config.json). You can install custom MCPs or MCPs - not available in the marketplace. + Cole a configuração do servidor MCP no formato do Claude Desktop + (claude_desktop_config.json). Apenas instalação manual é suportada + neste projeto.

- - - - - {/* Tabs */} -
- - -
- - {/* Sync Status Banner */} - {syncStatus?.isSyncing && ( -
-
-
- - Sincronizando marketplace... {syncStatus.totalCached} servidores - carregados - -
-
- )} - - {syncStatus?.lastSync && !syncStatus.isSyncing && ( -
-
- - Cache local ativo • {syncStatus.totalCached} servidores • Última - atualização: {new Date(syncStatus.lastSync).toLocaleString()} - - -
-
- )} - - {/* Content */} -
- {/* Sidebar - apenas para marketplace */} - {activeTab === "marketplace" && ( -
- -
- )} - - {/* Main Content */} -
- {/* Search and Filters - apenas para marketplace */} - {activeTab === "marketplace" && ( -
-
- { - // O useEffect já cuida de recarregar quando searchTerm muda - }} - /> -
- - - -
-
-
- )} - - {/* MCP Grid */} -
- {activeTab === "marketplace" && ( - <> - {loading ? ( -
-
-
- ) : ( -
- {mcps.map((mcp) => ( - handleInstall(mcp.id)} - onUninstall={() => handleUninstall(mcp.id)} - installingStatus={installingMcps.get(mcp.id)} - /> - ))} -
- )} - - {!loading && mcps.length === 0 && ( -
- -

No MCPs found

-

- Try adjusting your search or filters -

-
- )} - - )} - - {activeTab === "installed" && ( - <> - {loadingInstalled ? ( -
-
-
- ) : ( -
- {installedMcps.map((mcp) => ( - {}} // Não usado no modo installed - onEditConfig={() => handleEditConfig(mcp)} - onTestServer={() => handleTestServer(mcp.id)} - isTesting={testingMCP === mcp.id} - onUninstall={() => handleUninstall(mcp.id)} - installingStatus={installingMcps.get(mcp.id)} - /> - ))} -
- )} - - {!loadingInstalled && installedMcps.length === 0 && ( -
- -

Nenhum MCP instalado

-

Instale MCPs na aba Marketplace

-
- )} - - )} -
-
-
- - - {/* Manual Install Modal */} - setManualInstallModalOpen(false)} - onSuccess={async (mcpId) => { - console.log(`✅ Manual installation started for ${mcpId}`); - - // Adicionar ao tracking de instalação - setInstallingMcps((prev) => { - const next = new Map(prev); - next.set(mcpId, { - status: "pending", - message: "Installation started...", - percentage: 5, - }); - return next; - }); - - // Atualizar listas - await fetchMCPs(); - if (activeTab === "installed") { - await fetchInstalledMCPs(); - } - }} - /> - - {/* Config Modal */} - {selectedConfigMCP && ( - { - setConfigModalOpen(false); - setSelectedConfigMCP(null); - setMcpConfig(null); - setIsEditMode(false); - }} - onConfirm={handleConfirmInstall} - isEditMode={isEditMode} - /> - )} - - {/* Toast de resultado do teste */} - {testResult && ( -
-
- {testResult.success ? ( - - ) : ( - - )} -
-

{testResult.message}

- {testResult.responseTime && ( -

- Tempo de resposta: {testResult.responseTime}ms -

- )} - {testResult.error && ( -

- {testResult.error} -

- )} -
-
-
- )} - - ); -} diff --git a/components/mcp/MCPRatingStars.tsx b/components/mcp/MCPRatingStars.tsx deleted file mode 100644 index 889228b..0000000 --- a/components/mcp/MCPRatingStars.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; - -import { Star } from "lucide-react"; - -interface MCPRatingStarsProps { - rating: number; - size?: "sm" | "md" | "lg"; - showNumber?: boolean; -} - -export function MCPRatingStars({ - rating, - size = "sm", - showNumber = true, -}: MCPRatingStarsProps) { - const sizeClasses = { - sm: "h-3 w-3", - md: "h-4 w-4", - lg: "h-5 w-5", - }; - - const stars = []; - const fullStars = Math.floor(rating); - const hasHalfStar = rating % 1 >= 0.5; - - // Renderizar estrelas cheias - for (let i = 0; i < fullStars; i++) { - stars.push( - - ); - } - - // Renderizar meia estrela se necessário - if (hasHalfStar) { - stars.push( -
- -
- -
-
- ); - } - - // Renderizar estrelas vazias - const emptyStars = 5 - Math.ceil(rating); - for (let i = 0; i < emptyStars; i++) { - stars.push( - - ); - } - - return ( -
-
{stars}
- {showNumber && ( - - {rating.toFixed(1)} - - )} -
- ); -} diff --git a/components/mcp/MCPSearchBar.tsx b/components/mcp/MCPSearchBar.tsx deleted file mode 100644 index 820ce35..0000000 --- a/components/mcp/MCPSearchBar.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import { Search } from "lucide-react"; - -interface MCPSearchBarProps { - value: string; - onChange: (value: string) => void; - onSearch: () => void; -} - -export function MCPSearchBar({ value, onChange, onSearch }: MCPSearchBarProps) { - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - onSearch(); - } - }; - - return ( -
- - onChange(e.target.value)} - onKeyPress={handleKeyPress} - placeholder="Search MCPs..." - className="w-full pl-10 pr-4 py-2 bg-[var(--surface)] border border-[var(--border)] rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--primary)] focus:border-transparent mcp-search-input" - /> -
- ); -} diff --git a/components/mcp/VirtualizedMCPGrid.tsx b/components/mcp/VirtualizedMCPGrid.tsx deleted file mode 100644 index 0519ecb..0000000 --- a/components/mcp/VirtualizedMCPGrid.tsx +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/database/ollahub-preferences.db-shm b/database/ollahub-preferences.db-shm index 5deb7f9..1bf4fd7 100644 Binary files a/database/ollahub-preferences.db-shm and b/database/ollahub-preferences.db-shm differ diff --git a/database/ollahub-preferences.db-wal b/database/ollahub-preferences.db-wal index 5dcf2ff..2a1112d 100644 Binary files a/database/ollahub-preferences.db-wal and b/database/ollahub-preferences.db-wal differ diff --git a/database/ollahub.db-shm b/database/ollahub.db-shm index b8aa12d..5e93fa9 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 aaf8910..7188816 100644 Binary files a/database/ollahub.db-wal and b/database/ollahub.db-wal differ diff --git a/database/repositories/mcp-cache.ts b/database/repositories/mcp-cache.ts deleted file mode 100644 index 0815add..0000000 --- a/database/repositories/mcp-cache.ts +++ /dev/null @@ -1,544 +0,0 @@ -import { getDatabase } from "@/database/database"; -import { PulseMCPService } from "@/lib/services/pulsemcp"; - -const db = getDatabase(); - -export interface MCPSearchParams { - query?: string; - category?: string; - limit?: number; - offset?: number; - sortBy?: string; - order?: "asc" | "desc"; -} - -export interface MCPCacheItem { - id: string; - owner: string; - repo: string; - content_name: string; - publisher_id: string; - description?: string; - category?: string; - subfield?: string; - field?: string; - rating: number; - review_cnt: number; - content_tag_list?: string; - thumbnail_picture?: string; - website?: string; - detail_url?: string; - ext_info?: any; - created_at: number; - updated_at: number; -} - -export class MCPCacheRepository { - private static readonly CACHE_TTL = 24 * 60 * 60 * 1000; // 24 horas em millisegundos - - /** - * Verificar se cache está válido (< 24h) - */ - static isCacheValid(): boolean { - try { - const lastSync = this.getLastSync(); - if (!lastSync) return false; - - const now = Date.now(); - return now - lastSync < this.CACHE_TTL; - } catch (error) { - console.error("Error checking cache validity:", error); - return false; - } - } - - /** - * Obter timestamp da última sincronização - */ - static getLastSync(): number | null { - try { - const stmt = db.prepare( - "SELECT value FROM mcp_cache_metadata WHERE key = 'last_sync'" - ); - const result = stmt.get() as { value: string } | undefined; - return result ? parseInt(result.value) : null; - } catch (error) { - console.error("Error getting last sync:", error); - return null; - } - } - - /** - * Salvar múltiplos MCPs em batch (performance) - */ - static saveBatch(mcps: any[]): void { - try { - const stmt = db.prepare(` - INSERT INTO mcp_marketplace_cache ( - id, owner, repo, content_name, publisher_id, description, - category, subfield, field, rating, review_cnt, content_tag_list, - thumbnail_picture, website, detail_url, ext_info, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(id) DO UPDATE SET - owner = EXCLUDED.owner, - repo = EXCLUDED.repo, - content_name = EXCLUDED.content_name, - publisher_id = EXCLUDED.publisher_id, - description = EXCLUDED.description, - category = EXCLUDED.category, - subfield = EXCLUDED.subfield, - field = EXCLUDED.field, - rating = EXCLUDED.rating, - review_cnt = EXCLUDED.review_cnt, - content_tag_list = EXCLUDED.content_tag_list, - thumbnail_picture = EXCLUDED.thumbnail_picture, - website = EXCLUDED.website, - detail_url = EXCLUDED.detail_url, - ext_info = EXCLUDED.ext_info, - updated_at = EXCLUDED.updated_at - `); - - const now = Date.now(); - - for (const pulseMCPItem of mcps) { - // Transformar dados PulseMCP para formato interno - const mcpProvider = - PulseMCPService.transformToMCPProvider(pulseMCPItem); - - // Extrair owner/repo do package_name ou usar name como fallback - const packageName = pulseMCPItem.package_name || pulseMCPItem.name; - const [owner, repo] = packageName.includes("/") - ? packageName.split("/") - : ["", packageName]; - - stmt.run( - mcpProvider.id, - owner, - repo, - mcpProvider.name, - mcpProvider.author, - mcpProvider.description, - mcpProvider.category, - mcpProvider.subfield, - mcpProvider.field, - mcpProvider.rating, - mcpProvider.totalRatings, - mcpProvider.tags.join(","), - null, // Não temos ícones disponíveis - mcpProvider.homepage || null, - pulseMCPItem.source_code_url || null, - JSON.stringify({ - package_registry: pulseMCPItem.package_registry, - package_name: pulseMCPItem.package_name, - github_stars: pulseMCPItem.github_stars, - package_download_count: pulseMCPItem.package_download_count, - config: mcpProvider.config, - tools: mcpProvider.tools, - pulseMCP_meta: pulseMCPItem._meta, - }), - now, - now - ); - } - - console.log(`Saved batch of ${mcps.length} MCPs to cache`); - } catch (error) { - console.error("Error saving MCP batch:", error); - throw error; - } - } - - /** - * Limpar cache antigo - */ - static clearCache(): void { - try { - db.exec("DELETE FROM mcp_marketplace_cache"); - db.exec("DELETE FROM mcp_cache_metadata"); - console.log("MCP cache cleared"); - } catch (error) { - console.error("Error clearing cache:", error); - throw error; - } - } - - /** - * Buscar MCPs no cache com filtros - */ - static search(params: MCPSearchParams): MCPCacheItem[] { - try { - let query: string; - const queryParams: any[] = []; - - if (params.query) { - // Usar FTS5 para busca textual - query = ` - SELECT c.* FROM mcp_marketplace_cache c - INNER JOIN mcp_search_fts f ON c.rowid = f.rowid - WHERE mcp_search_fts MATCH ? - `; - - // Escapar caracteres especiais e adicionar wildcard - const searchTerm = params.query.replace(/[^a-zA-Z0-9\s]/g, " ").trim(); - queryParams.push(`${searchTerm}*`); // Prefix search - - // Filtro de categoria (se houver) - if (params.category) { - query += " AND (c.category = ? OR c.subfield = ?)"; - queryParams.push(params.category, params.category); - } - - // Ordenar por relevância FTS + rating - query += " ORDER BY f.rank, c.rating DESC"; - } else { - // Query sem busca textual (usar índices normais) - query = "SELECT * FROM mcp_marketplace_cache WHERE 1=1"; - - if (params.category) { - query += " AND (category = ? OR subfield = ?)"; - queryParams.push(params.category, params.category); - } - - // Ordenação por índice apropriado - const order = params.order || "desc"; - let orderClause = ""; - - switch (params.sortBy) { - case "rating": - orderClause = `ORDER BY rating ${order.toUpperCase()}`; - break; - case "downloads": - orderClause = `ORDER BY review_cnt ${order.toUpperCase()}`; - break; - case "name": - orderClause = `ORDER BY content_name ${order.toUpperCase()}`; - break; - case "recent": - orderClause = `ORDER BY updated_at ${order.toUpperCase()}`; - break; - case "total_ratings": - orderClause = `ORDER BY review_cnt ${order.toUpperCase()}`; - break; - case "updated_at": - orderClause = `ORDER BY updated_at ${order.toUpperCase()}`; - break; - default: - orderClause = `ORDER BY rating DESC`; // Padrão - } - - query += ` ${orderClause}`; - } - - // Paginação - if (params.limit) { - query += " LIMIT ? OFFSET ?"; - queryParams.push(params.limit, params.offset || 0); - } - - const stmt = db.prepare(query); - const results = stmt.all(...queryParams) as MCPCacheItem[]; - - console.log(`Found ${results.length} MCPs in cache for search:`, params); - return results; - } catch (error) { - console.error("Error searching MCP cache:", error); - return []; - } - } - - /** - * Contar total de MCPs no cache - */ - static count(filters?: { category?: string }): number { - try { - let query = - "SELECT COUNT(*) as count FROM mcp_marketplace_cache WHERE 1=1"; - const queryParams: any[] = []; - - if (filters?.category) { - query += " AND (category = ? OR subfield = ?)"; - queryParams.push(filters.category, filters.category); - } - - const stmt = db.prepare(query); - const result = stmt.get(...queryParams) as { count: number }; - return result.count; - } catch (error) { - console.error("Error counting MCP cache:", error); - return 0; - } - } - - /** - * Atualizar metadados do cache - */ - static updateMetadata(key: string, value: string): void { - try { - const stmt = db.prepare(` - INSERT INTO mcp_cache_metadata (key, value, updated_at) - VALUES (?, ?, ?) - ON CONFLICT(key) DO UPDATE SET - value = EXCLUDED.value, - updated_at = EXCLUDED.updated_at - `); - stmt.run(key, value, Date.now()); - } catch (error) { - console.error("Error updating cache metadata:", error); - throw error; - } - } - - /** - * Verificar se precisa sincronizar - */ - static needsSync(): boolean { - try { - // Se não há dados no cache, precisa sincronizar - const count = this.count(); - if (count === 0) return true; - - // Se cache expirou, precisa sincronizar - return !this.isCacheValid(); - } catch (error) { - console.error("Error checking if sync is needed:", error); - return true; - } - } - - /** - * Obter estatísticas do cache - */ - static getStats(): { - totalItems: number; - lastSync: number | null; - isValid: boolean; - categories: Record; - } { - try { - const totalItems = this.count(); - const lastSync = this.getLastSync(); - const isValid = this.isCacheValid(); - - // Contar por categoria - const categoryStmt = db.prepare(` - SELECT category, COUNT(*) as count - FROM mcp_marketplace_cache - WHERE category IS NOT NULL - GROUP BY category - `); - const categoryResults = categoryStmt.all() as Array<{ - category: string; - count: number; - }>; - - const categories = categoryResults.reduce((acc, row) => { - acc[row.category] = row.count; - return acc; - }, {} as Record); - - return { - totalItems, - lastSync, - isValid, - categories, - }; - } catch (error) { - console.error("Error getting cache stats:", error); - return { - totalItems: 0, - lastSync: null, - isValid: false, - categories: {}, - }; - } - } - - /** - * Verificar se uma tabela existe - */ - static tableExists(tableName: string): boolean { - try { - const stmt = db.prepare(` - SELECT name FROM sqlite_master - WHERE type='table' AND name=? - `); - const result = stmt.get(tableName); - return !!result; - } catch (error) { - console.error("Error checking if table exists:", error); - return false; - } - } - - /** - * Extrair categorias dinamicamente do cache - * Retorna categorias principais e outras categorias menos comuns - */ - static getCategories(): { - primary: Array<{ category: string; count: number }>; - others: Array<{ category: string; count: number }>; - } { - try { - // Categorias principais definidas (baseadas nos dados reais da API) - const primaryCategories = [ - "MAP", - "BROWSER", - "OFFICE", - "CODE", - "DATABASE", - "SEARCH", - "PAYMENT", - "CHART", - "FINANCE", - "OTHER", - ]; - - // Query para obter todas as categorias com contadores - const stmt = db.prepare(` - SELECT - COALESCE(subfield, category, 'other') as category, - COUNT(*) as count - FROM mcp_marketplace_cache - WHERE COALESCE(subfield, category, 'other') IS NOT NULL - GROUP BY COALESCE(subfield, category, 'other') - ORDER BY count DESC - `); - - const allCategories = stmt.all() as Array<{ - category: string; - count: number; - }>; - - // Separar em categorias principais e outras - const primary: Array<{ category: string; count: number }> = []; - const others: Array<{ category: string; count: number }> = []; - - for (const cat of allCategories) { - const normalizedCategory = cat.category.toUpperCase(); - - if (primaryCategories.includes(normalizedCategory)) { - primary.push({ - category: normalizedCategory, - count: cat.count, - }); - } else { - others.push({ - category: cat.category, - count: cat.count, - }); - } - } - - // Ordenar categorias principais por contagem (desc) - primary.sort((a, b) => b.count - a.count); - - // Ordenar outras categorias por contagem (desc) - others.sort((a, b) => b.count - a.count); - - console.log( - `📊 Extracted categories: ${primary.length} primary, ${others.length} others` - ); - - return { primary, others }; - } catch (error) { - console.error("Error extracting categories:", error); - return { primary: [], others: [] }; - } - } - - /** - * Obter estatísticas detalhadas de categorias - */ - static getCategoryStats(): { - totalCategories: number; - primaryCategories: number; - otherCategories: number; - totalMCPs: number; - categoryBreakdown: Record; - } { - try { - const categories = this.getCategories(); - const totalMCPs = this.count(); - - const categoryBreakdown: Record = {}; - - // Adicionar categorias principais - for (const cat of categories.primary) { - categoryBreakdown[cat.category] = cat.count; - } - - // Adicionar outras categorias - for (const cat of categories.others) { - categoryBreakdown[cat.category] = cat.count; - } - - return { - totalCategories: categories.primary.length + categories.others.length, - primaryCategories: categories.primary.length, - otherCategories: categories.others.length, - totalMCPs, - categoryBreakdown, - }; - } catch (error) { - console.error("Error getting category stats:", error); - return { - totalCategories: 0, - primaryCategories: 0, - otherCategories: 0, - totalMCPs: 0, - categoryBreakdown: {}, - }; - } - } - - /** - * Buscar MCP por owner e repo - */ - static findByOwnerRepo(owner: string, repo: string): MCPCacheItem | null { - try { - const stmt = db.prepare(` - SELECT * FROM mcp_marketplace_cache - WHERE owner = ? AND repo = ? - `); - const result = stmt.get(owner, repo) as MCPCacheItem | undefined; - return result || null; - } catch (error) { - console.error("Error finding MCP by owner/repo:", error); - return null; - } - } - - /** - * Buscar MCP por nome - */ - static findByName(name: string): MCPCacheItem | null { - try { - const stmt = db.prepare(` - SELECT * FROM mcp_marketplace_cache - WHERE content_name = ? OR repo = ? - `); - const result = stmt.get(name, name) as MCPCacheItem | undefined; - return result || null; - } catch (error) { - console.error("Error finding MCP by name:", error); - return null; - } - } - - /** - * Buscar MCP por ID - */ - static findById(id: string): MCPCacheItem | null { - try { - const stmt = db.prepare(` - SELECT * FROM mcp_marketplace_cache - WHERE id = ? - `); - const result = stmt.get(id) as MCPCacheItem | undefined; - return result || null; - } catch (error) { - console.error("Error finding MCP by ID:", error); - return null; - } - } -} diff --git a/database/schema.sql b/database/schema.sql index fa4a395..2df51ed 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -44,34 +44,7 @@ CREATE TABLE IF NOT EXISTS mcp_installations ( updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); --- Cache de todos os servidores MCP do marketplace -CREATE TABLE IF NOT EXISTS mcp_marketplace_cache ( - id TEXT PRIMARY KEY, - owner TEXT NOT NULL, - repo TEXT NOT NULL, - content_name TEXT NOT NULL, - publisher_id TEXT NOT NULL, - description TEXT, - category TEXT, - subfield TEXT, - field TEXT, - rating REAL DEFAULT 0, - review_cnt INTEGER DEFAULT 0, - content_tag_list TEXT, - thumbnail_picture TEXT, - website TEXT, - detail_url TEXT, - ext_info JSON, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Metadados do cache (última sincronização, total de registros) -CREATE TABLE IF NOT EXISTS mcp_cache_metadata ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); +-- Marketplace removido: tabelas e índices relacionados ao cache do marketplace foram descontinuados -- Tabela para rastrear MCPs ativos por conversa CREATE TABLE IF NOT EXISTS active_mcps ( @@ -93,8 +66,4 @@ CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC); CREATE INDEX idx_embeddings_message ON embeddings(message_id); CREATE INDEX idx_mcp_owner_repo ON mcp_installations(owner, repo); CREATE INDEX idx_mcp_installed_at ON mcp_installations(installed_at DESC); -CREATE INDEX idx_mcp_cache_category ON mcp_marketplace_cache(category); -CREATE INDEX idx_mcp_cache_subfield ON mcp_marketplace_cache(subfield); -CREATE INDEX idx_mcp_cache_rating ON mcp_marketplace_cache(rating DESC); -CREATE INDEX idx_mcp_cache_owner_repo ON mcp_marketplace_cache(owner, repo); -CREATE INDEX idx_mcp_cache_search ON mcp_marketplace_cache(content_name, description); +-- Índices do marketplace removidos diff --git a/lib/embeddings.ts b/lib/embeddings.ts index 6303d1e..976af16 100644 --- a/lib/embeddings.ts +++ b/lib/embeddings.ts @@ -9,20 +9,49 @@ export async function generateEmbedding( model: string = "nomic-embed-text" ): Promise { try { + // Verificar se deve forçar uso de CPU (útil quando GPU tem problemas de memória) + const forceCpu = + process.env.OLLAMA_NO_GPU === "1" || process.env.OLLAMA_NUM_GPU === "0"; + const response = await ollama.embeddings({ model, prompt: text, + options: forceCpu ? { num_gpu: 0 } : undefined, }); return new Float32Array(response.embedding); - } catch (error) { + } catch (error: any) { console.error("Erro ao gerar embedding:", error); + // Se erro de GPU e ainda não tentou CPU, tentar forçar CPU + const isGpuError = + error?.message?.includes("CUDA") || + error?.message?.includes("unable to allocate") || + error?.status_code === 500; + + if ( + isGpuError && + process.env.OLLAMA_NO_GPU !== "1" && + process.env.OLLAMA_NUM_GPU !== "0" + ) { + console.warn("Erro de GPU detectado, tentando usar CPU..."); + try { + const response = await ollama.embeddings({ + model, + prompt: text, + options: { num_gpu: 0 }, + }); + return new Float32Array(response.embedding); + } catch (cpuError) { + console.error("Erro mesmo usando CPU:", cpuError); + } + } + // Fallback: retornar vetor zero se modelo não estiver disponível console.warn( `Modelo ${model} não disponível, usando vetor zero como fallback` ); - return new Float32Array(384); // 384 dimensões padrão + return new Float32Array(768); // 768 dimensões para nomic-embed-text } } diff --git a/lib/prompts/mcp-tools-prompt.ts b/lib/prompts/mcp-tools-prompt.ts index 94fee8a..696a76f 100644 --- a/lib/prompts/mcp-tools-prompt.ts +++ b/lib/prompts/mcp-tools-prompt.ts @@ -150,6 +150,7 @@ export function extractToolCallFromResponse(response: string): { const parsed = JSON.parse(jsonText.trim()); + // Formato atual suportado pelo prompt engineering if (parsed.tool_call && parsed.tool_call.name) { return { name: parsed.tool_call.name, @@ -157,14 +158,60 @@ export function extractToolCallFromResponse(response: string): { }; } + // Formato OpenAI-like: { type: 'function', function: { name, parameters|arguments } } + // Algumas implementações usam `arguments` como string JSON + if (parsed.type === "function" && parsed.function && parsed.function.name) { + const fn = parsed.function; + let params: any = fn.parameters ?? fn.arguments ?? {}; + if (typeof params === "string") { + try { + params = JSON.parse(params); + } catch { + // se não for JSON válido, mantém string (executor fará fallback) + } + } + return { name: fn.name, parameters: params || {} }; + } + + // Formato simples: { name: "tool", parameters: {...} } + if (parsed.name && (parsed.parameters || parsed.arguments)) { + let params: any = parsed.parameters ?? parsed.arguments ?? {}; + if (typeof params === "string") { + try { + params = JSON.parse(params); + } catch { + // mantém string se não for JSON válido + } + } + return { name: parsed.name, parameters: params || {} }; + } + return null; } catch (error) { // Tentar extrair manualmente const nameMatch = response.match(/"name":\s*"([^"]+)"/); if (nameMatch) { const name = nameMatch[1]; - const paramsMatch = response.match(/"parameters":\s*({[^}]+})/); - const parameters = paramsMatch ? JSON.parse(paramsMatch[1]) : {}; + // Captura tanto parameters quanto arguments + const paramsMatch = + response.match(/"parameters"\s*:\s*({[\s\S]*?})/) || + response.match(/"arguments"\s*:\s*("[\s\S]*?"|{[\s\S]*?})/); + let parameters: any = {}; + if (paramsMatch) { + const raw = paramsMatch[1]; + try { + parameters = JSON.parse(raw); + } catch { + // caso venha como string com JSON dentro de aspas + if (raw.startsWith('"') && raw.endsWith('"')) { + try { + parameters = JSON.parse(raw.slice(1, -1)); + } catch { + parameters = {}; + } + } + } + } return { name, parameters }; } diff --git a/lib/services/mcp-executor.ts b/lib/services/mcp-executor.ts index 463272b..81777dd 100644 --- a/lib/services/mcp-executor.ts +++ b/lib/services/mcp-executor.ts @@ -31,16 +31,31 @@ export class MCPExecutor { } // Verificar se está pronto - if (mcpData.status !== "ready") { + // Se status é undefined/null, considerar como "ready" (MCPs instalados antes da implementação de status) + if (mcpData.status && mcpData.status !== "ready") { throw new Error( `MCP ${mcpId} is not ready (status: ${mcpData.status})` ); } // Obter comando executável - const config = JSON.parse(mcpData.config); - const command = config.command || mcpData.executable_command; - const args = config.args || []; + // mcpData.config já vem parseado do repositório, mas pode ser string em casos legados + const config = + typeof mcpData.config === "string" + ? JSON.parse(mcpData.config) + : mcpData.config || {}; + const commandFromConfig = config.command; + const argsFromConfig = Array.isArray(config.args) ? config.args : []; + + let command = commandFromConfig; + let args = argsFromConfig; + + // Se não houver command em config, reconstruir a partir de executable_command (se existir) + if (!command && mcpData.executable_command) { + const parts = String(mcpData.executable_command).trim().split(/\s+/); + command = parts[0]; + args = parts.slice(1); + } if (!command) { throw new Error(`No executable command found for MCP ${mcpId}`); @@ -100,14 +115,14 @@ export class MCPExecutor { ): Promise { return new Promise((resolve, reject) => { const env = { - ...process.env, + ...globalThis.process.env, ...(environmentPath && { - PATH: `${environmentPath}/node_modules/.bin:${process.env.PATH}`, + PATH: `${environmentPath}/node_modules/.bin:${globalThis.process.env.PATH}`, }), }; - const process = spawn(command, args, { - cwd: environmentPath || process.cwd(), + const childProcess = spawn(command, args, { + cwd: environmentPath || globalThis.process.cwd(), env, stdio: ["pipe", "pipe", "pipe"], }); @@ -119,26 +134,35 @@ export class MCPExecutor { // Timeout const timeout = setTimeout(() => { if (!responseReceived) { - process.kill(); + childProcess.kill(); reject(new Error(`Execution timeout after ${EXECUTION_TIMEOUT}ms`)); } }, EXECUTION_TIMEOUT); // Capturar stdout - process.stdout?.on("data", (data) => { + childProcess.stdout?.on("data", (data) => { stdout += data.toString(); + // Loga por linhas para diagnóstico + const printedLines = data + .toString() + .split("\n") + .filter((l: string) => l.trim().length > 0); + for (const l of printedLines) { + console.log(`📥 [MCP stdout] ${l}`); + } // Tentar parsear respostas JSON-RPC - const lines = stdout.split("\n"); - for (const line of lines) { + const parsedLines = stdout.split("\n"); + for (const line of parsedLines) { if (line.trim()) { try { const parsed = JSON.parse(line); + console.log("🔎 [MCP stdout parsed]", parsed); if (parsed.id === 2 && parsed.result) { // Resposta do tools/call clearTimeout(timeout); responseReceived = true; - process.kill(); + childProcess.kill(); resolve(parsed.result); } } catch (e) { @@ -149,18 +173,25 @@ export class MCPExecutor { }); // Capturar stderr - process.stderr?.on("data", (data) => { + childProcess.stderr?.on("data", (data) => { stderr += data.toString(); + const errLines = data + .toString() + .split("\n") + .filter((l: string) => l.trim().length > 0); + for (const l of errLines) { + console.warn(`⚠️ [MCP stderr] ${l}`); + } }); // Erro de processo - process.on("error", (error) => { + childProcess.on("error", (error) => { clearTimeout(timeout); reject(new Error(`Process error: ${error.message}`)); }); // Processo encerrou - process.on("close", (code) => { + childProcess.on("close", (code) => { clearTimeout(timeout); if (!responseReceived) { if (code !== 0) { @@ -186,7 +217,8 @@ export class MCPExecutor { }, id: 1, }; - process.stdin?.write(JSON.stringify(initRequest) + "\n"); + console.log("📤 [MCP send] initialize", initRequest); + childProcess.stdin?.write(JSON.stringify(initRequest) + "\n"); // Aguardar um pouco antes de enviar tool call setTimeout(() => { @@ -200,12 +232,13 @@ export class MCPExecutor { }, id: 2, }; - process.stdin?.write(JSON.stringify(toolCallRequest) + "\n"); - process.stdin?.end(); + console.log("📤 [MCP send] tools/call", toolCallRequest); + childProcess.stdin?.write(JSON.stringify(toolCallRequest) + "\n"); + childProcess.stdin?.end(); }, 500); } catch (error: any) { clearTimeout(timeout); - process.kill(); + childProcess.kill(); reject(new Error(`Failed to send JSON-RPC request: ${error.message}`)); } }); @@ -274,6 +307,16 @@ export class MCPExecutor { if (expectedType) { const actualType = typeof value; + // Tratar objeto vazio como "ausente" quando tipo esperado não é object + if ( + value && + actualType === "object" && + !Array.isArray(value) && + Object.keys(value).length === 0 && + expectedType !== "object" + ) { + continue; + } const valid = (expectedType === "string" && actualType === "string") || (expectedType === "number" && actualType === "number") || diff --git a/lib/services/mcp-sync.ts b/lib/services/mcp-sync.ts deleted file mode 100644 index cfb08a2..0000000 --- a/lib/services/mcp-sync.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { PulseMCPService } from "./pulsemcp"; -import { MCPCacheRepository } from "@/database/repositories/mcp-cache"; - -export interface SyncProgress { - current: number; - total: number; - page: number; - totalPages: number; -} - -export interface SyncResult { - success: boolean; - totalDownloaded: number; - errors: string[]; - duration: number; -} - -export class MCPSyncService { - private static isSyncingFlag = false; - private static syncPromise: Promise | null = null; - private static abortController: AbortController | null = null; - private static currentProgress: SyncProgress | null = null; - - /** - * Download completo com paginação incremental - */ - static async syncAll( - onProgress?: (progress: SyncProgress) => void - ): Promise { - // Se já existe um sync em andamento, retornar a promise existente - if (this.syncPromise) { - console.log("⏳ Sync already in progress, waiting for completion..."); - return this.syncPromise; - } - - const startTime = Date.now(); - const errors: string[] = []; - let totalDownloaded = 0; - - // Criar a promise de sync e armazená-la - this.syncPromise = (async () => { - try { - if (this.isSyncingFlag) { - throw new Error("Sync already in progress"); - } - - this.isSyncingFlag = true; - this.abortController = new AbortController(); - - console.log("🔄 Starting MCP marketplace sync..."); - - // Limpar cache antigo antes de começar - MCPCacheRepository.clearCache(); - - const allMCPs = await this.downloadAllPages(onProgress); - totalDownloaded = allMCPs.length; - - // Salvar todos os MCPs no banco - if (allMCPs.length > 0) { - MCPCacheRepository.saveBatch(allMCPs); - - // Atualizar metadados - MCPCacheRepository.updateMetadata("last_sync", Date.now().toString()); - MCPCacheRepository.updateMetadata( - "total_items", - allMCPs.length.toString() - ); - - console.log(`✅ Sync completed: ${totalDownloaded} MCPs downloaded`); - } - - const duration = Date.now() - startTime; - return { - success: true, - totalDownloaded, - errors, - duration, - }; - } catch (error: any) { - console.error("❌ Sync failed:", error); - errors.push(error.message); - - const duration = Date.now() - startTime; - return { - success: false, - totalDownloaded, - errors, - duration, - }; - } finally { - this.isSyncingFlag = false; - this.abortController = null; - this.currentProgress = null; - this.syncPromise = null; // Limpar a promise compartilhada - } - })(); - - return this.syncPromise; - } - - /** - * Sincronização em background (usado automaticamente) - */ - static async backgroundSync(): Promise { - try { - if (this.isSyncingFlag) { - console.log("Sync already in progress, skipping background sync"); - return; - } - - console.log("🔄 Starting background MCP sync..."); - await this.syncAll((progress) => { - console.log( - `📥 Downloaded ${progress.current}/${progress.total} MCPs (page ${progress.page}/${progress.totalPages})` - ); - }); - } catch (error) { - console.error("Background sync failed:", error); - } - } - - /** - * Verificar se sincronização está em andamento - */ - static isSyncing(): boolean { - return this.isSyncingFlag; - } - - /** - * Cancelar sincronização atual - */ - static cancelSync(): void { - if (this.abortController) { - this.abortController.abort(); - console.log("🛑 Sync cancelled by user"); - } - } - - /** - * Obter progresso atual da sincronização - */ - static getCurrentProgress(): SyncProgress | null { - return this.currentProgress; - } - - /** - * Download completo de todos os servidores PulseMCP (uma única chamada) - */ - private static async downloadAllPages( - onProgress?: (progress: SyncProgress) => void - ): Promise { - try { - console.log("📡 Starting complete download from PulseMCP..."); - - // Notificar início - this.currentProgress = { - current: 0, - total: 0, - page: 1, - totalPages: 1, - }; - - if (onProgress) { - onProgress(this.currentProgress); - } - - // Fazer uma única chamada para buscar todos os servidores - const response = await PulseMCPService.getAllServers(); - - console.log(`📊 Total servers available: ${response.total_count}`); - console.log(`📥 Downloaded ${response.servers.length} servers`); - - // Atualizar progresso - this.currentProgress = { - current: response.servers.length, - total: response.total_count, - page: 1, - totalPages: 1, - }; - - if (onProgress) { - onProgress(this.currentProgress); - } - - // Salvar em batches de 500 para evitar bloqueio - const batchSize = 500; - const totalBatches = Math.ceil(response.servers.length / batchSize); - - for (let i = 0; i < totalBatches; i++) { - const start = i * batchSize; - const end = Math.min(start + batchSize, response.servers.length); - const batch = response.servers.slice(start, end); - - console.log( - `💾 Saving batch ${i + 1}/${totalBatches} (${batch.length} items)...` - ); - MCPCacheRepository.saveBatch(batch); - - // Pequena pausa entre batches - await new Promise((resolve) => setTimeout(resolve, 100)); - } - - console.log( - `✅ Download completed: ${response.servers.length} total MCPs downloaded from PulseMCP` - ); - return response.servers; - } catch (error: any) { - console.error(`❌ Error downloading from PulseMCP:`, error); - - // Se for erro de abort, parar - if (this.abortController?.signal.aborted) { - console.log("🛑 Download cancelled"); - return []; - } - - // Para outros erros, lançar - throw error; - } - } - - /** - * Verificar se precisa sincronizar - */ - static needsSync(): boolean { - return MCPCacheRepository.needsSync(); - } - - /** - * Obter estatísticas do cache - */ - static getCacheStats() { - return MCPCacheRepository.getStats(); - } - - /** - * Forçar limpeza do cache - */ - static clearCache(): void { - MCPCacheRepository.clearCache(); - console.log("🗑️ Cache cleared"); - } - - /** - * Verificar saúde do sistema de cache - */ - static async healthCheck(): Promise<{ - cacheExists: boolean; - cacheValid: boolean; - totalItems: number; - lastSync: number | null; - needsSync: boolean; - }> { - try { - const cacheExists = MCPCacheRepository.tableExists( - "mcp_marketplace_cache" - ); - const stats = MCPCacheRepository.getStats(); - const needsSync = MCPCacheRepository.needsSync(); - - return { - cacheExists, - cacheValid: stats.isValid, - totalItems: stats.totalItems, - lastSync: stats.lastSync, - needsSync, - }; - } catch (error) { - console.error("Health check failed:", error); - return { - cacheExists: false, - cacheValid: false, - totalItems: 0, - lastSync: null, - needsSync: true, - }; - } - } -} diff --git a/lib/services/pulsemcp.ts b/lib/services/pulsemcp.ts deleted file mode 100644 index c4c946e..0000000 --- a/lib/services/pulsemcp.ts +++ /dev/null @@ -1,462 +0,0 @@ -import { - PulseMCPSearchParams, - PulseMCPResponse, - MCPServerMetadata, - MCPProvider, -} from "@/lib/types/mcp"; -import { MCPCache } from "@/lib/cache/mcp-cache"; - -const PULSEMCP_API_BASE = "https://api.pulsemcp.com/v0beta"; - -export class PulseMCPService { - // Mapear categorias do PulseMCP para categorias internas - private static categoryMap: Record = { - filesystem: "other", - github: "code", - google: "search", - brave: "search", - postgres: "database", - sqlite: "database", - mongodb: "database", - redis: "database", - aws: "other", - gcp: "other", - azure: "other", - slack: "other", - discord: "other", - notion: "office", - confluence: "office", - jira: "office", - figma: "other", - linear: "office", - trello: "office", - calendar: "office", - email: "office", - maps: "map", - weather: "other", - news: "other", - finance: "finance", - crypto: "finance", - stocks: "finance", - payment: "payment", - stripe: "payment", - paypal: "payment", - chart: "chart", - analytics: "chart", - monitoring: "other", - logging: "other", - security: "other", - auth: "other", - api: "other", - webhook: "other", - scraping: "other", - automation: "other", - ai: "other", - ml: "other", - data: "other", - etl: "other", - visualization: "chart", - reporting: "chart", - }; - - /** - * Buscar todos os servidores MCP na API PulseMCP (sem parâmetros) - * Com retry automático e backoff exponencial para rate limiting - */ - static async getAllServers(retryCount = 0): Promise { - const maxRetries = 3; - const baseDelay = 2000; // 2 segundos base - - try { - if (retryCount > 0) { - console.log( - `📡 Retry ${retryCount}/${maxRetries} - Fetching from PulseMCP API...` - ); - } else { - console.log("📡 Fetching all servers from PulseMCP API..."); - } - - const response = await fetch(`${PULSEMCP_API_BASE}/servers`, { - method: "GET", - headers: { - Accept: "application/json", - "User-Agent": "Ollahub-MCP-Client/1.0 (https://ollahub.com)", - }, - // Timeout de 30 segundos para download completo - signal: AbortSignal.timeout(30000), - }); - - if (!response.ok) { - // Se for 429 (Too Many Requests), tentar novamente com backoff - if (response.status === 429 && retryCount < maxRetries) { - const delay = baseDelay * Math.pow(2, retryCount); // Backoff exponencial - console.warn( - `⚠️ Rate limit hit (429). Waiting ${delay}ms before retry ${ - retryCount + 1 - }/${maxRetries}...` - ); - - await new Promise((resolve) => setTimeout(resolve, delay)); - return this.getAllServers(retryCount + 1); - } - - throw new Error( - `PulseMCP API error: ${response.status} ${response.statusText}` - ); - } - - const data = await response.json(); - console.log( - `✅ PulseMCP API returned ${data.servers.length} servers (total: ${data.total_count})` - ); - - return data; - } catch (error) { - // Se for erro de rate limit e ainda temos retries, tentar novamente - if ( - error instanceof Error && - error.message.includes("429") && - retryCount < maxRetries - ) { - const delay = baseDelay * Math.pow(2, retryCount); - console.warn( - `⚠️ Rate limit error. Waiting ${delay}ms before retry ${ - retryCount + 1 - }/${maxRetries}...` - ); - - await new Promise((resolve) => setTimeout(resolve, delay)); - return this.getAllServers(retryCount + 1); - } - - console.error("❌ Error calling PulseMCP API:", error); - throw new Error( - `Failed to fetch MCPs after ${retryCount + 1} attempts: ${ - error instanceof Error ? error.message : "Unknown error" - }` - ); - } - } - - /** - * Transformar resposta PulseMCP para formato interno MCPProvider - */ - static transformToMCPProvider(pulseMCPItem: MCPServerMetadata): MCPProvider { - const meta = pulseMCPItem._meta?.["com.pulsemcp"]; - const category = this.mapCategory( - pulseMCPItem.name, - pulseMCPItem.short_description - ); - - // Usar name como ID se package_name não estiver disponível - const id = - pulseMCPItem.package_name || - pulseMCPItem.name.toLowerCase().replace(/[^a-z0-9]/g, "-"); - - return { - id: id, - name: meta?.standardized_name || pulseMCPItem.name, - author: meta?.standardized_provider_name || "Unknown", - description: - meta?.standardized_description || pulseMCPItem.short_description, - version: "1.0.0", // PulseMCP não fornece versão específica - category, - tags: this.extractTags(pulseMCPItem.name, pulseMCPItem.short_description), - rating: this.calculateRating(pulseMCPItem), - totalRatings: pulseMCPItem.github_stars || 0, - repository: pulseMCPItem.source_code_url, - homepage: pulseMCPItem.external_url || pulseMCPItem.source_code_url, - installed: false, // Será verificado localmente - subfield: category.toUpperCase(), - field: "MCP SERVER", - config: this.generateConfigTemplate(pulseMCPItem), - tools: [], // PulseMCP não fornece schemas de ferramentas - }; - } - - /** - * Mapear categoria baseada no nome e descrição - */ - private static mapCategory( - name: string, - description: string - ): MCPProvider["category"] { - const text = `${name} ${description}`.toLowerCase(); - - // Buscar por palavras-chave específicas - for (const [keyword, category] of Object.entries(this.categoryMap)) { - if (text.includes(keyword)) { - return category; - } - } - - return "other"; - } - - /** - * Extrair tags baseadas no nome e descrição - */ - private static extractTags(name: string, description: string): string[] { - const text = `${name} ${description}`.toLowerCase(); - const tags: string[] = []; - - // Tags comuns - const commonTags = [ - "api", - "database", - "filesystem", - "github", - "google", - "aws", - "gcp", - "azure", - "slack", - "discord", - "notion", - "calendar", - "email", - "maps", - "weather", - "finance", - "crypto", - "payment", - "chart", - "analytics", - "monitoring", - "security", - "auth", - "webhook", - "scraping", - "automation", - "ai", - "ml", - ]; - - for (const tag of commonTags) { - if (text.includes(tag)) { - tags.push(tag); - } - } - - return tags.slice(0, 5); // Limitar a 5 tags - } - - /** - * Calcular rating baseado em métricas disponíveis - */ - private static calculateRating(item: MCPServerMetadata): number { - const meta = item._meta?.["com.pulsemcp"]; - let rating = 3.0; // Rating base - - // Ajustar baseado em downloads - if (meta?.estimated_downloads_all_time) { - const downloads = meta.estimated_downloads_all_time; - if (downloads > 1000000) rating += 1.0; - else if (downloads > 100000) rating += 0.5; - else if (downloads > 10000) rating += 0.2; - } - - // Ajustar baseado em GitHub stars - if (item.github_stars) { - if (item.github_stars > 1000) rating += 0.5; - else if (item.github_stars > 100) rating += 0.2; - } - - // Ajustar se é oficial - if (meta?.is_official) { - rating += 0.5; - } - - return Math.min(5.0, Math.max(1.0, rating)); - } - - /** - * Extrair capacidades baseadas no nome e descrição - */ - private static extractCapabilities(item: MCPServerMetadata): string[] { - const text = `${item.name} ${item.short_description}`.toLowerCase(); - const capabilities: string[] = []; - - // Capacidades comuns - const capabilityMap = { - read: ["read", "get", "fetch", "retrieve"], - write: ["write", "create", "update", "modify", "edit"], - delete: ["delete", "remove", "destroy"], - search: ["search", "find", "query", "filter"], - sync: ["sync", "synchronize", "backup"], - monitor: ["monitor", "watch", "track", "observe"], - authenticate: ["auth", "login", "authenticate"], - notify: ["notify", "alert", "message", "send"], - }; - - for (const [capability, keywords] of Object.entries(capabilityMap)) { - if (keywords.some((keyword) => text.includes(keyword))) { - capabilities.push(capability); - } - } - - return capabilities.slice(0, 5); // Limitar a 5 capacidades - } - - /** - * Gerar template de configuração para o servidor - */ - private static generateConfigTemplate(item: MCPServerMetadata): any[] { - const config: any[] = []; - - // Configuração básica - config.push({ - type: "server", - name: (item.package_name || item.name).replace(/^@[\w-]+\//, ""), - command: this.getCommand(item.package_registry), - args: this.getArgs(item), - }); - - // Adicionar variáveis de ambiente se necessário - const envVars = this.getEnvTemplate(item); - if (Object.keys(envVars).length > 0) { - config.push({ - type: "env", - variables: envVars, - }); - } - - return config; - } - - /** - * Obter comando baseado no registry - */ - private static getCommand(registry: string | null): string { - switch (registry) { - case "npm": - return "npx"; - case "pypi": - return "python"; - case "cargo": - return "cargo"; - default: - return "node"; - } - } - - /** - * Obter argumentos baseados no registry - */ - private static getArgs(item: MCPServerMetadata): string[] { - switch (item.package_registry) { - case "npm": - return ["-y", item.package_name || item.name]; - case "pypi": - return ["-m", item.package_name || item.name]; - case "cargo": - return ["run", "--bin", item.package_name || item.name]; - default: - return [item.package_name || item.name]; - } - } - - /** - * Obter template de variáveis de ambiente - */ - private static getEnvTemplate( - item: MCPServerMetadata - ): Record { - const template: Record = {}; - const text = `${item.name} ${item.short_description}`.toLowerCase(); - - // Templates comuns baseados em palavras-chave - if (text.includes("google")) { - template.GOOGLE_API_KEY = "your-google-api-key"; - } - if (text.includes("github")) { - template.GITHUB_TOKEN = "your-github-token"; - } - if (text.includes("openai")) { - template.OPENAI_API_KEY = "your-openai-api-key"; - } - if (text.includes("aws")) { - template.AWS_ACCESS_KEY_ID = "your-aws-access-key"; - template.AWS_SECRET_ACCESS_KEY = "your-aws-secret-key"; - } - if (text.includes("slack")) { - template.SLACK_BOT_TOKEN = "your-slack-bot-token"; - } - if (text.includes("discord")) { - template.DISCORD_TOKEN = "your-discord-token"; - } - - return template; - } - - /** - * Verificar se a API está disponível - */ - static async checkHealth(): Promise { - try { - const response = await fetch( - `${PULSEMCP_API_BASE}/servers?count_per_page=1`, - { - method: "GET", - headers: { - Accept: "application/json", - "User-Agent": "Ollahub-MCP-Client/1.0 (https://ollahub.com)", - }, - } - ); - return response.ok; - } catch (error) { - console.error("PulseMCP API health check failed:", error); - return false; - } - } - - /** - * Obter servidores populares - */ - static async getPopularServers( - limit: number = 10 - ): Promise { - try { - const response = await this.getAllServers(); - - return response.servers - .sort((a: MCPServerMetadata, b: MCPServerMetadata) => { - const aDownloads = - a._meta?.["com.pulsemcp"]?.estimated_downloads_all_time || 0; - const bDownloads = - b._meta?.["com.pulsemcp"]?.estimated_downloads_all_time || 0; - return bDownloads - aDownloads; - }) - .slice(0, limit); - } catch (error) { - console.error("Error getting popular servers:", error); - return []; - } - } - - /** - * Buscar servidores por integração específica - */ - static async getServersByIntegration( - integration: string - ): Promise { - try { - const response = await this.getAllServers(); - // Filtrar servidores que contenham a integração no nome ou descrição - return response.servers.filter( - (server) => - server.name.toLowerCase().includes(integration.toLowerCase()) || - server.short_description - .toLowerCase() - .includes(integration.toLowerCase()) - ); - } catch (error) { - console.error( - `Error getting servers for integration ${integration}:`, - error - ); - return []; - } - } -} diff --git a/lib/types/mcp.ts b/lib/types/mcp.ts index 414366a..dc708ba 100644 --- a/lib/types/mcp.ts +++ b/lib/types/mcp.ts @@ -54,51 +54,7 @@ export interface MCPTool { }; } -export interface DeepNLPSearchParams { - query?: string; - category?: string; - id?: string; - page_id?: number; - count_per_page?: number; - offset?: number; - mode?: "list" | "dict"; -} - -export interface DeepNLPResponse { - query?: string; - items: any[]; - count: number; - total_hits: number; -} - -export interface MCPServerConfig { - total_hits: number; - id: string; - items: any[]; -} - -export interface MCPSearchParams { - category?: string; - search?: string; - sort?: "rating" | "name" | "recent" | "total_ratings" | "updated_at"; - order?: "asc" | "desc"; - limit?: number; - offset?: number; -} - -// PulseMCP API Types -export interface PulseMCPSearchParams { - query?: string; - integrations?: string[]; - count_per_page?: number; - offset?: number; -} - -export interface PulseMCPResponse { - servers: MCPServerMetadata[]; - next?: string; - total_count: number; -} +// Tipos relacionados a busca/marketplace foram removidos export interface MCPTestResult { success: boolean; @@ -113,28 +69,4 @@ export interface MCPTestRequest { mcpId: string; } -export interface MCPServerMetadata { - name: string; - url: string; - external_url?: string; - short_description: string; - source_code_url: string; - github_stars?: number; - package_registry: "npm" | "pypi" | "cargo"; - package_name: string; - package_download_count?: number; - EXPERIMENTAL_ai_generated_description?: string; - _meta: { - "com.pulsemcp": { - estimated_downloads_all_time: number; - estimated_downloads_last_30_days: number; - estimated_downloads_last_7_days: number; - standardized_name: string; - standardized_description: string; - standardized_provider_name: string; - standardized_provider_url: string; - estimated_released_on: string; - is_official: boolean; - }; - }; -} +// Metadados específicos de PulseMCP removidos diff --git a/scripts/add-fts-to-mcp-cache.ts b/scripts/add-fts-to-mcp-cache.ts deleted file mode 100644 index 2231acb..0000000 --- a/scripts/add-fts-to-mcp-cache.ts +++ /dev/null @@ -1,66 +0,0 @@ -import Database from "better-sqlite3"; -import path from "path"; - -const dbPath = path.join(process.cwd(), "database", "ollahub.db"); -const db = new Database(dbPath); - -console.log("🔧 Adding FTS5 support to MCP cache..."); - -// Criar tabela FTS5 -db.exec(` - CREATE VIRTUAL TABLE IF NOT EXISTS mcp_search_fts USING fts5( - id UNINDEXED, - content_name, - description, - content_tag_list, - publisher_id, - content=mcp_marketplace_cache, - content_rowid=rowid - ); -`); - -// Popular com dados existentes -console.log("📦 Populating FTS index..."); -db.exec(` - INSERT INTO mcp_search_fts(rowid, id, content_name, description, content_tag_list, publisher_id) - SELECT rowid, id, content_name, description, content_tag_list, publisher_id - FROM mcp_marketplace_cache; -`); - -// Criar triggers -console.log("⚡ Creating triggers..."); -db.exec(` - CREATE TRIGGER IF NOT EXISTS mcp_fts_insert AFTER INSERT ON mcp_marketplace_cache BEGIN - INSERT INTO mcp_search_fts(rowid, id, content_name, description, content_tag_list, publisher_id) - VALUES (new.rowid, new.id, new.content_name, new.description, new.content_tag_list, new.publisher_id); - END; - - CREATE TRIGGER IF NOT EXISTS mcp_fts_update AFTER UPDATE ON mcp_marketplace_cache BEGIN - UPDATE mcp_search_fts - SET content_name = new.content_name, - description = new.description, - content_tag_list = new.content_tag_list, - publisher_id = new.publisher_id - WHERE rowid = old.rowid; - END; - - CREATE TRIGGER IF NOT EXISTS mcp_fts_delete AFTER DELETE ON mcp_marketplace_cache BEGIN - DELETE FROM mcp_search_fts WHERE rowid = old.rowid; - END; -`); - -// Criar índices compostos -console.log("🔍 Creating composite indexes..."); -db.exec(` - CREATE INDEX IF NOT EXISTS idx_mcp_cache_category_rating - ON mcp_marketplace_cache(category, rating DESC); - - CREATE INDEX IF NOT EXISTS idx_mcp_cache_subfield_rating - ON mcp_marketplace_cache(subfield, rating DESC); - - CREATE INDEX IF NOT EXISTS idx_mcp_cache_updated_rating - ON mcp_marketplace_cache(updated_at DESC, rating DESC); -`); - -console.log("✅ FTS5 migration completed!"); -db.close(); diff --git a/scripts/benchmark-performance.ts b/scripts/benchmark-performance.ts deleted file mode 100644 index dc814e9..0000000 --- a/scripts/benchmark-performance.ts +++ /dev/null @@ -1,187 +0,0 @@ -import Database from "better-sqlite3"; -import path from "path"; - -const dbPath = path.join(process.cwd(), "database", "ollahub.db"); -const db = new Database(dbPath); - -console.log("🚀 Benchmark de Performance - MCP Marketplace"); -console.log("=".repeat(50)); - -// Função para medir tempo de execução -function measureTime(name: string, fn: () => T): T { - const start = performance.now(); - const result = fn(); - const end = performance.now(); - console.log(`⏱️ ${name}: ${(end - start).toFixed(2)}ms`); - return result; -} - -// Benchmark 1: Busca textual com FTS5 -console.log("\n📊 BENCHMARK 1: Busca Textual"); -console.log("-".repeat(30)); - -const searchTerms = ["context", "search", "browser", "map", "ai"]; - -searchTerms.forEach((term) => { - // Busca com FTS5 (nova implementação) - const ftsResults = measureTime(`FTS5 "${term}"`, () => { - const stmt = db.prepare(` - SELECT c.* FROM mcp_marketplace_cache c - INNER JOIN mcp_search_fts f ON c.rowid = f.rowid - WHERE mcp_search_fts MATCH ? - ORDER BY f.rank, c.rating DESC - LIMIT 50 - `); - return stmt.all(`${term}*`); - }); - - // Busca com LIKE (implementação antiga) - const likeResults = measureTime(`LIKE "${term}"`, () => { - const stmt = db.prepare(` - SELECT * FROM mcp_marketplace_cache - WHERE content_name LIKE ? OR description LIKE ? OR content_tag_list LIKE ? - ORDER BY rating DESC - LIMIT 50 - `); - const searchPattern = `%${term}%`; - return stmt.all(searchPattern, searchPattern, searchPattern); - }); - - console.log(` 📈 Resultados FTS5: ${ftsResults.length}`); - console.log(` 📈 Resultados LIKE: ${likeResults.length}`); -}); - -// Benchmark 2: Queries com índices compostos -console.log("\n📊 BENCHMARK 2: Queries com Índices Compostos"); -console.log("-".repeat(30)); - -const categories = ["search", "browser", "map", "ai"]; - -categories.forEach((category) => { - // Query com índice composto (nova implementação) - const indexedResults = measureTime(`Índice composto "${category}"`, () => { - const stmt = db.prepare(` - SELECT * FROM mcp_marketplace_cache - WHERE category = ? - ORDER BY rating DESC - LIMIT 100 - `); - return stmt.all(category); - }); - - // Query sem índice composto (simulação) - const nonIndexedResults = measureTime(`Sem índice "${category}"`, () => { - const stmt = db.prepare(` - SELECT * FROM mcp_marketplace_cache - WHERE category = ? - ORDER BY rating DESC - LIMIT 100 - `); - return stmt.all(category); - }); - - console.log(` 📈 Resultados: ${indexedResults.length}`); -}); - -// Benchmark 3: Contagem total -console.log("\n📊 BENCHMARK 3: Operações de Contagem"); -console.log("-".repeat(30)); - -const totalCount = measureTime("Contagem total", () => { - const stmt = db.prepare( - "SELECT COUNT(*) as count FROM mcp_marketplace_cache" - ); - return stmt.get() as { count: number }; -}); - -const categoryCount = measureTime("Contagem por categoria", () => { - const stmt = db.prepare(` - SELECT category, COUNT(*) as count - FROM mcp_marketplace_cache - GROUP BY category - ORDER BY count DESC - `); - return stmt.all(); -}); - -console.log(` 📈 Total de MCPs: ${totalCount.count}`); -console.log(` 📈 Categorias: ${categoryCount.length}`); - -// Benchmark 4: Verificar índices criados -console.log("\n📊 BENCHMARK 4: Verificação de Índices"); -console.log("-".repeat(30)); - -const indexes = measureTime("Listar índices", () => { - const stmt = db.prepare(` - SELECT name, sql FROM sqlite_master - WHERE type = 'index' AND name LIKE '%mcp%' - ORDER BY name - `); - return stmt.all(); -}); - -console.log(` 📈 Índices encontrados: ${indexes.length}`); -indexes.forEach((idx: any) => { - console.log(` - ${idx.name}`); -}); - -// Benchmark 5: Verificar tabela FTS5 -console.log("\n📊 BENCHMARK 5: Verificação FTS5"); -console.log("-".repeat(30)); - -const ftsTables = measureTime("Verificar tabelas FTS5", () => { - const stmt = db.prepare(` - SELECT name FROM sqlite_master - WHERE type = 'table' AND name LIKE '%fts%' - `); - return stmt.all(); -}); - -console.log(` 📈 Tabelas FTS5: ${ftsTables.length}`); -ftsTables.forEach((table: any) => { - console.log(` - ${table.name}`); -}); - -// Benchmark 6: Teste de stress -console.log("\n📊 BENCHMARK 6: Teste de Stress"); -console.log("-".repeat(30)); - -const stressTest = measureTime("100 buscas simultâneas", () => { - const results = []; - for (let i = 0; i < 100; i++) { - const term = `test${i % 10}`; - const stmt = db.prepare(` - SELECT c.* FROM mcp_marketplace_cache c - INNER JOIN mcp_search_fts f ON c.rowid = f.rowid - WHERE mcp_search_fts MATCH ? - LIMIT 10 - `); - results.push(stmt.all(`${term}*`)); - } - return results; -}); - -console.log(` 📈 Buscas executadas: ${stressTest.length}`); -console.log( - ` 📈 Total de resultados: ${stressTest.reduce( - (sum, r) => sum + r.length, - 0 - )}` -); - -// Resumo final -console.log("\n🎯 RESUMO DOS GANHOS"); -console.log("=".repeat(50)); -console.log("✅ FTS5 Full-Text Search implementado"); -console.log("✅ Índices compostos criados"); -console.log("✅ React.memo aplicado aos cards"); -console.log("✅ useMemo para processamento de categorias"); -console.log("✅ Triggers automáticos para sincronização FTS5"); -console.log("\n📈 Ganhos esperados:"); -console.log(" - Busca textual: 10-100x mais rápida"); -console.log(" - Queries com categoria: 5-15x mais rápidas"); -console.log(" - Re-renders desnecessários: reduzidos em 80%"); -console.log(" - Processamento de categorias: otimizado"); - -db.close(); -console.log("\n✅ Benchmark concluído!"); diff --git a/scripts/force-pulsemcp-sync.ts b/scripts/force-pulsemcp-sync.ts deleted file mode 100644 index 896dc7d..0000000 --- a/scripts/force-pulsemcp-sync.ts +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env tsx - -import { MCPSyncService } from "../lib/services/mcp-sync"; -import { MCPCacheRepository } from "../database/repositories/mcp-cache"; -import { PulseMCPService } from "../lib/services/pulsemcp"; - -async function forcePulseMCPSync() { - console.log("🔄 Forçando sincronização completa com PulseMCP..."); - - try { - // 1. Verificar se PulseMCP API está funcionando - console.log("🔍 Verificando conectividade com PulseMCP API..."); - const isHealthy = await PulseMCPService.checkHealth(); - - if (!isHealthy) { - console.error("❌ PulseMCP API não está acessível"); - return; - } - - console.log("✅ PulseMCP API está funcionando"); - - // 2. Limpar cache antigo completamente - console.log("🗑️ Limpando cache antigo do DeepNLP..."); - MCPCacheRepository.clearCache(); - console.log("✅ Cache antigo limpo"); - - // 3. Forçar sincronização completa - console.log("📥 Iniciando download completo de servidores PulseMCP..."); - - const result = await MCPSyncService.syncAll((progress) => { - const percentage = - progress.total > 0 - ? Math.round((progress.current / progress.total) * 100) - : 0; - console.log( - `📊 Progresso: ${progress.current}/${progress.total} (${percentage}%) - Página ${progress.page}/${progress.totalPages}` - ); - }); - - if (result.success) { - console.log(`✅ Sincronização concluída com sucesso!`); - console.log(`📊 Total de servidores baixados: ${result.totalDownloaded}`); - console.log(`⏱️ Tempo total: ${Math.round(result.duration / 1000)}s`); - - // 4. Verificar estatísticas do cache - const stats = MCPCacheRepository.getStats(); - console.log(`📈 Estatísticas do cache:`); - console.log(` - Total de itens: ${stats.totalItems}`); - console.log( - ` - Última sincronização: ${ - stats.lastSync ? new Date(stats.lastSync).toLocaleString() : "Nunca" - }` - ); - console.log(` - Cache válido: ${stats.isValid ? "Sim" : "Não"}`); - - // 5. Testar busca de alguns servidores - console.log("🔍 Testando busca de servidores..."); - const sampleServers = MCPCacheRepository.search({ limit: 5 }); - console.log( - `📋 Encontrados ${sampleServers.length} servidores no cache:` - ); - - sampleServers.forEach((server, index) => { - console.log( - ` ${index + 1}. ${server.content_name} (${server.category})` - ); - }); - } else { - console.error("❌ Sincronização falhou:"); - result.errors.forEach((error) => console.error(` - ${error}`)); - } - } catch (error) { - console.error("❌ Erro durante sincronização:", error); - } -} - -// Executar se chamado diretamente -if (require.main === module) { - forcePulseMCPSync() - .then(() => { - console.log("🎉 Script concluído"); - process.exit(0); - }) - .catch((error) => { - console.error("💥 Erro fatal:", error); - process.exit(1); - }); -} - -export { forcePulseMCPSync }; diff --git a/scripts/init-db.ts b/scripts/init-db.ts index b75d2f6..515eef6 100644 --- a/scripts/init-db.ts +++ b/scripts/init-db.ts @@ -92,56 +92,7 @@ function runMigrations(db: Database.Database): void { console.log("✅ Migração 002_mcp_installations aplicada"); } - // Aplicar migração para tabela MCP marketplace cache se não foi aplicada - if (!appliedNames.has("003_mcp_marketplace_cache")) { - console.log("📋 Aplicando migração: 003_mcp_marketplace_cache"); - - db.exec(` - -- Tabela principal de cache - CREATE TABLE IF NOT EXISTS mcp_marketplace_cache ( - id TEXT PRIMARY KEY, - owner TEXT NOT NULL, - repo TEXT NOT NULL, - content_name TEXT NOT NULL, - publisher_id TEXT NOT NULL, - description TEXT, - category TEXT, - subfield TEXT, - field TEXT, - rating REAL DEFAULT 0, - review_cnt INTEGER DEFAULT 0, - content_tag_list TEXT, - thumbnail_picture TEXT, - website TEXT, - detail_url TEXT, - ext_info JSON, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - - -- Metadados - CREATE TABLE IF NOT EXISTS mcp_cache_metadata ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - - -- Índices - CREATE INDEX IF NOT EXISTS idx_mcp_cache_category ON mcp_marketplace_cache(category); - CREATE INDEX IF NOT EXISTS idx_mcp_cache_subfield ON mcp_marketplace_cache(subfield); - CREATE INDEX IF NOT EXISTS idx_mcp_cache_rating ON mcp_marketplace_cache(rating DESC); - CREATE INDEX IF NOT EXISTS idx_mcp_cache_owner_repo ON mcp_marketplace_cache(owner, repo); - CREATE INDEX IF NOT EXISTS idx_mcp_cache_search ON mcp_marketplace_cache(content_name, description); - `); - - // Registrar migração - db.prepare("INSERT INTO _migrations (name, applied_at) VALUES (?, ?)").run( - "003_mcp_marketplace_cache", - Date.now() - ); - - console.log("✅ Migração 003_mcp_marketplace_cache aplicada"); - } + // Migração de marketplace removida // Inicializar extensão vetorial se disponível try { @@ -231,22 +182,6 @@ if (require.main === module) { ]) .then(async () => { console.log("🎉 Inicialização concluída"); - - // Iniciar sincronização do marketplace se necessário - try { - const { MCPSyncService } = await import("@/lib/services/mcp-sync"); - const { MCPCacheRepository } = await import("@/database/repositories/mcp-cache"); - - if (MCPCacheRepository.needsSync()) { - console.log("🔄 Iniciando sincronização inicial do marketplace..."); - MCPSyncService.backgroundSync(); - } else { - console.log("✅ Cache do marketplace já está atualizado"); - } - } catch (error) { - console.log("⚠️ Não foi possível inicializar sincronização do marketplace:", error); - } - process.exit(0); }) .catch((error) => { diff --git a/scripts/test-pulsemcp-direct.ts b/scripts/test-pulsemcp-direct.ts deleted file mode 100644 index 47c4a42..0000000 --- a/scripts/test-pulsemcp-direct.ts +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env tsx - -async function testPulseMCPDirect() { - console.log("🔍 Testando PulseMCP API diretamente..."); - - try { - const url = - "https://api.pulsemcp.com/v0beta/servers?count_per_page=5&offset=0"; - - console.log(`📡 Fazendo requisição para: ${url}`); - - const response = await fetch(url, { - method: "GET", - headers: { - Accept: "application/json", - "User-Agent": "Ollahub-MCP-Client/1.0 (https://ollahub.com)", - "Content-Type": "application/json", - }, - }); - - console.log(`📊 Status: ${response.status} ${response.statusText}`); - console.log(`📋 Headers:`, Object.fromEntries(response.headers.entries())); - - if (!response.ok) { - const errorText = await response.text(); - console.error(`❌ Erro ${response.status}:`, errorText); - return; - } - - const data = await response.json(); - console.log(`✅ Sucesso! Total de servidores: ${data.total_count}`); - console.log(`📋 Primeiros 3 servidores:`); - - data.servers.slice(0, 3).forEach((server: any, index: number) => { - console.log(` ${index + 1}. ${server.name}`); - console.log(` - Package: ${server.package_name || "N/A"}`); - console.log(` - Registry: ${server.package_registry || "N/A"}`); - console.log(` - Stars: ${server.github_stars || 0}`); - }); - } catch (error) { - console.error("❌ Erro na requisição:", error); - } -} - -testPulseMCPDirect() - .then(() => { - console.log("🎉 Teste concluído"); - process.exit(0); - }) - .catch((error) => { - console.error("💥 Erro fatal:", error); - process.exit(1); - }); diff --git a/scripts/test-simple-sync.ts b/scripts/test-simple-sync.ts deleted file mode 100644 index 4f4978e..0000000 --- a/scripts/test-simple-sync.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { MCPCacheRepository } from "../database/repositories/mcp-cache"; -import { MCPSyncService } from "../lib/services/mcp-sync"; -import { PulseMCPService } from "../lib/services/pulsemcp"; - -async function testSimpleSync() { - console.log("🔄 Testando sincronização simples com PulseMCP..."); - - try { - // 1. Verificar se PulseMCP API está funcionando - console.log("🔍 Verificando conectividade com PulseMCP API..."); - const isHealthy = await PulseMCPService.checkHealth(); - - if (!isHealthy) { - console.error("❌ PulseMCP API não está acessível"); - return; - } - - console.log("✅ PulseMCP API está funcionando"); - - // 2. Limpar cache antigo - console.log("🗑️ Limpando cache antigo..."); - MCPCacheRepository.clearCache(); - console.log("✅ Cache antigo limpo"); - - // 3. Testar chamada direta à API - console.log("📡 Testando chamada direta à API PulseMCP..."); - const response = await PulseMCPService.getAllServers(); - console.log( - `✅ API retornou ${response.servers.length} servidores (total: ${response.total_count})` - ); - - // 4. Salvar alguns servidores no banco para teste - console.log("💾 Salvando primeiros 10 servidores no banco..."); - const testBatch = response.servers.slice(0, 10); - MCPCacheRepository.saveBatch(testBatch); - console.log("✅ Servidores salvos no banco"); - - // 5. Verificar se foram salvos corretamente - console.log("🔍 Verificando dados salvos..."); - const savedServers = MCPCacheRepository.search({ limit: 10 }); - console.log(`📋 Encontrados ${savedServers.length} servidores no banco:`); - - savedServers.forEach((server, index) => { - console.log( - ` ${index + 1}. ${server.content_name} (${server.category})` - ); - }); - - // 6. Testar sincronização completa - console.log("🚀 Iniciando sincronização completa..."); - const result = await MCPSyncService.syncAll((progress) => { - const percentage = - progress.total > 0 - ? Math.round((progress.current / progress.total) * 100) - : 0; - console.log( - `📊 Progresso: ${progress.current}/${progress.total} (${percentage}%)` - ); - }); - - if (result.success) { - console.log(`✅ Sincronização concluída com sucesso!`); - console.log(`📊 Total de servidores baixados: ${result.totalDownloaded}`); - console.log(`⏱️ Tempo total: ${Math.round(result.duration / 1000)}s`); - - // 7. Verificar estatísticas finais - const stats = MCPCacheRepository.getStats(); - console.log(`📈 Estatísticas finais do cache:`); - console.log(` - Total de itens: ${stats.totalItems}`); - console.log( - ` - Última sincronização: ${ - stats.lastSync ? new Date(stats.lastSync).toLocaleString() : "Nunca" - }` - ); - console.log(` - Cache válido: ${stats.isValid ? "Sim" : "Não"}`); - } else { - console.error("❌ Sincronização falhou:"); - result.errors.forEach((error) => console.error(` - ${error}`)); - } - } catch (error) { - console.error("❌ Erro durante teste:", error); - } -} - -testSimpleSync(); - -export { testSimpleSync };