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.
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-28 20:19:00 -03:00
1 parent ce010d39ee
commit 1042f2136b
15 files changed
+1237 -398

No files matched your search

+161 -108
View File
@@ -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<string, MCPProvider['category']> = {
'office': 'office',
'code': 'code',
'browser': 'browser',
'database': 'database',
'search': 'search',
'payment': 'payment',
'map': 'map',
'chart': 'chart',
'finance': 'finance'
private static reverseCategoryMap: Record<string, MCPProvider["category"]> = {
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<DeepNLPResponse> {
static async searchMCPs(
params: DeepNLPSearchParams,
retryCount = 0
): Promise<DeepNLPResponse> {
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<MCPServerConfig> {
static async getServerConfig(
id: string,
retryCount = 0
): Promise<MCPServerConfig> {
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<boolean> {
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;
}
}
+58 -123
View File
@@ -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<any[]> {
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;
}
}
/**
+422
View File
@@ -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<string, MCPProvider["category"]> = {
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<PulseMCPResponse> {
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<string, string> {
const template: Record<string, string> = {};
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<boolean> {
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<MCPServerMetadata[]> {
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<MCPServerMetadata[]> {
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 [];
}
}
}