Skip to main content
Glama
evalstate

Hugging Face MCP Server

by evalstate
gradio-cache.ts•9.6 kB
/** * Two-level cache system for Gradio endpoint discovery * * Cache 1: Space metadata from HuggingFace API (with ETag support) * Cache 2: Gradio schemas from Gradio endpoints */ import { logger } from './logger.js'; import type { Tool } from '@modelcontextprotocol/sdk/types.js'; /** * Configuration for cache TTLs and timeouts */ export const CACHE_CONFIG = { // Space metadata cache TTL (default: 5 minutes) SPACE_METADATA_TTL: parseInt(process.env.GRADIO_SPACE_CACHE_TTL || '300000', 10), // Schema cache TTL (default: 5 minutes) SCHEMA_TTL: parseInt(process.env.GRADIO_SCHEMA_CACHE_TTL || '300000', 10), // Discovery concurrency (default: 10 parallel requests) DISCOVERY_CONCURRENCY: parseInt(process.env.GRADIO_DISCOVERY_CONCURRENCY || '10', 10), // Space info timeout (default: 5 seconds) SPACE_INFO_TIMEOUT: parseInt(process.env.GRADIO_SPACE_INFO_TIMEOUT || '5000', 10), // Schema fetch timeout (default: 7.5 seconds) SCHEMA_TIMEOUT: parseInt(process.env.GRADIO_SCHEMA_TIMEOUT || '7500', 10), }; /** * Cached space metadata from HuggingFace API * * IMPORTANT: Only public spaces are cached. Private spaces are always fetched fresh * for security and to ensure auth-sensitive information is never stale. */ export interface CachedSpaceMetadata { // Core space data _id: string; // e.g., "gradio_evalstate-flux1-schnell" name: string; // e.g., "evalstate/flux1_schnell" subdomain: string; // e.g., "evalstate-flux1-schnell" emoji: string; // e.g., "đź”§" private: boolean; // Used for auth header forwarding sdk: string; // e.g., "gradio", "static", "streamlit" // Optional runtime info runtime?: { stage?: string; // "RUNNING", "SLEEPING", etc. hardware?: string; }; // Cache metadata etag?: string; // For conditional requests (If-None-Match) fetchedAt: number; // Timestamp when entry was created (NOT last access time) } /** * Cached Gradio schema * * IMPORTANT: Only schemas for public spaces are cached. Private space schemas * are always fetched fresh for security. */ export interface CachedSchema { // Schema data tools: Tool[]; // Array of tool definitions with inputSchema // Cache metadata fetchedAt: number; // Timestamp when entry was created (NOT last access time) } /** * Cache statistics for observability */ export interface CacheStats { metadataHits: number; metadataMisses: number; metadataEtagRevalidations: number; schemaHits: number; schemaMisses: number; metadataCacheSize: number; schemaCacheSize: number; } /** * In-memory cache for space metadata */ class SpaceMetadataCache { private cache = new Map<string, CachedSpaceMetadata>(); private stats = { hits: 0, misses: 0, etagRevalidations: 0, }; /** * Get cached metadata if valid (within TTL) * * TTL is calculated from entry creation time (fetchedAt), not last access time. * This ensures entries expire predictably after a fixed duration. */ get(spaceName: string): CachedSpaceMetadata | null { const entry = this.cache.get(spaceName); if (!entry) { this.stats.misses++; logger.trace({ spaceName }, 'Space metadata cache miss'); return null; } // Check age from creation time, not last access const age = Date.now() - entry.fetchedAt; const isValid = age < CACHE_CONFIG.SPACE_METADATA_TTL; if (!isValid) { this.stats.misses++; logger.trace({ spaceName, age, ttl: CACHE_CONFIG.SPACE_METADATA_TTL }, 'Space metadata cache expired'); return null; } this.stats.hits++; logger.trace({ spaceName, age }, 'Space metadata cache hit'); return entry; } /** * Get cached metadata regardless of TTL (for ETag revalidation) */ getForRevalidation(spaceName: string): CachedSpaceMetadata | null { return this.cache.get(spaceName) || null; } /** * Set or update cached metadata */ set(spaceName: string, metadata: CachedSpaceMetadata): void { this.cache.set(spaceName, metadata); logger.trace({ spaceName, hasEtag: !!metadata.etag }, 'Space metadata cached'); } /** * Update timestamp after ETag revalidation (304 response) */ updateTimestamp(spaceName: string): void { const entry = this.cache.get(spaceName); if (entry) { entry.fetchedAt = Date.now(); this.stats.etagRevalidations++; logger.trace({ spaceName }, 'Space metadata timestamp updated after 304'); } } /** * Clear all cached entries and reset statistics */ clear(): void { this.cache.clear(); this.stats.hits = 0; this.stats.misses = 0; this.stats.etagRevalidations = 0; logger.debug('Space metadata cache cleared'); } /** * Get cache statistics */ getStats(): { hits: number; misses: number; etagRevalidations: number; size: number } { return { hits: this.stats.hits, misses: this.stats.misses, etagRevalidations: this.stats.etagRevalidations, size: this.cache.size, }; } } /** * In-memory cache for Gradio schemas */ class SchemaCache { private cache = new Map<string, CachedSchema>(); private stats = { hits: 0, misses: 0, }; /** * Get cached schema if valid (within TTL) * * TTL is calculated from entry creation time (fetchedAt), not last access time. * This ensures entries expire predictably after a fixed duration. */ get(spaceName: string): CachedSchema | null { const entry = this.cache.get(spaceName); if (!entry) { this.stats.misses++; logger.trace({ spaceName }, 'Schema cache miss'); return null; } // Check age from creation time, not last access const age = Date.now() - entry.fetchedAt; const isValid = age < CACHE_CONFIG.SCHEMA_TTL; if (!isValid) { this.stats.misses++; logger.trace({ spaceName, age, ttl: CACHE_CONFIG.SCHEMA_TTL }, 'Schema cache expired'); return null; } this.stats.hits++; logger.trace({ spaceName, age, toolCount: entry.tools.length }, 'Schema cache hit'); return entry; } /** * Set or update cached schema */ set(spaceName: string, schema: CachedSchema): void { this.cache.set(spaceName, schema); logger.trace({ spaceName, toolCount: schema.tools.length }, 'Schema cached'); } /** * Clear all cached entries and reset statistics */ clear(): void { this.cache.clear(); this.stats.hits = 0; this.stats.misses = 0; logger.debug('Schema cache cleared'); } /** * Get cache statistics */ getStats(): { hits: number; misses: number; size: number } { return { hits: this.stats.hits, misses: this.stats.misses, size: this.cache.size, }; } } /** * Module-level singleton cache instances */ export const spaceMetadataCache = new SpaceMetadataCache(); export const schemaCache = new SchemaCache(); /** * Get combined cache statistics */ export function getCacheStats(): CacheStats { const metadataStats = spaceMetadataCache.getStats(); const schemaStats = schemaCache.getStats(); return { metadataHits: metadataStats.hits, metadataMisses: metadataStats.misses, metadataEtagRevalidations: metadataStats.etagRevalidations, schemaHits: schemaStats.hits, schemaMisses: schemaStats.misses, metadataCacheSize: metadataStats.size, schemaCacheSize: schemaStats.size, }; } /** * Clear all caches */ export function clearAllCaches(): void { spaceMetadataCache.clear(); schemaCache.clear(); logger.info('All Gradio caches cleared'); } /** * Log cache statistics at debug level */ export function logCacheStats(): void { const stats = getCacheStats(); const metadataHitRate = stats.metadataHits + stats.metadataMisses > 0 ? ((stats.metadataHits / (stats.metadataHits + stats.metadataMisses)) * 100).toFixed(1) : '0.0'; const schemaHitRate = stats.schemaHits + stats.schemaMisses > 0 ? ((stats.schemaHits / (stats.schemaHits + stats.schemaMisses)) * 100).toFixed(1) : '0.0'; logger.debug( { metadata: { hits: stats.metadataHits, misses: stats.metadataMisses, etagRevalidations: stats.metadataEtagRevalidations, hitRate: `${metadataHitRate}%`, cacheSize: stats.metadataCacheSize, }, schema: { hits: stats.schemaHits, misses: stats.schemaMisses, hitRate: `${schemaHitRate}%`, cacheSize: stats.schemaCacheSize, }, }, 'Gradio cache statistics' ); } /** * Format cache statistics for metrics API */ export function formatCacheMetricsForAPI(): { spaceMetadata: { hits: number; misses: number; hitRate: number; etagRevalidations: number; cacheSize: number; }; schemas: { hits: number; misses: number; hitRate: number; cacheSize: number; }; totalHits: number; totalMisses: number; overallHitRate: number; } { const stats = getCacheStats(); const metadataTotal = stats.metadataHits + stats.metadataMisses; const metadataHitRate = metadataTotal > 0 ? Math.round((stats.metadataHits / metadataTotal) * 10000) / 100 : 0; const schemaTotal = stats.schemaHits + stats.schemaMisses; const schemaHitRate = schemaTotal > 0 ? Math.round((stats.schemaHits / schemaTotal) * 10000) / 100 : 0; const totalHits = stats.metadataHits + stats.schemaHits; const totalMisses = stats.metadataMisses + stats.schemaMisses; const grandTotal = totalHits + totalMisses; const overallHitRate = grandTotal > 0 ? Math.round((totalHits / grandTotal) * 10000) / 100 : 0; return { spaceMetadata: { hits: stats.metadataHits, misses: stats.metadataMisses, hitRate: metadataHitRate, etagRevalidations: stats.metadataEtagRevalidations, cacheSize: stats.metadataCacheSize, }, schemas: { hits: stats.schemaHits, misses: stats.schemaMisses, hitRate: schemaHitRate, cacheSize: stats.schemaCacheSize, }, totalHits, totalMisses, overallHitRate, }; }

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/evalstate/hf-mcp-server'

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