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:
1 parent
ce010d39ee
commit
1042f2136b
15 files changed
+1237
-398
No files matched your search
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user