refactor(api): remove MCP marketplace cache integration

Removed dependencies on MCPCacheRepository and PulseMCPService from MCP routes to simplify installation and listing logic, avoiding reliance on potentially stale cached data and streamlining the codebase. Updated available-for-chat filter to include MCPs without explicit status.
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-29 15:35:15 -03:00
1 parent 4739a13458
commit f96d4ed696
34 files changed
+260 -3775

No files matched your search

+31 -2
View File
@@ -9,20 +9,49 @@ export async function generateEmbedding(
model: string = "nomic-embed-text"
): Promise<Float32Array> {
try {
// Verificar se deve forçar uso de CPU (útil quando GPU tem problemas de memória)
const forceCpu =
process.env.OLLAMA_NO_GPU === "1" || process.env.OLLAMA_NUM_GPU === "0";
const response = await ollama.embeddings({
model,
prompt: text,
options: forceCpu ? { num_gpu: 0 } : undefined,
});
return new Float32Array(response.embedding);
} catch (error) {
} catch (error: any) {
console.error("Erro ao gerar embedding:", error);
// Se erro de GPU e ainda não tentou CPU, tentar forçar CPU
const isGpuError =
error?.message?.includes("CUDA") ||
error?.message?.includes("unable to allocate") ||
error?.status_code === 500;
if (
isGpuError &&
process.env.OLLAMA_NO_GPU !== "1" &&
process.env.OLLAMA_NUM_GPU !== "0"
) {
console.warn("Erro de GPU detectado, tentando usar CPU...");
try {
const response = await ollama.embeddings({
model,
prompt: text,
options: { num_gpu: 0 },
});
return new Float32Array(response.embedding);
} catch (cpuError) {
console.error("Erro mesmo usando CPU:", cpuError);
}
}
// Fallback: retornar vetor zero se modelo não estiver disponível
console.warn(
`Modelo ${model} não disponível, usando vetor zero como fallback`
);
return new Float32Array(384); // 384 dimensões padrão
return new Float32Array(768); // 768 dimensões para nomic-embed-text
}
}
+49 -2
View File
@@ -150,6 +150,7 @@ export function extractToolCallFromResponse(response: string): {
const parsed = JSON.parse(jsonText.trim());
// Formato atual suportado pelo prompt engineering
if (parsed.tool_call && parsed.tool_call.name) {
return {
name: parsed.tool_call.name,
@@ -157,14 +158,60 @@ export function extractToolCallFromResponse(response: string): {
};
}
// Formato OpenAI-like: { type: 'function', function: { name, parameters|arguments } }
// Algumas implementações usam `arguments` como string JSON
if (parsed.type === "function" && parsed.function && parsed.function.name) {
const fn = parsed.function;
let params: any = fn.parameters ?? fn.arguments ?? {};
if (typeof params === "string") {
try {
params = JSON.parse(params);
} catch {
// se não for JSON válido, mantém string (executor fará fallback)
}
}
return { name: fn.name, parameters: params || {} };
}
// Formato simples: { name: "tool", parameters: {...} }
if (parsed.name && (parsed.parameters || parsed.arguments)) {
let params: any = parsed.parameters ?? parsed.arguments ?? {};
if (typeof params === "string") {
try {
params = JSON.parse(params);
} catch {
// mantém string se não for JSON válido
}
}
return { name: parsed.name, parameters: params || {} };
}
return null;
} catch (error) {
// Tentar extrair manualmente
const nameMatch = response.match(/"name":\s*"([^"]+)"/);
if (nameMatch) {
const name = nameMatch[1];
const paramsMatch = response.match(/"parameters":\s*({[^}]+})/);
const parameters = paramsMatch ? JSON.parse(paramsMatch[1]) : {};
// Captura tanto parameters quanto arguments
const paramsMatch =
response.match(/"parameters"\s*:\s*({[\s\S]*?})/) ||
response.match(/"arguments"\s*:\s*("[\s\S]*?"|{[\s\S]*?})/);
let parameters: any = {};
if (paramsMatch) {
const raw = paramsMatch[1];
try {
parameters = JSON.parse(raw);
} catch {
// caso venha como string com JSON dentro de aspas
if (raw.startsWith('"') && raw.endsWith('"')) {
try {
parameters = JSON.parse(raw.slice(1, -1));
} catch {
parameters = {};
}
}
}
}
return { name, parameters };
}
+63 -20
View File
@@ -31,16 +31,31 @@ export class MCPExecutor {
}
// Verificar se está pronto
if (mcpData.status !== "ready") {
// Se status é undefined/null, considerar como "ready" (MCPs instalados antes da implementação de status)
if (mcpData.status && mcpData.status !== "ready") {
throw new Error(
`MCP ${mcpId} is not ready (status: ${mcpData.status})`
);
}
// Obter comando executável
const config = JSON.parse(mcpData.config);
const command = config.command || mcpData.executable_command;
const args = config.args || [];
// mcpData.config já vem parseado do repositório, mas pode ser string em casos legados
const config =
typeof mcpData.config === "string"
? JSON.parse(mcpData.config)
: mcpData.config || {};
const commandFromConfig = config.command;
const argsFromConfig = Array.isArray(config.args) ? config.args : [];
let command = commandFromConfig;
let args = argsFromConfig;
// Se não houver command em config, reconstruir a partir de executable_command (se existir)
if (!command && mcpData.executable_command) {
const parts = String(mcpData.executable_command).trim().split(/\s+/);
command = parts[0];
args = parts.slice(1);
}
if (!command) {
throw new Error(`No executable command found for MCP ${mcpId}`);
@@ -100,14 +115,14 @@ export class MCPExecutor {
): Promise<any> {
return new Promise((resolve, reject) => {
const env = {
...process.env,
...globalThis.process.env,
...(environmentPath && {
PATH: `${environmentPath}/node_modules/.bin:${process.env.PATH}`,
PATH: `${environmentPath}/node_modules/.bin:${globalThis.process.env.PATH}`,
}),
};
const process = spawn(command, args, {
cwd: environmentPath || process.cwd(),
const childProcess = spawn(command, args, {
cwd: environmentPath || globalThis.process.cwd(),
env,
stdio: ["pipe", "pipe", "pipe"],
});
@@ -119,26 +134,35 @@ export class MCPExecutor {
// Timeout
const timeout = setTimeout(() => {
if (!responseReceived) {
process.kill();
childProcess.kill();
reject(new Error(`Execution timeout after ${EXECUTION_TIMEOUT}ms`));
}
}, EXECUTION_TIMEOUT);
// Capturar stdout
process.stdout?.on("data", (data) => {
childProcess.stdout?.on("data", (data) => {
stdout += data.toString();
// Loga por linhas para diagnóstico
const printedLines = data
.toString()
.split("\n")
.filter((l: string) => l.trim().length > 0);
for (const l of printedLines) {
console.log(`📥 [MCP stdout] ${l}`);
}
// Tentar parsear respostas JSON-RPC
const lines = stdout.split("\n");
for (const line of lines) {
const parsedLines = stdout.split("\n");
for (const line of parsedLines) {
if (line.trim()) {
try {
const parsed = JSON.parse(line);
console.log("🔎 [MCP stdout parsed]", parsed);
if (parsed.id === 2 && parsed.result) {
// Resposta do tools/call
clearTimeout(timeout);
responseReceived = true;
process.kill();
childProcess.kill();
resolve(parsed.result);
}
} catch (e) {
@@ -149,18 +173,25 @@ export class MCPExecutor {
});
// Capturar stderr
process.stderr?.on("data", (data) => {
childProcess.stderr?.on("data", (data) => {
stderr += data.toString();
const errLines = data
.toString()
.split("\n")
.filter((l: string) => l.trim().length > 0);
for (const l of errLines) {
console.warn(`⚠️ [MCP stderr] ${l}`);
}
});
// Erro de processo
process.on("error", (error) => {
childProcess.on("error", (error) => {
clearTimeout(timeout);
reject(new Error(`Process error: ${error.message}`));
});
// Processo encerrou
process.on("close", (code) => {
childProcess.on("close", (code) => {
clearTimeout(timeout);
if (!responseReceived) {
if (code !== 0) {
@@ -186,7 +217,8 @@ export class MCPExecutor {
},
id: 1,
};
process.stdin?.write(JSON.stringify(initRequest) + "\n");
console.log("📤 [MCP send] initialize", initRequest);
childProcess.stdin?.write(JSON.stringify(initRequest) + "\n");
// Aguardar um pouco antes de enviar tool call
setTimeout(() => {
@@ -200,12 +232,13 @@ export class MCPExecutor {
},
id: 2,
};
process.stdin?.write(JSON.stringify(toolCallRequest) + "\n");
process.stdin?.end();
console.log("📤 [MCP send] tools/call", toolCallRequest);
childProcess.stdin?.write(JSON.stringify(toolCallRequest) + "\n");
childProcess.stdin?.end();
}, 500);
} catch (error: any) {
clearTimeout(timeout);
process.kill();
childProcess.kill();
reject(new Error(`Failed to send JSON-RPC request: ${error.message}`));
}
});
@@ -274,6 +307,16 @@ export class MCPExecutor {
if (expectedType) {
const actualType = typeof value;
// Tratar objeto vazio como "ausente" quando tipo esperado não é object
if (
value &&
actualType === "object" &&
!Array.isArray(value) &&
Object.keys(value).length === 0 &&
expectedType !== "object"
) {
continue;
}
const valid =
(expectedType === "string" && actualType === "string") ||
(expectedType === "number" && actualType === "number") ||
-278
View File
@@ -1,278 +0,0 @@
import { PulseMCPService } from "./pulsemcp";
import { MCPCacheRepository } from "@/database/repositories/mcp-cache";
export interface SyncProgress {
current: number;
total: number;
page: number;
totalPages: number;
}
export interface SyncResult {
success: boolean;
totalDownloaded: number;
errors: string[];
duration: number;
}
export class MCPSyncService {
private static isSyncingFlag = false;
private static syncPromise: Promise<SyncResult> | null = null;
private static abortController: AbortController | null = null;
private static currentProgress: SyncProgress | null = null;
/**
* Download completo com paginação incremental
*/
static async syncAll(
onProgress?: (progress: SyncProgress) => void
): Promise<SyncResult> {
// Se já existe um sync em andamento, retornar a promise existente
if (this.syncPromise) {
console.log("⏳ Sync already in progress, waiting for completion...");
return this.syncPromise;
}
const startTime = Date.now();
const errors: string[] = [];
let totalDownloaded = 0;
// Criar a promise de sync e armazená-la
this.syncPromise = (async () => {
try {
if (this.isSyncingFlag) {
throw new Error("Sync already in progress");
}
this.isSyncingFlag = true;
this.abortController = new AbortController();
console.log("🔄 Starting MCP marketplace sync...");
// Limpar cache antigo antes de começar
MCPCacheRepository.clearCache();
const allMCPs = await this.downloadAllPages(onProgress);
totalDownloaded = allMCPs.length;
// Salvar todos os MCPs no banco
if (allMCPs.length > 0) {
MCPCacheRepository.saveBatch(allMCPs);
// Atualizar metadados
MCPCacheRepository.updateMetadata("last_sync", Date.now().toString());
MCPCacheRepository.updateMetadata(
"total_items",
allMCPs.length.toString()
);
console.log(`✅ Sync completed: ${totalDownloaded} MCPs downloaded`);
}
const duration = Date.now() - startTime;
return {
success: true,
totalDownloaded,
errors,
duration,
};
} catch (error: any) {
console.error("❌ Sync failed:", error);
errors.push(error.message);
const duration = Date.now() - startTime;
return {
success: false,
totalDownloaded,
errors,
duration,
};
} finally {
this.isSyncingFlag = false;
this.abortController = null;
this.currentProgress = null;
this.syncPromise = null; // Limpar a promise compartilhada
}
})();
return this.syncPromise;
}
/**
* Sincronização em background (usado automaticamente)
*/
static async backgroundSync(): Promise<void> {
try {
if (this.isSyncingFlag) {
console.log("Sync already in progress, skipping background sync");
return;
}
console.log("🔄 Starting background MCP sync...");
await this.syncAll((progress) => {
console.log(
`📥 Downloaded ${progress.current}/${progress.total} MCPs (page ${progress.page}/${progress.totalPages})`
);
});
} catch (error) {
console.error("Background sync failed:", error);
}
}
/**
* Verificar se sincronização está em andamento
*/
static isSyncing(): boolean {
return this.isSyncingFlag;
}
/**
* Cancelar sincronização atual
*/
static cancelSync(): void {
if (this.abortController) {
this.abortController.abort();
console.log("🛑 Sync cancelled by user");
}
}
/**
* Obter progresso atual da sincronização
*/
static getCurrentProgress(): SyncProgress | null {
return this.currentProgress;
}
/**
* Download completo de todos os servidores PulseMCP (uma única chamada)
*/
private static async downloadAllPages(
onProgress?: (progress: SyncProgress) => void
): Promise<any[]> {
try {
console.log("📡 Starting complete download from PulseMCP...");
// Notificar início
this.currentProgress = {
current: 0,
total: 0,
page: 1,
totalPages: 1,
};
if (onProgress) {
onProgress(this.currentProgress);
}
// Fazer uma única chamada para buscar todos os servidores
const response = await PulseMCPService.getAllServers();
console.log(`📊 Total servers available: ${response.total_count}`);
console.log(`📥 Downloaded ${response.servers.length} servers`);
// Atualizar progresso
this.currentProgress = {
current: response.servers.length,
total: response.total_count,
page: 1,
totalPages: 1,
};
if (onProgress) {
onProgress(this.currentProgress);
}
// Salvar em batches de 500 para evitar bloqueio
const batchSize = 500;
const totalBatches = Math.ceil(response.servers.length / batchSize);
for (let i = 0; i < totalBatches; i++) {
const start = i * batchSize;
const end = Math.min(start + batchSize, response.servers.length);
const batch = response.servers.slice(start, end);
console.log(
`💾 Saving batch ${i + 1}/${totalBatches} (${batch.length} items)...`
);
MCPCacheRepository.saveBatch(batch);
// Pequena pausa entre batches
await new Promise((resolve) => setTimeout(resolve, 100));
}
console.log(
`✅ Download completed: ${response.servers.length} total MCPs downloaded from PulseMCP`
);
return response.servers;
} catch (error: any) {
console.error(`❌ Error downloading from PulseMCP:`, error);
// Se for erro de abort, parar
if (this.abortController?.signal.aborted) {
console.log("🛑 Download cancelled");
return [];
}
// Para outros erros, lançar
throw error;
}
}
/**
* Verificar se precisa sincronizar
*/
static needsSync(): boolean {
return MCPCacheRepository.needsSync();
}
/**
* Obter estatísticas do cache
*/
static getCacheStats() {
return MCPCacheRepository.getStats();
}
/**
* Forçar limpeza do cache
*/
static clearCache(): void {
MCPCacheRepository.clearCache();
console.log("🗑️ Cache cleared");
}
/**
* Verificar saúde do sistema de cache
*/
static async healthCheck(): Promise<{
cacheExists: boolean;
cacheValid: boolean;
totalItems: number;
lastSync: number | null;
needsSync: boolean;
}> {
try {
const cacheExists = MCPCacheRepository.tableExists(
"mcp_marketplace_cache"
);
const stats = MCPCacheRepository.getStats();
const needsSync = MCPCacheRepository.needsSync();
return {
cacheExists,
cacheValid: stats.isValid,
totalItems: stats.totalItems,
lastSync: stats.lastSync,
needsSync,
};
} catch (error) {
console.error("Health check failed:", error);
return {
cacheExists: false,
cacheValid: false,
totalItems: 0,
lastSync: null,
needsSync: true,
};
}
}
}
-462
View File
@@ -1,462 +0,0 @@
import {
PulseMCPSearchParams,
PulseMCPResponse,
MCPServerMetadata,
MCPProvider,
} from "@/lib/types/mcp";
import { MCPCache } from "@/lib/cache/mcp-cache";
const PULSEMCP_API_BASE = "https://api.pulsemcp.com/v0beta";
export class PulseMCPService {
// Mapear categorias do PulseMCP para categorias internas
private static categoryMap: Record<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)
* Com retry automático e backoff exponencial para rate limiting
*/
static async getAllServers(retryCount = 0): Promise<PulseMCPResponse> {
const maxRetries = 3;
const baseDelay = 2000; // 2 segundos base
try {
if (retryCount > 0) {
console.log(
`📡 Retry ${retryCount}/${maxRetries} - Fetching from PulseMCP API...`
);
} else {
console.log("📡 Fetching all servers from PulseMCP API...");
}
const response = await fetch(`${PULSEMCP_API_BASE}/servers`, {
method: "GET",
headers: {
Accept: "application/json",
"User-Agent": "Ollahub-MCP-Client/1.0 (https://ollahub.com)",
},
// Timeout de 30 segundos para download completo
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
// Se for 429 (Too Many Requests), tentar novamente com backoff
if (response.status === 429 && retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); // Backoff exponencial
console.warn(
`⚠️ Rate limit hit (429). Waiting ${delay}ms before retry ${
retryCount + 1
}/${maxRetries}...`
);
await new Promise((resolve) => setTimeout(resolve, delay));
return this.getAllServers(retryCount + 1);
}
throw new Error(
`PulseMCP API error: ${response.status} ${response.statusText}`
);
}
const data = await response.json();
console.log(
`✅ PulseMCP API returned ${data.servers.length} servers (total: ${data.total_count})`
);
return data;
} catch (error) {
// Se for erro de rate limit e ainda temos retries, tentar novamente
if (
error instanceof Error &&
error.message.includes("429") &&
retryCount < maxRetries
) {
const delay = baseDelay * Math.pow(2, retryCount);
console.warn(
`⚠️ Rate limit error. Waiting ${delay}ms before retry ${
retryCount + 1
}/${maxRetries}...`
);
await new Promise((resolve) => setTimeout(resolve, delay));
return this.getAllServers(retryCount + 1);
}
console.error("❌ Error calling PulseMCP API:", error);
throw new Error(
`Failed to fetch MCPs after ${retryCount + 1} attempts: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
}
/**
* Transformar resposta PulseMCP para formato interno MCPProvider
*/
static transformToMCPProvider(pulseMCPItem: MCPServerMetadata): MCPProvider {
const meta = pulseMCPItem._meta?.["com.pulsemcp"];
const category = this.mapCategory(
pulseMCPItem.name,
pulseMCPItem.short_description
);
// Usar name como ID se package_name não estiver disponível
const id =
pulseMCPItem.package_name ||
pulseMCPItem.name.toLowerCase().replace(/[^a-z0-9]/g, "-");
return {
id: id,
name: meta?.standardized_name || pulseMCPItem.name,
author: meta?.standardized_provider_name || "Unknown",
description:
meta?.standardized_description || pulseMCPItem.short_description,
version: "1.0.0", // PulseMCP não fornece versão específica
category,
tags: this.extractTags(pulseMCPItem.name, pulseMCPItem.short_description),
rating: this.calculateRating(pulseMCPItem),
totalRatings: pulseMCPItem.github_stars || 0,
repository: pulseMCPItem.source_code_url,
homepage: pulseMCPItem.external_url || pulseMCPItem.source_code_url,
installed: false, // Será verificado localmente
subfield: category.toUpperCase(),
field: "MCP SERVER",
config: this.generateConfigTemplate(pulseMCPItem),
tools: [], // PulseMCP não fornece schemas de ferramentas
};
}
/**
* Mapear categoria baseada no nome e descrição
*/
private static mapCategory(
name: string,
description: string
): MCPProvider["category"] {
const text = `${name} ${description}`.toLowerCase();
// Buscar por palavras-chave específicas
for (const [keyword, category] of Object.entries(this.categoryMap)) {
if (text.includes(keyword)) {
return category;
}
}
return "other";
}
/**
* Extrair tags baseadas no nome e descrição
*/
private static extractTags(name: string, description: string): string[] {
const text = `${name} ${description}`.toLowerCase();
const tags: string[] = [];
// Tags comuns
const commonTags = [
"api",
"database",
"filesystem",
"github",
"google",
"aws",
"gcp",
"azure",
"slack",
"discord",
"notion",
"calendar",
"email",
"maps",
"weather",
"finance",
"crypto",
"payment",
"chart",
"analytics",
"monitoring",
"security",
"auth",
"webhook",
"scraping",
"automation",
"ai",
"ml",
];
for (const tag of commonTags) {
if (text.includes(tag)) {
tags.push(tag);
}
}
return tags.slice(0, 5); // Limitar a 5 tags
}
/**
* Calcular rating baseado em métricas disponíveis
*/
private static calculateRating(item: MCPServerMetadata): number {
const meta = item._meta?.["com.pulsemcp"];
let rating = 3.0; // Rating base
// Ajustar baseado em downloads
if (meta?.estimated_downloads_all_time) {
const downloads = meta.estimated_downloads_all_time;
if (downloads > 1000000) rating += 1.0;
else if (downloads > 100000) rating += 0.5;
else if (downloads > 10000) rating += 0.2;
}
// Ajustar baseado em GitHub stars
if (item.github_stars) {
if (item.github_stars > 1000) rating += 0.5;
else if (item.github_stars > 100) rating += 0.2;
}
// Ajustar se é oficial
if (meta?.is_official) {
rating += 0.5;
}
return Math.min(5.0, Math.max(1.0, rating));
}
/**
* Extrair capacidades baseadas no nome e descrição
*/
private static extractCapabilities(item: MCPServerMetadata): string[] {
const text = `${item.name} ${item.short_description}`.toLowerCase();
const capabilities: string[] = [];
// Capacidades comuns
const capabilityMap = {
read: ["read", "get", "fetch", "retrieve"],
write: ["write", "create", "update", "modify", "edit"],
delete: ["delete", "remove", "destroy"],
search: ["search", "find", "query", "filter"],
sync: ["sync", "synchronize", "backup"],
monitor: ["monitor", "watch", "track", "observe"],
authenticate: ["auth", "login", "authenticate"],
notify: ["notify", "alert", "message", "send"],
};
for (const [capability, keywords] of Object.entries(capabilityMap)) {
if (keywords.some((keyword) => text.includes(keyword))) {
capabilities.push(capability);
}
}
return capabilities.slice(0, 5); // Limitar a 5 capacidades
}
/**
* Gerar template de configuração para o servidor
*/
private static generateConfigTemplate(item: MCPServerMetadata): any[] {
const config: any[] = [];
// Configuração básica
config.push({
type: "server",
name: (item.package_name || item.name).replace(/^@[\w-]+\//, ""),
command: this.getCommand(item.package_registry),
args: this.getArgs(item),
});
// Adicionar variáveis de ambiente se necessário
const envVars = this.getEnvTemplate(item);
if (Object.keys(envVars).length > 0) {
config.push({
type: "env",
variables: envVars,
});
}
return config;
}
/**
* Obter comando baseado no registry
*/
private static getCommand(registry: string | null): string {
switch (registry) {
case "npm":
return "npx";
case "pypi":
return "python";
case "cargo":
return "cargo";
default:
return "node";
}
}
/**
* Obter argumentos baseados no registry
*/
private static getArgs(item: MCPServerMetadata): string[] {
switch (item.package_registry) {
case "npm":
return ["-y", item.package_name || item.name];
case "pypi":
return ["-m", item.package_name || item.name];
case "cargo":
return ["run", "--bin", item.package_name || item.name];
default:
return [item.package_name || item.name];
}
}
/**
* Obter template de variáveis de ambiente
*/
private static getEnvTemplate(
item: MCPServerMetadata
): Record<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 [];
}
}
}
+2 -70
View File
@@ -54,51 +54,7 @@ export interface MCPTool {
};
}
export interface DeepNLPSearchParams {
query?: string;
category?: string;
id?: string;
page_id?: number;
count_per_page?: number;
offset?: number;
mode?: "list" | "dict";
}
export interface DeepNLPResponse {
query?: string;
items: any[];
count: number;
total_hits: number;
}
export interface MCPServerConfig {
total_hits: number;
id: string;
items: any[];
}
export interface MCPSearchParams {
category?: string;
search?: string;
sort?: "rating" | "name" | "recent" | "total_ratings" | "updated_at";
order?: "asc" | "desc";
limit?: number;
offset?: number;
}
// PulseMCP API Types
export interface PulseMCPSearchParams {
query?: string;
integrations?: string[];
count_per_page?: number;
offset?: number;
}
export interface PulseMCPResponse {
servers: MCPServerMetadata[];
next?: string;
total_count: number;
}
// Tipos relacionados a busca/marketplace foram removidos
export interface MCPTestResult {
success: boolean;
@@ -113,28 +69,4 @@ export interface MCPTestRequest {
mcpId: string;
}
export interface MCPServerMetadata {
name: string;
url: string;
external_url?: string;
short_description: string;
source_code_url: string;
github_stars?: number;
package_registry: "npm" | "pypi" | "cargo";
package_name: string;
package_download_count?: number;
EXPERIMENTAL_ai_generated_description?: string;
_meta: {
"com.pulsemcp": {
estimated_downloads_all_time: number;
estimated_downloads_last_30_days: number;
estimated_downloads_last_7_days: number;
standardized_name: string;
standardized_description: string;
standardized_provider_name: string;
standardized_provider_url: string;
estimated_released_on: string;
is_official: boolean;
};
};
}
// Metadados específicos de PulseMCP removidos