Skip to main content
Glama

hypertool-mcp

cache.tsโ€ข6.11 kB
/** * Tool cache implementation using node-cache with TTL support */ import { EventEmitter } from "events"; import NodeCache from "node-cache"; import { IToolCache, DiscoveredTool, DiscoveryConfig } from "./types.js"; /** * Tool cache implementation using node-cache with TTL and server indexing */ export class ToolCache extends EventEmitter implements IToolCache { private cache: NodeCache; private serverIndex = new Map<string, Set<string>>(); private stats = { hits: 0, misses: 0, sets: 0, deletes: 0, }; private config: DiscoveryConfig; constructor(config: DiscoveryConfig) { super(); this.config = config; // Initialize node-cache with TTL in seconds const ttlSeconds = Math.floor((config.cacheTtl || 300000) / 1000); this.cache = new NodeCache({ stdTTL: ttlSeconds, checkperiod: Math.floor(ttlSeconds * 0.1), // Check for expired keys every 10% of TTL useClones: false, // Better performance, we manage object mutations ourselves }); // Listen for cache events this.cache.on("set", (key, value) => { this.emit("set", { key, tool: value }); }); this.cache.on("del", (key, value) => { this.emit("delete", { key, tool: value }); }); this.cache.on("expired", (key, value) => { this.emit("expired", { key, tool: value }); // Clean up server index when items expire this.removeFromServerIndex(key, value?.serverName); }); } /** * Store a tool in the cache */ async set(key: string, tool: DiscoveredTool, ttl?: number): Promise<void> { // Set TTL in seconds if provided, otherwise use default if (ttl) { const ttlSeconds = Math.ceil(ttl / 1000); // Convert ms to seconds, round up this.cache.set(key, tool, ttlSeconds); } else { this.cache.set(key, tool); } // Update server index const serverKey = tool.serverName; if (!this.serverIndex.has(serverKey)) { this.serverIndex.set(serverKey, new Set()); } this.serverIndex.get(serverKey)!.add(key); this.stats.sets++; } /** * Retrieve a tool from the cache */ async get(key: string): Promise<DiscoveredTool | null> { const tool = this.cache.get<DiscoveredTool>(key); if (tool) { this.stats.hits++; this.emit("hit", { key, tool }); return tool; } else { this.stats.misses++; this.emit("miss", { key }); return null; } } /** * Check if a tool exists in cache */ async has(key: string): Promise<boolean> { return this.cache.has(key); } /** * Remove a tool from cache */ async delete(key: string): Promise<void> { const tool = this.cache.get<DiscoveredTool>(key); const success = this.cache.del(key); if (success && tool) { this.removeFromServerIndex(key, tool.serverName); this.stats.deletes++; } } /** * Clear all tools for a server */ async clearServer(serverName: string): Promise<void> { const serverTools = this.serverIndex.get(serverName); if (serverTools) { const keys = Array.from(serverTools); const toolCount = keys.length; this.cache.del(keys); this.serverIndex.delete(serverName); this.stats.deletes += toolCount; this.emit("serverCleared", { serverName, toolCount, clearedKeys: keys, }); } } /** * Clear entire cache */ async clear(): Promise<void> { this.cache.flushAll(); this.serverIndex.clear(); this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0, }; } /** * Get tools by server name */ getToolsByServer(serverName: string): DiscoveredTool[] { const serverTools = this.serverIndex.get(serverName); if (!serverTools) { return []; } const tools: DiscoveredTool[] = []; for (const key of serverTools) { const tool = this.cache.get<DiscoveredTool>(key); if (tool) { tools.push(tool); } else { // Clean up stale index entry serverTools.delete(key); } } return tools; } /** * Get all cached tools */ getAllTools(): DiscoveredTool[] { const keys = this.cache.keys(); const tools: DiscoveredTool[] = []; for (const key of keys) { const tool = this.cache.get<DiscoveredTool>(key); if (tool) { tools.push(tool); } } return tools; } /** * Get cache statistics */ async getStats(): Promise<{ size: number; hitRate: number; missRate: number; }> { const total = this.stats.hits + this.stats.misses; return { size: this.cache.keys().length, hitRate: total > 0 ? this.stats.hits / total : 0, missRate: total > 0 ? this.stats.misses / total : 0, }; } /** * Get internal cache statistics */ getCacheStats() { return { ...this.stats, keys: this.cache.keys().length, serverCount: this.serverIndex.size, }; } /** * Manually trigger cleanup of expired entries */ async cleanupExpiredEntries(): Promise<void> { // node-cache handles this automatically, but we can emit a cleanup event const beforeKeys = this.cache.keys().length; // Force a check for expired keys this.cache.keys().forEach((key) => { this.cache.get(key); // This will trigger expiration check }); const afterKeys = this.cache.keys().length; const cleanedCount = beforeKeys - afterKeys; if (cleanedCount > 0) { this.emit("cleanup", { cleanedCount }); } } /** * Destroy the cache and cleanup resources */ destroy(): void { this.cache.close(); this.serverIndex.clear(); this.removeAllListeners(); } /** * Remove a key from the server index */ private removeFromServerIndex(key: string, serverName?: string): void { if (!serverName) return; const serverTools = this.serverIndex.get(serverName); if (serverTools) { serverTools.delete(key); if (serverTools.size === 0) { this.serverIndex.delete(serverName); } } } }

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/toolprint/hypertool-mcp'

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