From 1042f2136ba65dc2f72ba1e55452acd2f6def228 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Tue, 28 Oct 2025 20:19:00 -0300 Subject: [PATCH] refactor(api): migrate MCP listing from DeepNLP to PulseMCP service - Updated imports to use PulseMCPSearchParams and PulseMCPService instead of DeepNLP equivalents - Replaced DeepNLP API calls with PulseMCP API endpoints and parameters - Modified cached MCP transformation to manual mapping for PulseMCP data format - Enhanced error handling and retry logic in server tools retrieval - Added PulseMCP API base URL constant for centralized configuration This change switches the underlying MCP provider from DeepNLP to PulseMCP, ensuring compatibility with the new API while maintaining existing functionality for MCP listing and tool retrieval. --- app/api/mcp/list/route.ts | 85 +++-- app/api/mcp/test-pulsemcp/route.ts | 69 ++++ components/mcp/MCPCard.tsx | 83 ++--- components/mcp/MCPDetailModal.tsx | 185 ++++++----- components/mcp/MCPMarketplaceModal.tsx | 5 +- database/ollahub.db-shm | Bin 32768 -> 32768 bytes database/ollahub.db-wal | Bin 4152992 -> 4165352 bytes database/repositories/mcp-cache.ts | 54 ++-- lib/services/deepnlp.ts | 269 +++++++++------- lib/services/mcp-sync.ts | 181 ++++------- lib/services/pulsemcp.ts | 422 +++++++++++++++++++++++++ lib/types/mcp.ts | 52 ++- scripts/force-pulsemcp-sync.ts | 90 ++++++ scripts/test-pulsemcp-direct.ts | 53 ++++ scripts/test-simple-sync.ts | 87 +++++ 15 files changed, 1237 insertions(+), 398 deletions(-) create mode 100644 app/api/mcp/test-pulsemcp/route.ts create mode 100644 lib/services/pulsemcp.ts create mode 100644 scripts/force-pulsemcp-sync.ts create mode 100644 scripts/test-pulsemcp-direct.ts create mode 100644 scripts/test-simple-sync.ts diff --git a/app/api/mcp/list/route.ts b/app/api/mcp/list/route.ts index 96424ee..d1fe336 100644 --- a/app/api/mcp/list/route.ts +++ b/app/api/mcp/list/route.ts @@ -2,13 +2,15 @@ import { NextResponse } from "next/server"; import { MCPProvider, MCPSearchParams, - DeepNLPSearchParams, + PulseMCPSearchParams, } from "@/lib/types/mcp"; -import { DeepNLPService } from "@/lib/services/deepnlp"; +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) { @@ -55,10 +57,25 @@ export async function GET(request: Request) { if (cachedMcps.length > 0 || cacheValid) { console.log(`Found ${cachedMcps.length} MCPs in cache`); - // Transformar para formato interno - const mcps = cachedMcps.map((item) => - DeepNLPService.transformToMCPProvider(item) - ); + // Os dados já estão no formato interno após transformação no saveBatch + const mcps = cachedMcps.map((item) => ({ + id: item.id, + name: item.content_name, + author: item.publisher_id, + description: item.description || "", + version: "1.0.0", + category: item.category as MCPProvider["category"], + tags: item.content_tag_list ? item.content_tag_list.split(",") : [], + rating: item.rating, + totalRatings: item.review_cnt, + repository: item.detail_url || "", + homepage: item.website || "", + installed: false, // Será definido abaixo + subfield: item.subfield || "", + field: item.field || "", + config: [], + tools: [], + })); // Adicionar status de instalação const installedMCPs = MCPRepository.listInstalled(); @@ -81,32 +98,46 @@ export async function GET(request: Request) { }); } - // 3. Fallback: buscar da API se cache vazio - console.log("Cache empty, falling back to DeepNLP API"); + // 3. Fallback: buscar da API PulseMCP se cache vazio + console.log("Cache empty, falling back to PulseMCP API"); - const deepnlpCategory = - category && category !== "all" - ? DeepNLPService.getCategoryMapping(category) - : undefined; - - const deepnlpParams: DeepNLPSearchParams = { + const pulseMCPParams: PulseMCPSearchParams = { query: search, - category: deepnlpCategory, - page_id: page, count_per_page: limit, - mode: "list", + offset: page * limit, }; - console.log("Calling DeepNLP API with params:", deepnlpParams); + console.log("Calling PulseMCP API with params:", pulseMCPParams); - const response = await DeepNLPService.searchMCPs(deepnlpParams); - const mcps = response.items.map((item) => - DeepNLPService.transformToMCPProvider(item) + 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) => ({ + const mcpsWithStatus = mcps.map((mcp: MCPProvider) => ({ ...mcp, installed: installedMCPs.includes(mcp.id), })); @@ -114,12 +145,12 @@ export async function GET(request: Request) { return NextResponse.json({ success: true, mcps: mcpsWithStatus, - total: response.total_hits, - hasMore: response.total_hits > (page + 1) * limit, + total: data.total_count, + hasMore: data.total_count > (page + 1) * limit, cached: false, }); } catch (error: any) { - console.error("Error fetching MCPs from DeepNLP:", error); + 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"); @@ -136,13 +167,13 @@ export async function GET(request: Request) { tags: ["maps", "location", "directions"], rating: 4.5, totalRatings: 2, - downloads: 20, - capabilities: ["geocoding", "directions", "places"], repository: "https://github.com/modelcontextprotocol/servers/tree/main/src/google-maps", installed: false, subfield: "MAP", field: "MCP SERVER", + config: [], + tools: [], }, ]; diff --git a/app/api/mcp/test-pulsemcp/route.ts b/app/api/mcp/test-pulsemcp/route.ts new file mode 100644 index 0000000..c3d8028 --- /dev/null +++ b/app/api/mcp/test-pulsemcp/route.ts @@ -0,0 +1,69 @@ +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/components/mcp/MCPCard.tsx b/components/mcp/MCPCard.tsx index 3271b81..42de537 100644 --- a/components/mcp/MCPCard.tsx +++ b/components/mcp/MCPCard.tsx @@ -1,6 +1,13 @@ "use client"; -import { Package, Star, Download, Eye, ExternalLink, CheckCircle, Zap } from "lucide-react"; +import { + Package, + Star, + Eye, + ExternalLink, + CheckCircle, + Zap, +} from "lucide-react"; import { MCPProvider } from "@/lib/types/mcp"; interface MCPCardProps { @@ -10,19 +17,34 @@ interface MCPCardProps { onViewDetails: () => void; } -export function MCPCard({ mcp, onInstall, onUninstall, onViewDetails }: MCPCardProps) { +export function MCPCard({ + mcp, + onInstall, + onUninstall, + onViewDetails, +}: MCPCardProps) { const getCategoryColor = (category: string) => { switch (category) { - case "map": return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"; - case "browser": return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"; - case "office": return "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"; - case "search": return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"; - case "database": return "bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200"; - case "finance": return "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200"; - case "code": return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"; - case "chart": return "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200"; - case "payment": return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"; - default: return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200"; + case "map": + return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"; + case "browser": + return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"; + case "office": + return "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"; + case "search": + return "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"; + case "database": + return "bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200"; + case "finance": + return "bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200"; + case "code": + return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"; + case "chart": + return "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200"; + case "payment": + return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"; + default: + return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200"; } }; @@ -32,39 +54,32 @@ export function MCPCard({ mcp, onInstall, onUninstall, onViewDetails }: MCPCardP return downloads.toString(); }; - const isOfficial = mcp.tags?.includes('official') || false; - const toolCount = mcp.tools?.length || mcp.capabilities?.length || 0; + const isOfficial = mcp.tags?.includes("official") || false; + const toolCount = mcp.tools?.length || 0; return (
{/* Header */}
- {mcp.iconUrl ? ( - {mcp.name} { - e.currentTarget.style.display = 'none'; - e.currentTarget.nextElementSibling?.classList.remove('hidden'); - }} - /> - ) : null} -
+

{mcp.name}

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

by {mcp.author}

+

+ by {mcp.author} +

- + {mcp.category}
@@ -81,10 +96,6 @@ export function MCPCard({ mcp, onInstall, onUninstall, onViewDetails }: MCPCardP {mcp.rating} ({mcp.totalRatings})
-
- - {formatDownloads(mcp.downloads)} -
{toolCount > 0 && (
@@ -122,7 +133,7 @@ export function MCPCard({ mcp, onInstall, onUninstall, onViewDetails }: MCPCardP Details - + {mcp.installed ? (