Skip to main content
Glama
portel-dev

NCP - Natural Context Provider

by portel-dev
mcp-update-checker.ts6.99 kB
/** * MCP Update Checker * Detects version updates for installed MCPs and manages cache invalidation */ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; import { randomBytes } from 'crypto'; import chalk from 'chalk'; import { logger } from './logger.js'; export interface MCPVersionInfo { name: string; currentVersion: string; latestVersion?: string; hasUpdate: boolean; lastChecked?: number; } interface MCPUpdateCache { [mcpName: string]: { currentVersion: string; latestVersion?: string; lastCheck: number; notificationShown: boolean; }; } export class MCPUpdateChecker { private cacheFile: string; private readonly checkInterval = 24 * 60 * 60 * 1000; // 24 hours private inFlightRequests: Map<string, Promise<string | null>> = new Map(); constructor() { const ncpDir = join(homedir(), '.ncp'); if (!existsSync(ncpDir)) { mkdirSync(ncpDir, { recursive: true }); } this.cacheFile = join(ncpDir, 'mcp-updates.json'); } private loadCache(): MCPUpdateCache { try { if (!existsSync(this.cacheFile)) { return {}; } return JSON.parse(readFileSync(this.cacheFile, 'utf8')); } catch { return {}; } } private saveCache(cache: MCPUpdateCache): void { try { // Atomic write: write to temp file then rename const tempFile = `${this.cacheFile}.${randomBytes(4).toString('hex')}`; writeFileSync(tempFile, JSON.stringify(cache, null, 2)); // Atomic rename (POSIX-compliant, works on Windows too) const fs = require('fs'); fs.renameSync(tempFile, this.cacheFile); } catch (error) { logger.debug(`Failed to save MCP update cache: ${error}`); } } /** * Fetch latest version from npm registry with deduplication * Prevents duplicate requests if multiple tools call this simultaneously */ private async fetchLatestVersionFromNpm(packageName: string): Promise<string | null> { // Return in-flight request if it exists if (this.inFlightRequests.has(packageName)) { return this.inFlightRequests.get(packageName)!; } const promise = (async () => { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 3000); const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, { signal: controller.signal }); clearTimeout(timeout); if (!response.ok) { return null; } const data = await response.json(); return data.version || null; } catch (error) { logger.debug(`Failed to fetch latest version for ${packageName}: ${error}`); return null; } finally { // Clean up in-flight request this.inFlightRequests.delete(packageName); } })(); this.inFlightRequests.set(packageName, promise); return promise; } /** * Compare semantic versions * Returns true if latestVersion > currentVersion */ private compareVersions(current: string, latest: string): boolean { try { const parseVersion = (v: string) => { return v.split('.').map(num => { const match = num.match(/^\d+/); return match ? parseInt(match[0], 10) : 0; }); }; const currentParts = parseVersion(current); const latestParts = parseVersion(latest); for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) { const currentPart = currentParts[i] || 0; const latestPart = latestParts[i] || 0; if (latestPart > currentPart) return true; if (latestPart < currentPart) return false; } return false; } catch { return false; } } /** * Check for updates for a specific MCP */ async checkMCPUpdate(mcpName: string, currentVersion: string, packageName?: string): Promise<MCPVersionInfo> { const cache = this.loadCache(); const now = Date.now(); const pkg = packageName || `@modelcontextprotocol/server-${mcpName}`; // Check cache validity const cachedInfo = cache[mcpName]; const shouldCheck = !cachedInfo || (now - cachedInfo.lastCheck) > this.checkInterval; let latestVersion = cachedInfo?.latestVersion; if (shouldCheck) { const fetchedVersion = await this.fetchLatestVersionFromNpm(pkg); if (fetchedVersion) { latestVersion = fetchedVersion; // Update cache if (!cache[mcpName]) { cache[mcpName] = { currentVersion, latestVersion: fetchedVersion, lastCheck: now, notificationShown: false }; } else { cache[mcpName].latestVersion = fetchedVersion; cache[mcpName].lastCheck = now; } this.saveCache(cache); } } const hasUpdate = latestVersion ? this.compareVersions(currentVersion, latestVersion) : false; return { name: mcpName, currentVersion, latestVersion, hasUpdate, lastChecked: cachedInfo?.lastCheck }; } /** * Get inline notification message for outdated MCP */ getUpdateNotification(mcpInfo: MCPVersionInfo): string | null { if (!mcpInfo.hasUpdate || !mcpInfo.latestVersion) { return null; } return chalk.yellow( `⚠️ "${mcpInfo.name}" v${mcpInfo.currentVersion} → v${mcpInfo.latestVersion} available. ` + `Update with: ${chalk.cyan(`ncp update ${mcpInfo.name}`)}` ); } /** * Record that notification was shown to avoid spam */ markNotificationShown(mcpName: string): void { const cache = this.loadCache(); if (cache[mcpName]) { cache[mcpName].notificationShown = true; this.saveCache(cache); } } /** * Check if notification should be shown (once per day per MCP) */ shouldShowNotification(mcpName: string): boolean { const cache = this.loadCache(); const info = cache[mcpName]; if (!info) return true; if (info.notificationShown) return false; // Re-show notification if it's been more than 24 hours const now = Date.now(); return (now - info.lastCheck) > this.checkInterval; } /** * Clear version cache for an MCP (called after update) */ invalidateMCPCache(mcpName: string): void { const cache = this.loadCache(); delete cache[mcpName]; this.saveCache(cache); logger.debug(`Invalidated update cache for ${mcpName}`); } /** * Get all MCPs with available updates */ async checkAllMCPUpdates( mcps: Array<{ name: string; version: string; packageName?: string }> ): Promise<MCPVersionInfo[]> { const results: MCPVersionInfo[] = []; for (const mcp of mcps) { const info = await this.checkMCPUpdate(mcp.name, mcp.version, mcp.packageName); results.push(info); } return results.filter(info => info.hasUpdate); } }

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