Skip to main content
Glama
resource-manager.ts13 kB
/** * MCP Resource Manager * Manages lifecycle of MCP resources (register, update, cleanup) * Part of Jaxon Digital Optimizely DXP MCP Server - DXP-136 Phase 1 */ import { DXPEvent, getResourceTypeFromEvent, isTerminalEvent } from './event-types'; import { getGlobalNotificationSender } from './notification-sender'; /** * Resource metadata */ export interface ResourceMetadata { operationId: string; resourceType: string; project: string; environment: string; createdAt: number; updatedAt: number; isTerminal: boolean; completedAt: number | null; } /** * Resource state */ export interface ResourceState extends Record<string, any> { eventType: string; lastUpdated: string; } /** * MCP Resource */ export interface Resource { uri: string; name: string; description: string; mimeType: string; state: ResourceState; metadata: ResourceMetadata; } /** * Resource filter options */ export interface ResourceFilters { type?: string; terminal?: boolean; project?: string; } /** * Resource statistics */ export interface ResourceStats { total: number; active: number; terminal: number; byType: { deployment: number; export: number; download: number; }; } /** * Resource Manager * Tracks all active MCP resources and their state */ export class ResourceManager { private resources: Map<string, Resource>; private subscriptions: Map<string, Set<string>>; private cleanupTTL: number; private cleanupInterval: NodeJS.Timeout | null; constructor() { // Map of resourceUri -> resource data this.resources = new Map<string, Resource>(); // Map of resourceUri -> set of subscriber client IDs (DXP-134) this.subscriptions = new Map<string, Set<string>>(); // Cleanup configuration this.cleanupTTL = 5 * 60 * 1000; // 5 minutes after completion this.cleanupInterval = null; } /** * Start automatic cleanup timer */ startCleanup(): void { if (this.cleanupInterval) return; this.cleanupInterval = setInterval(() => { this.cleanupExpiredResources(); }, 60 * 1000); // Check every minute } /** * Stop automatic cleanup timer */ stopCleanup(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } } /** * Register or update a resource based on an event * @param event - Event object * @returns Resource URI */ registerOrUpdateResource(event: DXPEvent): string { const resourceType = getResourceTypeFromEvent(event.eventType); const resourceUri = `${resourceType}://${event.operationId}`; const existingResource = this.resources.get(resourceUri); const now = Date.now(); const resource: Resource = { uri: resourceUri, name: this.generateResourceName(event), description: this.generateResourceDescription(event), mimeType: 'application/json', // Resource state state: { ...(event.data || {}), eventType: event.eventType, lastUpdated: event.timestamp }, // Metadata metadata: { operationId: event.operationId, resourceType, project: event.project || 'unknown', environment: event.environment || 'unknown', createdAt: existingResource ? existingResource.metadata.createdAt : now, updatedAt: now, isTerminal: isTerminalEvent(event.eventType), completedAt: isTerminalEvent(event.eventType) ? now : null } }; this.resources.set(resourceUri, resource); if (process.env.DEBUG === 'true') { console.error(`[RESOURCE] ${existingResource ? 'Updated' : 'Registered'}: ${resourceUri}`); } // DXP-134: Auto-notify subscribers when resource is updated if (existingResource && this.hasSubscribers(resourceUri)) { const notificationSender = getGlobalNotificationSender(); if (notificationSender) { // Send notification asynchronously (non-blocking) setImmediate(() => { notificationSender.sendResourceUpdated(resourceUri).catch(err => { console.error(`[RESOURCE] Failed to notify subscribers for ${resourceUri}:`, err.message); }); }); if (process.env.DEBUG === 'true') { const subscriberCount = this.getSubscribers(resourceUri).length; console.error(`[RESOURCE] Notifying ${subscriberCount} subscriber(s) for ${resourceUri}`); } } } return resourceUri; } /** * Generate human-readable resource name * @param event - Event object * @returns Resource name */ private generateResourceName(event: DXPEvent): string { const type = getResourceTypeFromEvent(event.eventType); let name = `${type} ${event.operationId}`; if (event.project) { name = `${event.project} ${name}`; } if (event.environment) { name = `${name} (${event.environment})`; } return name; } /** * Generate resource description with current status * @param event - Event object * @returns Resource description */ private generateResourceDescription(event: DXPEvent): string { const type = getResourceTypeFromEvent(event.eventType); const status = event.eventType.split('.')[1] || 'unknown'; let desc = `${type} operation status: ${status}`; if (event.data && event.data.progress !== undefined) { desc += ` (${event.data.progress}% complete)`; } return desc; } /** * Get a resource by URI * @param resourceUri - Resource URI * @returns Resource data or null */ getResource(resourceUri: string): Resource | null { return this.resources.get(resourceUri) || null; } /** * Get all resources, optionally filtered * @param filters - Filter options * @returns Array of resources */ listResources(filters: ResourceFilters = {}): Resource[] { let resources = Array.from(this.resources.values()); // Filter by resource type if (filters.type) { resources = resources.filter(r => r.metadata.resourceType === filters.type); } // Filter by terminal state if (filters.terminal !== undefined) { resources = resources.filter(r => r.metadata.isTerminal === filters.terminal); } // Filter by project if (filters.project) { resources = resources.filter(r => r.metadata.project === filters.project); } return resources; } /** * Read resource contents (returns current state as JSON) * @param resourceUri - Resource URI * @returns Resource contents as JSON string */ readResource(resourceUri: string): string { const resource = this.getResource(resourceUri); if (!resource) { throw new Error(`Resource not found: ${resourceUri}`); } return JSON.stringify({ uri: resource.uri, state: resource.state, metadata: resource.metadata, lastUpdated: resource.state.lastUpdated }, null, 2); } /** * Remove a resource * @param resourceUri - Resource URI * @returns True if removed */ removeResource(resourceUri: string): boolean { const removed = this.resources.delete(resourceUri); // Also remove any subscriptions for this resource (DXP-134) this.subscriptions.delete(resourceUri); if (removed && process.env.DEBUG === 'true') { console.error(`[RESOURCE] Removed: ${resourceUri}`); } return removed; } /** * Cleanup expired terminal resources * @returns Number of resources cleaned up */ cleanupExpiredResources(): number { const now = Date.now(); let cleaned = 0; for (const [uri, resource] of this.resources.entries()) { if (resource.metadata.isTerminal && resource.metadata.completedAt) { const age = now - resource.metadata.completedAt; if (age > this.cleanupTTL) { this.removeResource(uri); cleaned++; } } } if (cleaned > 0 && process.env.DEBUG === 'true') { console.error(`[RESOURCE] Cleaned up ${cleaned} expired resources`); } return cleaned; } /** * Subscribe to a resource (DXP-134) * @param resourceUri - Resource URI to subscribe to * @param clientId - Optional client identifier (defaults to 'default') * @returns True if subscription was added */ subscribe(resourceUri: string, clientId: string = 'default'): boolean { // Verify resource exists if (!this.resources.has(resourceUri)) { return false; } // Get or create subscriber set for this resource if (!this.subscriptions.has(resourceUri)) { this.subscriptions.set(resourceUri, new Set<string>()); } const subscribers = this.subscriptions.get(resourceUri)!; const wasNew = !subscribers.has(clientId); subscribers.add(clientId); if (wasNew && process.env.DEBUG === 'true') { console.error(`[RESOURCE] Client ${clientId} subscribed to ${resourceUri}`); } return wasNew; } /** * Unsubscribe from a resource (DXP-134) * @param resourceUri - Resource URI to unsubscribe from * @param clientId - Optional client identifier (defaults to 'default') * @returns True if subscription was removed */ unsubscribe(resourceUri: string, clientId: string = 'default'): boolean { const subscribers = this.subscriptions.get(resourceUri); if (!subscribers) { return false; } const removed = subscribers.delete(clientId); // Clean up empty subscription sets if (subscribers.size === 0) { this.subscriptions.delete(resourceUri); } if (removed && process.env.DEBUG === 'true') { console.error(`[RESOURCE] Client ${clientId} unsubscribed from ${resourceUri}`); } return removed; } /** * Get subscribers for a resource (DXP-134) * @param resourceUri - Resource URI * @returns Array of subscriber client IDs */ getSubscribers(resourceUri: string): string[] { const subscribers = this.subscriptions.get(resourceUri); return subscribers ? Array.from(subscribers) : []; } /** * Check if a resource has any subscribers (DXP-134) * @param resourceUri - Resource URI * @returns True if resource has subscribers */ hasSubscribers(resourceUri: string): boolean { const subscribers = this.subscriptions.get(resourceUri); return subscribers ? subscribers.size > 0 : false; } /** * Get statistics about managed resources * @returns Resource statistics */ getStats(): ResourceStats { const resources = Array.from(this.resources.values()); return { total: resources.length, active: resources.filter(r => !r.metadata.isTerminal).length, terminal: resources.filter(r => r.metadata.isTerminal).length, byType: { deployment: resources.filter(r => r.metadata.resourceType === 'deployment').length, export: resources.filter(r => r.metadata.resourceType === 'export').length, download: resources.filter(r => r.metadata.resourceType === 'download').length } }; } /** * Reset all resources (for testing) */ reset(): void { this.stopCleanup(); this.resources.clear(); this.subscriptions.clear(); // DXP-134: Also clear subscriptions } } // Singleton instance let globalManager: ResourceManager | null = null; /** * Get the global resource manager instance * @returns Global manager */ export function getGlobalResourceManager(): ResourceManager { if (!globalManager) { globalManager = new ResourceManager(); globalManager.startCleanup(); } return globalManager; } /** * Reset the global manager (for testing) */ export function resetGlobalResourceManager(): void { if (globalManager) { globalManager.reset(); } globalManager = null; }

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/JaxonDigital/optimizely-dxp-mcp'

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