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 ? (
-

{
- 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 ? (
@@ -171,14 +184,16 @@ export function MCPDetailModal({ mcp, isOpen, onClose, onInstall }: MCPDetailMod
Capabilities
- {(mcp.tools || mcp.capabilities || []).map((capability, index) => (
+ {(mcp.tools || []).map((capability, index) => (
- {typeof capability === 'string' ? capability : (capability as any).name || String(capability)}
+ {typeof capability === "string"
+ ? capability
+ : (capability as any).name || String(capability)}
))}
@@ -189,35 +204,54 @@ export function MCPDetailModal({ mcp, isOpen, onClose, onInstall }: MCPDetailMod
{activeTab === "configuration" && (
-
Installation Configuration
+
+ Installation Configuration
+
{mcp.config && mcp.config.length > 0 ? (
{mcp.config.map((configItem, index) => (
-
-
Configuration {index + 1}
+
+
+ Configuration {index + 1}
+
{configItem.mcpServers && (
-
Command
+
+ Command
+
- {configItem.mcpServers.command} {configItem.mcpServers.arguments?.join(' ')}
+ {configItem.mcpServers.command}{" "}
+ {configItem.mcpServers.arguments?.join(" ")}
)}
- {configItem.mcpServers?.env && Object.keys(configItem.mcpServers.env).length > 0 && (
-
-
Environment Variables
-
- {Object.entries(configItem.mcpServers.env).map(([key, value]) => (
-
-
- {key}={String(value)}
-
-
- ))}
+ {configItem.mcpServers?.env &&
+ Object.keys(configItem.mcpServers.env).length >
+ 0 && (
+
+
+ Environment Variables
+
+
+ {Object.entries(
+ configItem.mcpServers.env
+ ).map(([key, value]) => (
+
+
+ {key}={String(value)}
+
+
+ ))}
+
-
- )}
+ )}
))}
@@ -232,25 +266,45 @@ export function MCPDetailModal({ mcp, isOpen, onClose, onInstall }: MCPDetailMod
{mcp.tools && mcp.tools.length > 0 && (
-
Available Tools
+
+ Available Tools
+
{mcp.tools.map((tool, index) => (
-
+
{tool.name}
-
{tool.description}
+
+ {tool.description}
+
{tool.input_schema && (
-
Parameters
+
+ Parameters
+
- {Object.entries(tool.input_schema.properties || {}).map(([param, schema]) => (
-
+ {Object.entries(
+ tool.input_schema.properties || {}
+ ).map(([param, schema]) => (
+
{param}
- {typeof schema === 'object' && schema ? (schema as any).type || 'any' : 'any'}
- {tool.input_schema.required?.includes(param) && (
- *
+ {typeof schema === "object" && schema
+ ? (schema as any).type || "any"
+ : "any"}
+ {tool.input_schema.required?.includes(
+ param
+ ) && (
+
+ *
+
)}
@@ -269,23 +323,10 @@ export function MCPDetailModal({ mcp, isOpen, onClose, onInstall }: MCPDetailMod
{activeTab === "screenshots" && (
Screenshots
- {mcp.screenshotUrls && mcp.screenshotUrls.length > 0 ? (
-
- {mcp.screenshotUrls.map((url, index) => (
-

- ))}
-
- ) : (
-
-
-
No screenshots available
-
- )}
+
+
+
No screenshots available
+
)}
diff --git a/components/mcp/MCPMarketplaceModal.tsx b/components/mcp/MCPMarketplaceModal.tsx
index 74d844e..48b0b9b 100644
--- a/components/mcp/MCPMarketplaceModal.tsx
+++ b/components/mcp/MCPMarketplaceModal.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect, useCallback, useMemo } from "react";
-import { X, Search, Filter, Package, Star, Download, Eye } from "lucide-react";
+import { X, Search, Filter, Package, Star, Eye } from "lucide-react";
import { MCPProvider } from "@/lib/types/mcp";
import { MCPCard } from "./MCPCard";
import { MCPCategoryFilter } from "./MCPCategoryFilter";
@@ -22,7 +22,7 @@ export function MCPMarketplaceModal({
const [searchTerm, setSearchTerm] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
const [sortBy, setSortBy] = useState<
- "rating" | "downloads" | "name" | "recent" | "total_ratings" | "updated_at"
+ "rating" | "name" | "recent" | "total_ratings" | "updated_at"
>("rating");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
const [selectedMCP, setSelectedMCP] = useState
(null);
@@ -283,7 +283,6 @@ export function MCPMarketplaceModal({
className="px-3 py-2 bg-[var(--surface)] border border-[var(--border)] rounded-md text-sm"
>
-
diff --git a/database/ollahub.db-shm b/database/ollahub.db-shm
index 29ed79e..39dedae 100644
Binary files a/database/ollahub.db-shm and b/database/ollahub.db-shm differ
diff --git a/database/ollahub.db-wal b/database/ollahub.db-wal
index f12de47..c3b7445 100644
Binary files a/database/ollahub.db-wal and b/database/ollahub.db-wal differ
diff --git a/database/repositories/mcp-cache.ts b/database/repositories/mcp-cache.ts
index 110431e..deea595 100644
--- a/database/repositories/mcp-cache.ts
+++ b/database/repositories/mcp-cache.ts
@@ -1,4 +1,5 @@
import { getDatabase } from "@/database/database";
+import { PulseMCPService } from "@/lib/services/pulsemcp";
const db = getDatabase();
@@ -99,25 +100,42 @@ export class MCPCacheRepository {
const now = Date.now();
- for (const mcp of mcps) {
- const [owner, repo] = (mcp.id || "").split("/");
+ 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(
- mcp.id || "",
- owner || "",
- repo || "",
- mcp.content_name || "",
- mcp.publisher_id || "",
- mcp.description || null,
- mcp.category || null,
- mcp.subfield || null,
- mcp.field || null,
- parseFloat(mcp.rating || "0"),
- parseInt(mcp.review_cnt || "0"),
- mcp.content_tag_list || null,
- mcp.thumbnail_picture || null,
- mcp.website || null,
- mcp.detail_url || null,
- mcp.ext_info ? JSON.stringify(mcp.ext_info) : null,
+ 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
);
diff --git a/lib/services/deepnlp.ts b/lib/services/deepnlp.ts
index 5c8a7b6..71bbb18 100644
--- a/lib/services/deepnlp.ts
+++ b/lib/services/deepnlp.ts
@@ -1,9 +1,9 @@
-import {
- DeepNLPSearchParams,
- DeepNLPResponse,
- MCPServerConfig,
- MCPTool,
- MCPProvider
+import {
+ DeepNLPSearchParams,
+ DeepNLPResponse,
+ MCPServerConfig,
+ MCPTool,
+ MCPProvider,
} from "@/lib/types/mcp";
import { MCPCache } from "@/lib/cache/mcp-cache";
@@ -12,30 +12,33 @@ const DEEPNLP_API_BASE = "https://www.deepnlp.org/api/mcp_marketplace/v1";
export class DeepNLPService {
// Mapear categorias do projeto para categorias DeepNLP
private static categoryMap = {
- productivity: ['office'],
- development: ['code', 'browser'],
- data: ['database'],
- integration: ['search', 'payment'],
- other: ['map', 'chart', 'finance']
+ productivity: ["office"],
+ development: ["code", "browser"],
+ data: ["database"],
+ integration: ["search", "payment"],
+ other: ["map", "chart", "finance"],
};
// Mapear categorias DeepNLP para categorias internas
- private static reverseCategoryMap: Record = {
- 'office': 'office',
- 'code': 'code',
- 'browser': 'browser',
- 'database': 'database',
- 'search': 'search',
- 'payment': 'payment',
- 'map': 'map',
- 'chart': 'chart',
- 'finance': 'finance'
+ private static reverseCategoryMap: Record = {
+ office: "office",
+ code: "code",
+ browser: "browser",
+ database: "database",
+ search: "search",
+ payment: "payment",
+ map: "map",
+ chart: "chart",
+ finance: "finance",
};
/**
* Buscar servidores MCP na API DeepNLP
*/
- static async searchMCPs(params: DeepNLPSearchParams, retryCount = 0): Promise {
+ static async searchMCPs(
+ params: DeepNLPSearchParams,
+ retryCount = 0
+ ): Promise {
const maxRetries = 3;
const baseDelay = 1000; // 1 segundo
@@ -43,67 +46,88 @@ export class DeepNLPService {
// Verificar cache primeiro
const cacheKey = MCPCache.generateKey(params);
const cachedResult = MCPCache.get(cacheKey);
-
+
if (cachedResult) {
- console.log('Returning cached search results');
+ console.log("Returning cached search results");
return cachedResult;
}
const url = new URL(DEEPNLP_API_BASE);
-
- // Adicionar parâmetros de busca
- if (params.query) url.searchParams.set('query', params.query);
- if (params.category) url.searchParams.set('category', params.category);
- if (params.id) url.searchParams.set('id', params.id);
- if (params.page_id !== undefined) url.searchParams.set('page_id', params.page_id.toString());
- if (params.count_per_page !== undefined) url.searchParams.set('count_per_page', params.count_per_page.toString());
- if (params.offset !== undefined) url.searchParams.set('offset', params.offset.toString());
- if (params.mode) url.searchParams.set('mode', params.mode);
- console.log(`DeepNLP API Request (attempt ${retryCount + 1}):`, url.toString());
+ // Adicionar parâmetros de busca
+ if (params.query) url.searchParams.set("query", params.query);
+ if (params.category) url.searchParams.set("category", params.category);
+ if (params.id) url.searchParams.set("id", params.id);
+ if (params.page_id !== undefined)
+ url.searchParams.set("page_id", params.page_id.toString());
+ if (params.count_per_page !== undefined)
+ url.searchParams.set(
+ "count_per_page",
+ params.count_per_page.toString()
+ );
+ if (params.offset !== undefined)
+ url.searchParams.set("offset", params.offset.toString());
+ if (params.mode) url.searchParams.set("mode", params.mode);
+
+ console.log(
+ `DeepNLP API Request (attempt ${retryCount + 1}):`,
+ url.toString()
+ );
const response = await fetch(url.toString(), {
- method: 'GET',
+ method: "GET",
headers: {
- 'Accept': 'application/json',
- 'User-Agent': 'Ollahub-MCP-Client/1.0'
+ Accept: "application/json",
+ "User-Agent": "Ollahub-MCP-Client/1.0",
},
// Timeout de 10 segundos
- signal: AbortSignal.timeout(10000)
+ signal: AbortSignal.timeout(10000),
});
if (!response.ok) {
- throw new Error(`DeepNLP API error: ${response.status} ${response.statusText}`);
+ throw new Error(
+ `DeepNLP API error: ${response.status} ${response.statusText}`
+ );
}
const data = await response.json();
- console.log('DeepNLP API Response:', data);
+ console.log("DeepNLP API Response:", data);
// Salvar no cache
- MCPCache.setWithDataType(cacheKey, data, 'search');
+ MCPCache.setWithDataType(cacheKey, data, "search");
return data;
} catch (error) {
- console.error(`Error calling DeepNLP API (attempt ${retryCount + 1}):`, error);
-
+ console.error(
+ `Error calling DeepNLP API (attempt ${retryCount + 1}):`,
+ error
+ );
+
// Se ainda temos tentativas restantes, tentar novamente com backoff exponencial
if (retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); // Backoff exponencial
console.log(`Retrying in ${delay}ms...`);
-
- await new Promise(resolve => setTimeout(resolve, delay));
+
+ await new Promise((resolve) => setTimeout(resolve, delay));
return this.searchMCPs(params, retryCount + 1);
}
-
+
// Se esgotamos as tentativas, lançar erro
- throw new Error(`Failed to fetch MCPs after ${maxRetries + 1} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ throw new Error(
+ `Failed to fetch MCPs after ${maxRetries + 1} attempts: ${
+ error instanceof Error ? error.message : "Unknown error"
+ }`
+ );
}
}
/**
* Obter configuração completa de um servidor específico
*/
- static async getServerConfig(id: string, retryCount = 0): Promise {
+ static async getServerConfig(
+ id: string,
+ retryCount = 0
+ ): Promise {
const maxRetries = 3;
const baseDelay = 1000;
@@ -111,52 +135,66 @@ export class DeepNLPService {
// Verificar cache primeiro
const cacheKey = MCPCache.generateServerConfigKey(id);
const cachedResult = MCPCache.get(cacheKey);
-
+
if (cachedResult) {
- console.log('Returning cached server config');
+ console.log("Returning cached server config");
return cachedResult;
}
- const [owner, repo] = id.split('/');
+ const [owner, repo] = id.split("/");
if (!owner || !repo) {
- throw new Error(`Invalid MCP ID format: ${id}. Expected format: owner/repo`);
+ throw new Error(
+ `Invalid MCP ID format: ${id}. Expected format: owner/repo`
+ );
}
const url = `${DEEPNLP_API_BASE}/server/${owner}/${repo}`;
- console.log(`DeepNLP Server Config Request (attempt ${retryCount + 1}):`, url);
+ console.log(
+ `DeepNLP Server Config Request (attempt ${retryCount + 1}):`,
+ url
+ );
const response = await fetch(url, {
- method: 'GET',
+ method: "GET",
headers: {
- 'Accept': 'application/json',
- 'User-Agent': 'Ollahub-MCP-Client/1.0'
+ Accept: "application/json",
+ "User-Agent": "Ollahub-MCP-Client/1.0",
},
- signal: AbortSignal.timeout(10000)
+ signal: AbortSignal.timeout(10000),
});
if (!response.ok) {
- throw new Error(`DeepNLP Server Config API error: ${response.status} ${response.statusText}`);
+ throw new Error(
+ `DeepNLP Server Config API error: ${response.status} ${response.statusText}`
+ );
}
const data = await response.json();
- console.log('DeepNLP Server Config Response:', data);
+ console.log("DeepNLP Server Config Response:", data);
// Salvar no cache
- MCPCache.setWithDataType(cacheKey, data, 'config');
+ MCPCache.setWithDataType(cacheKey, data, "config");
return data;
} catch (error) {
- console.error(`Error getting server config from DeepNLP (attempt ${retryCount + 1}):`, error);
-
+ console.error(
+ `Error getting server config from DeepNLP (attempt ${retryCount + 1}):`,
+ error
+ );
+
if (retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`Retrying server config in ${delay}ms...`);
-
- await new Promise(resolve => setTimeout(resolve, delay));
+
+ await new Promise((resolve) => setTimeout(resolve, delay));
return this.getServerConfig(id, retryCount + 1);
}
-
- throw new Error(`Failed to get server config after ${maxRetries + 1} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`);
+
+ throw new Error(
+ `Failed to get server config after ${maxRetries + 1} attempts: ${
+ error instanceof Error ? error.message : "Unknown error"
+ }`
+ );
}
}
@@ -171,54 +209,68 @@ export class DeepNLPService {
// Verificar cache primeiro
const cacheKey = MCPCache.generateServerToolsKey(id);
const cachedResult = MCPCache.get(cacheKey);
-
+
if (cachedResult) {
- console.log('Returning cached server tools');
+ console.log("Returning cached server tools");
return cachedResult;
}
- const [owner, repo] = id.split('/');
+ const [owner, repo] = id.split("/");
if (!owner || !repo) {
- throw new Error(`Invalid MCP ID format: ${id}. Expected format: owner/repo`);
+ throw new Error(
+ `Invalid MCP ID format: ${id}. Expected format: owner/repo`
+ );
}
const url = `${DEEPNLP_API_BASE}/tools/${owner}/${repo}`;
- console.log(`DeepNLP Server Tools Request (attempt ${retryCount + 1}):`, url);
+ console.log(
+ `DeepNLP Server Tools Request (attempt ${retryCount + 1}):`,
+ url
+ );
const response = await fetch(url, {
- method: 'GET',
+ method: "GET",
headers: {
- 'Accept': 'application/json',
- 'User-Agent': 'Ollahub-MCP-Client/1.0'
+ Accept: "application/json",
+ "User-Agent": "Ollahub-MCP-Client/1.0",
},
- signal: AbortSignal.timeout(10000)
+ signal: AbortSignal.timeout(10000),
});
if (!response.ok) {
- throw new Error(`DeepNLP Server Tools API error: ${response.status} ${response.statusText}`);
+ throw new Error(
+ `DeepNLP Server Tools API error: ${response.status} ${response.statusText}`
+ );
}
const data = await response.json();
- console.log('DeepNLP Server Tools Response:', data);
+ console.log("DeepNLP Server Tools Response:", data);
const tools = data.tools || [];
// Salvar no cache
- MCPCache.setWithDataType(cacheKey, tools, 'tools');
+ MCPCache.setWithDataType(cacheKey, tools, "tools");
return tools;
} catch (error) {
- console.error(`Error getting server tools from DeepNLP (attempt ${retryCount + 1}):`, error);
-
+ console.error(
+ `Error getting server tools from DeepNLP (attempt ${retryCount + 1}):`,
+ error
+ );
+
if (retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`Retrying server tools in ${delay}ms...`);
-
- await new Promise(resolve => setTimeout(resolve, delay));
+
+ await new Promise((resolve) => setTimeout(resolve, delay));
return this.getServerTools(id, retryCount + 1);
}
-
- throw new Error(`Failed to get server tools after ${maxRetries + 1} attempts: ${error instanceof Error ? error.message : 'Unknown error'}`);
+
+ throw new Error(
+ `Failed to get server tools after ${maxRetries + 1} attempts: ${
+ error instanceof Error ? error.message : "Unknown error"
+ }`
+ );
}
}
@@ -226,44 +278,45 @@ export class DeepNLPService {
* Transformar resposta DeepNLP para formato interno MCPProvider
*/
static transformToMCPProvider(deepnlpItem: any): MCPProvider {
- const category = this.mapCategory(deepnlpItem.subfield || 'other');
-
+ const category = this.mapCategory(deepnlpItem.subfield || "other");
+
return {
- id: deepnlpItem.id || '',
- name: deepnlpItem.content_name || '',
- author: deepnlpItem.publisher_id || '',
- description: deepnlpItem.description || '',
- version: '1.0.0', // DeepNLP não fornece versão específica
+ id: deepnlpItem.id || "",
+ name: deepnlpItem.content_name || "",
+ author: deepnlpItem.publisher_id || "",
+ description: deepnlpItem.description || "",
+ version: "1.0.0", // DeepNLP não fornece versão específica
category,
- tags: deepnlpItem.content_tag_list ? deepnlpItem.content_tag_list.split(',').map((tag: string) => tag.trim()) : [],
- rating: parseFloat(deepnlpItem.rating || '0'),
- totalRatings: parseInt(deepnlpItem.review_cnt || '0'),
- downloads: parseInt(deepnlpItem.review_cnt || '0') * 10, // Estimativa baseada em reviews
- iconUrl: deepnlpItem.thumbnail_picture,
- screenshotUrls: [],
- capabilities: deepnlpItem.ext_info?.tools || [],
+ tags: deepnlpItem.content_tag_list
+ ? deepnlpItem.content_tag_list
+ .split(",")
+ .map((tag: string) => tag.trim())
+ : [],
+ rating: parseFloat(deepnlpItem.rating || "0"),
+ totalRatings: parseInt(deepnlpItem.review_cnt || "0"),
repository: deepnlpItem.website,
homepage: deepnlpItem.website,
installed: false, // Será verificado localmente
subfield: deepnlpItem.subfield,
field: deepnlpItem.field,
- config: deepnlpItem.config,
- tools: deepnlpItem.ext_info?.tools || []
+ config: deepnlpItem.config || [],
+ tools: deepnlpItem.ext_info?.tools || [],
};
}
/**
* Mapear categoria DeepNLP para categoria interna
*/
- static mapCategory(subfield: string): MCPProvider['category'] {
- return this.reverseCategoryMap[subfield?.toLowerCase()] || 'other';
+ static mapCategory(subfield: string): MCPProvider["category"] {
+ return this.reverseCategoryMap[subfield?.toLowerCase()] || "other";
}
/**
* Obter mapeamento de categoria interna para DeepNLP
*/
static getCategoryMapping(internalCategory: string): string | undefined {
- const mappings = this.categoryMap[internalCategory as keyof typeof this.categoryMap];
+ const mappings =
+ this.categoryMap[internalCategory as keyof typeof this.categoryMap];
return mappings?.[0]; // Retorna a primeira categoria DeepNLP correspondente
}
@@ -273,15 +326,15 @@ export class DeepNLPService {
static async checkHealth(): Promise {
try {
const response = await fetch(`${DEEPNLP_API_BASE}?count_per_page=1`, {
- method: 'GET',
+ method: "GET",
headers: {
- 'Accept': 'application/json',
- 'User-Agent': 'Ollahub-MCP-Client/1.0'
- }
+ Accept: "application/json",
+ "User-Agent": "Ollahub-MCP-Client/1.0",
+ },
});
return response.ok;
} catch (error) {
- console.error('DeepNLP API health check failed:', error);
+ console.error("DeepNLP API health check failed:", error);
return false;
}
}
diff --git a/lib/services/mcp-sync.ts b/lib/services/mcp-sync.ts
index 1e942cc..f51b586 100644
--- a/lib/services/mcp-sync.ts
+++ b/lib/services/mcp-sync.ts
@@ -1,4 +1,4 @@
-import { DeepNLPService } from "./deepnlp";
+import { PulseMCPService } from "./pulsemcp";
import { MCPCacheRepository } from "@/database/repositories/mcp-cache";
export interface SyncProgress {
@@ -131,143 +131,78 @@ export class MCPSyncService {
}
/**
- * Download paginado de todas as páginas - SEM LIMITE ARTIFICIAL
+ * Download completo de todos os servidores PulseMCP (uma única chamada)
*/
private static async downloadAllPages(
onProgress?: (progress: SyncProgress) => void
): Promise {
- const allMCPs: any[] = [];
- let page = 0;
- let COUNT_PER_PAGE = 100; // Começar com 100
- let totalHits = 0;
- let consecutiveEmptyPages = 0; // Contador para páginas vazias consecutivas
+ try {
+ console.log("📡 Starting complete download from PulseMCP...");
- console.log("📡 Starting unlimited paginated download...");
+ // Notificar início
+ this.currentProgress = {
+ current: 0,
+ total: 0,
+ page: 1,
+ totalPages: 1,
+ };
- while (!this.abortController?.signal.aborted) {
- try {
- const params = {
- mode: "list" as const,
- page_id: page,
- count_per_page: COUNT_PER_PAGE,
- };
+ 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(
- `📄 Downloading page ${
- page + 1
- } (${COUNT_PER_PAGE} items per page)...`
+ `💾 Saving batch ${i + 1}/${totalBatches} (${batch.length} items)...`
);
+ MCPCacheRepository.saveBatch(batch);
- const response = await DeepNLPService.searchMCPs(params);
-
- // Se não há itens na resposta, incrementar contador de páginas vazias
- if (!response.items || response.items.length === 0) {
- consecutiveEmptyPages++;
- console.log(
- `⚠️ Empty page ${
- page + 1
- } (${consecutiveEmptyPages} consecutive empty pages)`
- );
-
- // Se temos 3 páginas vazias consecutivas, parar o download
- if (consecutiveEmptyPages >= 3) {
- console.log(
- "🛑 Stopping download: 3 consecutive empty pages detected"
- );
- break;
- }
-
- page++;
- continue;
- }
-
- // Reset contador de páginas vazias se encontramos dados
- consecutiveEmptyPages = 0;
- allMCPs.push(...response.items);
-
- // Atualizar total hits na primeira página
- if (page === 0) {
- totalHits = response.total_hits;
- console.log(`📊 Total MCPs available: ${totalHits}`);
- }
-
- // Notificar progresso
- this.currentProgress = {
- current: allMCPs.length,
- total: totalHits,
- page: page + 1,
- totalPages:
- totalHits > 0 ? Math.ceil(totalHits / COUNT_PER_PAGE) : page + 1,
- };
-
- if (onProgress) {
- onProgress(this.currentProgress);
- }
-
- // Incrementar página
- page++;
-
- // Salvar em batch a cada 500 itens para evitar bloqueio
- if (allMCPs.length % 500 === 0) {
- console.log(`💾 Saving batch of 500 MCPs to database...`);
- MCPCacheRepository.saveBatch(allMCPs.slice(-500));
- }
-
- // Pequena pausa entre requisições para não sobrecarregar a API
+ // Pequena pausa entre batches
await new Promise((resolve) => setTimeout(resolve, 100));
-
- // Aumentar tamanho da página progressivamente para otimizar
- if (page === 5 && COUNT_PER_PAGE === 100) {
- COUNT_PER_PAGE = 500;
- console.log("🚀 Increasing page size to 500 for faster download");
- } else if (page === 20 && COUNT_PER_PAGE === 500) {
- COUNT_PER_PAGE = 1000;
- console.log("🚀 Increasing page size to 1000 for maximum efficiency");
- }
-
- // Log de progresso a cada 10 páginas
- if (page % 10 === 0) {
- console.log(
- `📈 Progress: ${allMCPs.length} MCPs downloaded (page ${page})`
- );
- }
- } catch (error: any) {
- console.error(`❌ Error downloading page ${page + 1}:`, error);
-
- // Se for erro de abort, parar
- if (this.abortController?.signal.aborted) {
- console.log("🛑 Download cancelled");
- break;
- }
-
- // Se for erro de rede, tentar continuar
- if (
- error.message.includes("fetch") ||
- error.message.includes("network")
- ) {
- console.log("🔄 Network error, retrying in 2 seconds...");
- await new Promise((resolve) => setTimeout(resolve, 2000));
- continue;
- }
-
- // Para outros erros, parar
- throw error;
}
- }
- // Salvar último batch se houver
- if (allMCPs.length > 0) {
- const remainingItems = allMCPs.length % 500;
- if (remainingItems > 0) {
- console.log(`💾 Saving final batch of ${remainingItems} MCPs...`);
- MCPCacheRepository.saveBatch(allMCPs.slice(-remainingItems));
+ 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 [];
}
- }
- console.log(
- `✅ Download completed: ${allMCPs.length} total MCPs downloaded`
- );
- return allMCPs;
+ // Para outros erros, lançar
+ throw error;
+ }
}
/**
diff --git a/lib/services/pulsemcp.ts b/lib/services/pulsemcp.ts
new file mode 100644
index 0000000..a24cf1f
--- /dev/null
+++ b/lib/services/pulsemcp.ts
@@ -0,0 +1,422 @@
+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)
+ */
+ static async getAllServers(): Promise {
+ try {
+ 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) {
+ 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) {
+ console.error("❌ Error calling PulseMCP API:", error);
+ throw new Error(
+ `Failed to fetch MCPs: ${
+ 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 397bbfe..32a5247 100644
--- a/lib/types/mcp.ts
+++ b/lib/types/mcp.ts
@@ -18,10 +18,6 @@ export interface MCPProvider {
tags: string[]; // mapeado de content_tag_list
rating: number; // já existe na API
totalRatings: number; // mapeado de review_cnt
- downloads: number; // opcional, calcular baseado em reviews
- iconUrl?: string; // mapeado de thumbnail_picture
- screenshotUrls?: string[];
- capabilities: string[]; // mapeado de ext_info.tools
repository?: string; // mapeado de website
homepage?: string;
installed: boolean; // verificar localmente
@@ -81,14 +77,48 @@ export interface MCPServerConfig {
export interface MCPSearchParams {
category?: string;
search?: string;
- sort?:
- | "rating"
- | "downloads"
- | "name"
- | "recent"
- | "total_ratings"
- | "updated_at";
+ 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;
+}
+
+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;
+ };
+ };
+}
diff --git a/scripts/force-pulsemcp-sync.ts b/scripts/force-pulsemcp-sync.ts
new file mode 100644
index 0000000..896dc7d
--- /dev/null
+++ b/scripts/force-pulsemcp-sync.ts
@@ -0,0 +1,90 @@
+#!/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/test-pulsemcp-direct.ts b/scripts/test-pulsemcp-direct.ts
new file mode 100644
index 0000000..47c4a42
--- /dev/null
+++ b/scripts/test-pulsemcp-direct.ts
@@ -0,0 +1,53 @@
+#!/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
new file mode 100644
index 0000000..4f4978e
--- /dev/null
+++ b/scripts/test-simple-sync.ts
@@ -0,0 +1,87 @@
+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 };