From f96d4ed696d01e772c271724a6365d4f27a3d357 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Wed, 29 Oct 2025 15:35:15 -0300 Subject: [PATCH] refactor(api): remove MCP marketplace cache integration Removed dependencies on MCPCacheRepository and PulseMCPService from MCP routes to simplify installation and listing logic, avoiding reliance on potentially stale cached data and streamlining the codebase. Updated available-for-chat filter to include MCPs without explicit status. --- app/api/mcp/available-for-chat/route.ts | 2 +- app/api/mcp/categories/route.ts | 68 -- app/api/mcp/install/route.ts | 13 - app/api/mcp/installed/route.ts | 58 +- app/api/mcp/list/route.ts | 201 ----- app/api/mcp/server-config/route.ts | 63 +- app/api/mcp/sync/route.ts | 91 -- app/api/mcp/test-pulsemcp/route.ts | 69 -- app/api/ollama/chat/route.ts | 90 +- components/ClientHome.tsx | 14 +- components/mcp/MCPCategoryFilter.tsx | 136 --- components/mcp/MCPManualInstallModal.tsx | 8 +- components/mcp/MCPMarketplaceModal.tsx | 1008 ---------------------- components/mcp/MCPRatingStars.tsx | 74 -- components/mcp/MCPSearchBar.tsx | 31 - components/mcp/VirtualizedMCPGrid.tsx | 1 - database/ollahub-preferences.db-shm | Bin 32768 -> 32768 bytes database/ollahub-preferences.db-wal | Bin 193672 -> 226632 bytes database/ollahub.db-shm | Bin 32768 -> 32768 bytes database/ollahub.db-wal | Bin 1788112 -> 3901672 bytes database/repositories/mcp-cache.ts | 544 ------------ database/schema.sql | 35 +- lib/embeddings.ts | 33 +- lib/prompts/mcp-tools-prompt.ts | 51 +- lib/services/mcp-executor.ts | 83 +- lib/services/mcp-sync.ts | 278 ------ lib/services/pulsemcp.ts | 462 ---------- lib/types/mcp.ts | 72 +- scripts/add-fts-to-mcp-cache.ts | 66 -- scripts/benchmark-performance.ts | 187 ---- scripts/force-pulsemcp-sync.ts | 90 -- scripts/init-db.ts | 67 +- scripts/test-pulsemcp-direct.ts | 53 -- scripts/test-simple-sync.ts | 87 -- 34 files changed, 260 insertions(+), 3775 deletions(-) delete mode 100644 app/api/mcp/categories/route.ts delete mode 100644 app/api/mcp/list/route.ts delete mode 100644 app/api/mcp/sync/route.ts delete mode 100644 app/api/mcp/test-pulsemcp/route.ts delete mode 100644 components/mcp/MCPCategoryFilter.tsx delete mode 100644 components/mcp/MCPMarketplaceModal.tsx delete mode 100644 components/mcp/MCPRatingStars.tsx delete mode 100644 components/mcp/MCPSearchBar.tsx delete mode 100644 components/mcp/VirtualizedMCPGrid.tsx delete mode 100644 database/repositories/mcp-cache.ts delete mode 100644 lib/services/mcp-sync.ts delete mode 100644 lib/services/pulsemcp.ts delete mode 100644 scripts/add-fts-to-mcp-cache.ts delete mode 100644 scripts/benchmark-performance.ts delete mode 100644 scripts/force-pulsemcp-sync.ts delete mode 100644 scripts/test-pulsemcp-direct.ts delete mode 100644 scripts/test-simple-sync.ts 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 5deb7f9060f2b36e3d259f3eed5abbaf16e5ab27..1bf4fd7fa641118cb8d1da965410eaed49710ace 100644 GIT binary patch delta 179 zcmZo@U}|V!s+V}A%K!t63=9J1Kn^nySF%6-ag#6R^ID0HS1Mj+TxaZ#g}%~eTtKRN znAsq6|04mYI1_{U#KyxD=O}D;VVvc-`Ja;xGrIwU5rYYX*=9qx?@a8548{zm4Cb31 I884av0CX)iQUCw| delta 168 zcmZo@U}|V!s+V}A%K!t63=9JLKn^nyKjynLb9>8T=Cu+XuT;FuxWW$qUcN-PbTO&w wVP=EO{f`8o;!F(s6B`e2PGOwoIJtmPVe>yH9p=qRj2Dbff+BeTSK(CL23=${75As0Gx*48kydwK04Cy7OxM*nA@4Kfk0nPzuG^M;=Gt z?*6$h5XD#t3}cPajWvjF2(dPPaT#JPhHKv(eBv1T?ZspiVcS8%~7N(9b051#%a{vGU diff --git a/database/ollahub.db-shm b/database/ollahub.db-shm index b8aa12dbd0f374fc49a015238de9c106f61be4b0..5e93fa90be92a2cb7bf625d2ed79f0aaf880d622 100644 GIT binary patch delta 2079 zcmchXd6bT27{feT<5*t>%N}rzMuPf&zqS^W+oY-y;XhQ zD&@j_zPMcZe3jj}Q*d8$&b|#3myFClF>b_y<=bX=X|pP4Lc*2mJ#TMiyS|tHu?kq9 ze8ZG8Vr{@#Y>CeFKs77p%nBr1d1pDx@qZQimpXO+K2e1$JXINYq6tp7K$j?Q9%a30M&(b( zi?X8q1q1eiq{~d=##8&YVD^Rm|rq<#UCj z;+vs|r=d5!-gOd6CE9bV1ZeN{T zxz7}HHZ}-{-nS#xl_jj^2t^dqO=_dw8Z4(}be?lDt#pROfc? zqCc68XDYK;$X9G-ALofuf~u;H($q~;^pf7xJ6@@DbJjNuhd!{Q_89N8hNA>^y>3=p zJ*lViOXXf@1fdV@n04a=)^d!ZxC14&F_{_6=M%nQJ)8NN-}nQm zoGR-zy`e>3<3r3@-69xUUw7(pP17qLSKN{UzP>ivCfldB(N0-mt|y6_)aP#6^8`!T zz$sq$wAmJ1;9KasS|v1HuWFvh#amj8ufL78sW#I-vu|ya?Y1-KqX;*UOf4F44-e3r zp^WB5Ugd3;@guuAgHNTEqM4fS`DB^1vQ-U#1NHC@e=4nzZ-9-nX*SC~x9{vH+hf0* z5u_w3+(a7p@*q$0G-Jr(HRkdeo7lten98W4W@$mhbVb`>umt4@Q->zBWiW2BpgG6e z#io93-`jTEZx<{;QR1n{&D=ptIx>J~n7}kr60HilQEw}! zz_siQyOsp1QI}@4<0(e*JTLPmi}{9~9Q5kMnsd0r)b!hQhnnavwb0$VNB63w9?*m8 zpojFZI_eR1QfGD1qw1>1)Lp|hLeFZXMrpLhXspKRIgQr@P1GbkuNU;9vgBy8rs`$y z0`mOxY=~DZ$(%zSgE4j0P)(KLHMz|ED{WUKcc61Plxv5qGw<>(hltfRs-gSTQv*Eh zB=fJgl@aa!uHn#o_N#Rvm+$zMt8}eus+FG5z=(F2%SkCkF8>lX!_ayu%81aDa=%C`r{cSML_| zNg?m)I&B>sjJaKn)J&Nkv&=}2WHt{C#?(_|HPLbaaQ6F!Zl$RGgV%6XV9Fb8MSK_`?i0C-CcWPQK!(H#w01&*XnjR+|I)XE9E`avP{|R-$!C1|fD9Q%4mO4&#x}+&j6mKQM&{KFYk&-OMlKG9 z62=b3X^aa%5~mm^p9nUZ9LWEJi<6<0v6FE+<3gzTu`q}^lRt!WfmJe10?BAFPJR~! z6_^hdcoRDfC}YRSv=S=tDgmVT1JmRe$$CJ>1*XZ*(x8kdnNY@~94O;KKFGK)Oq1^w cfxPgJY4V*?sK~7fK8QIts-cW)b>IL10EXV7CjbBd diff --git a/database/ollahub.db-wal b/database/ollahub.db-wal index aaf8910057cee95c094a647662d17d4a5c84f316..7188816334878a372c89a9d761b223c1062d02f1 100644 GIT binary patch delta 277786 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 delta 64 zcmV~$NfCko006)NIph#fxh+H)I`G&6bRh^ac#*-~YrlhYUpIl!LnM|+J!Nu*(o3b* Rc>DNjb$Wx*WIlf9@(1Ce6=eVb 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 };