Skip to main content
Glama
portel-dev

NCP - Natural Context Provider

by portel-dev
cache-patcher.ts12.8 kB
/** * Cache Patcher for NCP * Provides incremental, MCP-by-MCP cache patching operations * Enables fast startup by avoiding full re-indexing */ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; import { join } from 'path'; import { createHash } from 'crypto'; import { getCacheDirectory } from '../utils/ncp-paths.js'; import { logger } from '../utils/logger.js'; export interface Tool { name: string; description: string; inputSchema?: any; } export interface ToolMetadataCache { version: string; profileHash: string; // SHA256 of entire profile lastModified: number; mcps: { [mcpName: string]: { configHash: string; // SHA256 of command+args+env discoveredAt: number; tools: Array<{ name: string; description: string; inputSchema: any; }>; serverInfo: { name: string; version: string; description?: string; }; } } } export interface EmbeddingsCache { version: string; modelVersion: string; // all-MiniLM-L6-v2 lastModified: number; vectors: { [toolId: string]: number[]; // toolId = "mcpName:toolName" }; metadata: { [toolId: string]: { mcpName: string; generatedAt: number; enhancedDescription: string; // Used for generation } } } export interface MCPConfig { command?: string; // Optional: for stdio transport args?: string[]; env?: Record<string, string>; url?: string; // Optional: for HTTP/SSE transport } export class CachePatcher { private cacheDir: string; private toolMetadataCachePath: string; private embeddingsCachePath: string; private embeddingsMetadataCachePath: string; constructor() { this.cacheDir = getCacheDirectory(); this.toolMetadataCachePath = join(this.cacheDir, 'all-tools.json'); this.embeddingsCachePath = join(this.cacheDir, 'embeddings.json'); this.embeddingsMetadataCachePath = join(this.cacheDir, 'embeddings-metadata.json'); // Ensure cache directory exists if (!existsSync(this.cacheDir)) { mkdirSync(this.cacheDir, { recursive: true }); } } /** * Generate SHA256 hash for MCP configuration */ generateConfigHash(config: MCPConfig): string { const hashInput = JSON.stringify({ command: config.command, args: config.args || [], env: config.env || {} }); return createHash('sha256').update(hashInput).digest('hex'); } /** * Generate SHA256 hash for entire profile */ generateProfileHash(profile: any): string { const hashInput = JSON.stringify(profile.mcpServers || {}); return createHash('sha256').update(hashInput).digest('hex'); } /** * Load cache with atomic file operations and error handling */ private async loadCache<T>(path: string, defaultValue: T): Promise<T> { try { if (!existsSync(path)) { logger.debug(`Cache file not found: ${path}, using default`); return defaultValue; } const content = readFileSync(path, 'utf-8'); const parsed = JSON.parse(content); logger.debug(`Loaded cache from ${path}`); return parsed as T; } catch (error: any) { logger.warn(`Failed to load cache from ${path}: ${error.message}, using default`); return defaultValue; } } /** * Save cache with atomic file operations to prevent corruption */ private async saveCache<T>(path: string, data: T): Promise<void> { try { const tmpPath = `${path}.tmp`; const content = JSON.stringify(data, null, 2); // Write to temporary file first writeFileSync(tmpPath, content, 'utf-8'); // Atomic replacement await this.atomicReplace(tmpPath, path); logger.debug(`Saved cache to ${path}`); } catch (error: any) { logger.error(`Failed to save cache to ${path}: ${error.message}`); throw error; } } /** * Atomic file replacement to prevent corruption */ private async atomicReplace(tmpPath: string, finalPath: string): Promise<void> { const fs = await import('fs/promises'); await fs.rename(tmpPath, finalPath); } /** * Load tool metadata cache */ async loadToolMetadataCache(): Promise<ToolMetadataCache> { const defaultCache: ToolMetadataCache = { version: '1.0.0', profileHash: '', lastModified: Date.now(), mcps: {} }; return await this.loadCache(this.toolMetadataCachePath, defaultCache); } /** * Save tool metadata cache */ async saveToolMetadataCache(cache: ToolMetadataCache): Promise<void> { cache.lastModified = Date.now(); await this.saveCache(this.toolMetadataCachePath, cache); } /** * Load embeddings cache */ async loadEmbeddingsCache(): Promise<EmbeddingsCache> { const defaultCache: EmbeddingsCache = { version: '1.0.0', modelVersion: 'all-MiniLM-L6-v2', lastModified: Date.now(), vectors: {}, metadata: {} }; return await this.loadCache(this.embeddingsCachePath, defaultCache); } /** * Save embeddings cache */ async saveEmbeddingsCache(cache: EmbeddingsCache): Promise<void> { cache.lastModified = Date.now(); await this.saveCache(this.embeddingsCachePath, cache); } /** * Patch tool metadata cache - Add MCP */ async patchAddMCP(mcpName: string, config: MCPConfig, tools: Tool[], serverInfo: any): Promise<void> { logger.info(`🔧 Patching tool metadata cache: adding ${mcpName}`); const cache = await this.loadToolMetadataCache(); const configHash = this.generateConfigHash(config); cache.mcps[mcpName] = { configHash, discoveredAt: Date.now(), tools: tools.map(tool => ({ name: tool.name, description: tool.description || 'No description available', inputSchema: tool.inputSchema || {} })), serverInfo: { name: serverInfo?.name || mcpName, version: serverInfo?.version || '1.0.0', description: serverInfo?.description } }; await this.saveToolMetadataCache(cache); logger.info(`✅ Added ${tools.length} tools from ${mcpName} to metadata cache`); } /** * Patch tool metadata cache - Remove MCP */ async patchRemoveMCP(mcpName: string): Promise<void> { logger.info(`🔧 Patching tool metadata cache: removing ${mcpName}`); const cache = await this.loadToolMetadataCache(); if (cache.mcps[mcpName]) { const toolCount = cache.mcps[mcpName].tools.length; delete cache.mcps[mcpName]; await this.saveToolMetadataCache(cache); logger.info(`✅ Removed ${toolCount} tools from ${mcpName} from metadata cache`); } else { logger.warn(`MCP ${mcpName} not found in metadata cache`); } } /** * Patch tool metadata cache - Update MCP */ async patchUpdateMCP(mcpName: string, config: MCPConfig, tools: Tool[], serverInfo: any): Promise<void> { logger.info(`🔧 Patching tool metadata cache: updating ${mcpName}`); // Remove then add for clean update await this.patchRemoveMCP(mcpName); await this.patchAddMCP(mcpName, config, tools, serverInfo); } /** * Patch embeddings cache - Add MCP tools */ async patchAddEmbeddings(mcpName: string, toolEmbeddings: Map<string, any>): Promise<void> { logger.info(`🔧 Patching embeddings cache: adding ${mcpName} vectors`); const cache = await this.loadEmbeddingsCache(); let addedCount = 0; for (const [toolId, embeddingData] of toolEmbeddings) { if (embeddingData && embeddingData.embedding) { // Convert Float32Array to regular array for JSON serialization cache.vectors[toolId] = Array.from(embeddingData.embedding); cache.metadata[toolId] = { mcpName, generatedAt: Date.now(), enhancedDescription: embeddingData.enhancedDescription || '' }; addedCount++; } } await this.saveEmbeddingsCache(cache); logger.info(`✅ Added ${addedCount} embeddings for ${mcpName}`); } /** * Patch embeddings cache - Remove MCP tools */ async patchRemoveEmbeddings(mcpName: string): Promise<void> { logger.info(`🔧 Patching embeddings cache: removing ${mcpName} vectors`); const cache = await this.loadEmbeddingsCache(); let removedCount = 0; // Remove all tool embeddings for this MCP const toolIdsToRemove = Object.keys(cache.metadata).filter( toolId => cache.metadata[toolId].mcpName === mcpName ); for (const toolId of toolIdsToRemove) { delete cache.vectors[toolId]; delete cache.metadata[toolId]; removedCount++; } await this.saveEmbeddingsCache(cache); logger.info(`✅ Removed ${removedCount} embeddings for ${mcpName}`); } /** * Update profile hash in tool metadata cache */ async updateProfileHash(profileHash: string): Promise<void> { const cache = await this.loadToolMetadataCache(); cache.profileHash = profileHash; await this.saveToolMetadataCache(cache); logger.debug(`Updated profile hash: ${profileHash.substring(0, 8)}...`); } /** * Validate if cache is current with profile */ async validateCacheWithProfile(currentProfileHash: string): Promise<boolean> { try { const cache = await this.loadToolMetadataCache(); // Handle empty or corrupt cache if (!cache || !cache.profileHash) { logger.info('Cache validation failed: no profile hash found'); return false; } // Handle version mismatches if (cache.version !== '1.0.0') { logger.info(`Cache validation failed: version mismatch (${cache.version} → 1.0.0)`); return false; } const isValid = cache.profileHash === currentProfileHash; if (!isValid) { logger.info(`Cache validation failed: profile changed (${cache.profileHash?.substring(0, 8)}... → ${currentProfileHash.substring(0, 8)}...)`); } else { logger.debug(`Cache validation passed: ${currentProfileHash.substring(0, 8)}...`); } return isValid; } catch (error: any) { logger.warn(`Cache validation error: ${error.message}`); return false; } } /** * Validate cache integrity and repair if needed */ async validateAndRepairCache(): Promise<{ valid: boolean; repaired: boolean }> { try { const stats = await this.getCacheStats(); if (!stats.toolMetadataExists) { logger.warn('Tool metadata cache missing'); return { valid: false, repaired: false }; } const cache = await this.loadToolMetadataCache(); // Check for corruption if (!cache.mcps || typeof cache.mcps !== 'object') { logger.warn('Cache corruption detected: invalid mcps structure'); return { valid: false, repaired: false }; } // Check for missing tools let hasMissingTools = false; for (const [mcpName, mcpData] of Object.entries(cache.mcps)) { if (!Array.isArray(mcpData.tools)) { logger.warn(`Cache corruption detected: invalid tools array for ${mcpName}`); hasMissingTools = true; } } if (hasMissingTools) { logger.warn('Cache has missing or invalid tool data'); return { valid: false, repaired: false }; } logger.debug('Cache integrity validation passed'); return { valid: true, repaired: false }; } catch (error: any) { logger.error(`Cache validation failed: ${error.message}`); return { valid: false, repaired: false }; } } /** * Get cache statistics */ async getCacheStats(): Promise<{ toolMetadataExists: boolean; embeddingsExists: boolean; mcpCount: number; toolCount: number; embeddingCount: number; lastModified: Date | null; }> { const toolMetadataExists = existsSync(this.toolMetadataCachePath); const embeddingsExists = existsSync(this.embeddingsCachePath); let mcpCount = 0; let toolCount = 0; let embeddingCount = 0; let lastModified: Date | null = null; if (toolMetadataExists) { try { const cache = await this.loadToolMetadataCache(); mcpCount = Object.keys(cache.mcps).length; toolCount = Object.values(cache.mcps).reduce((sum, mcp) => sum + mcp.tools.length, 0); lastModified = new Date(cache.lastModified); } catch (error) { // Ignore errors for stats } } if (embeddingsExists) { try { const cache = await this.loadEmbeddingsCache(); embeddingCount = Object.keys(cache.vectors).length; } catch (error) { // Ignore errors for stats } } return { toolMetadataExists, embeddingsExists, mcpCount, toolCount, embeddingCount, lastModified }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/portel-dev/ncp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server