Files
OllaHub/lib/services/mcp-sync.ts
T
Evandro Fonseca Junior f19bf10ef3 feat(mcp): enhance MCP installation with status tracking and custom configs
- Add /mcp-servers/ to .gitignore for MCP server installations
- Extend mcp_installations table with status, logs, and validation fields
- Refactor MCP install route to support custom configs, enable logs, and async installation
- Improve MCPSyncService with promise-based syncing to prevent concurrent operations

This update improves MCP management by adding robust installation tracking, custom configuration support, and better error handling for a more reliable user experience.
2025-10-28 21:59:00 -03:00

279 lines
7.2 KiB
TypeScript

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,
};
}
}
}