Skip to main content
Glama
mcp_server_enhanced.js19.2 kB
/** * Enhanced MCP Server with Advanced Resource Management */ import { MCPServer, Tool, } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { WebSocketServerTransport } from "@modelcontextprotocol/sdk/server/websocket.js"; import dotenv from "dotenv"; import express from "express"; import { WebSocketServer } from "ws"; // Import services import ghostService from "./services/ghostServiceImproved.js"; import { ResourceManager } from "./resources/ResourceManager.js"; import { ErrorHandler, ValidationError } from "./errors/index.js"; import { errorLogger, mcpCors, RateLimiter, healthCheck, GracefulShutdown } from "./middleware/errorMiddleware.js"; dotenv.config(); console.log("Initializing Enhanced MCP Server..."); // Initialize components const resourceManager = new ResourceManager(ghostService); const rateLimiter = new RateLimiter(); const gracefulShutdown = new GracefulShutdown(); // Create MCP Server const mcpServer = new MCPServer({ metadata: { name: "Ghost CMS Manager", description: "Enhanced MCP Server for Ghost CMS with advanced resource management", version: "2.0.0", capabilities: { resources: true, tools: true, subscriptions: true, batch: true } }, }); // --- Register Resources with Enhanced Fetching --- console.log("Registering enhanced resources..."); // Ghost Post Resource const postResource = resourceManager.registerResource( "ghost/post", { type: "object", properties: { id: { type: "string" }, uuid: { type: "string" }, title: { type: "string" }, slug: { type: "string" }, html: { type: ["string", "null"] }, status: { type: "string", enum: ["draft", "published", "scheduled"] }, feature_image: { type: ["string", "null"] }, published_at: { type: ["string", "null"], format: "date-time" }, tags: { type: "array", items: { $ref: "ghost/tag#/schema" } }, meta_title: { type: ["string", "null"] }, meta_description: { type: ["string", "null"] } }, required: ["id", "uuid", "title", "slug", "status"] }, { description: "Ghost blog post with support for multiple identifier types", examples: [ "ghost/post/123", "ghost/post/slug:my-awesome-post", "ghost/post/uuid:550e8400-e29b-41d4-a716-446655440000", "ghost/posts?status=published&limit=10&page=1" ] } ); mcpServer.addResource(postResource); // Ghost Tag Resource const tagResource = resourceManager.registerResource( "ghost/tag", { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, slug: { type: "string" }, description: { type: ["string", "null"] }, feature_image: { type: ["string", "null"] }, visibility: { type: "string", enum: ["public", "internal"] }, meta_title: { type: ["string", "null"] }, meta_description: { type: ["string", "null"] } }, required: ["id", "name", "slug"] }, { description: "Ghost tag for categorizing posts", examples: [ "ghost/tag/technology", "ghost/tag/slug:web-development", "ghost/tag/name:JavaScript", "ghost/tags?limit=20" ] } ); mcpServer.addResource(tagResource); // --- Enhanced Tools --- console.log("Registering enhanced tools..."); // Batch Operations Tool const batchOperationsTool = new Tool({ name: "ghost_batch_operations", description: "Execute multiple Ghost operations in a single request", inputSchema: { type: "object", properties: { operations: { type: "array", items: { type: "object", properties: { id: { type: "string", description: "Operation ID for reference" }, type: { type: "string", enum: ["create_post", "update_post", "create_tag", "fetch_resource"] }, data: { type: "object", description: "Operation-specific data" } }, required: ["id", "type", "data"] }, minItems: 1, maxItems: 10 }, stopOnError: { type: "boolean", default: false, description: "Stop processing on first error" } }, required: ["operations"] }, outputSchema: { type: "object", properties: { results: { type: "object", additionalProperties: { type: "object", properties: { success: { type: "boolean" }, data: { type: "object" }, error: { type: "object" } } } } } }, implementation: async (input) => { const results = {}; for (const operation of input.operations) { try { let result; switch (operation.type) { case 'create_post': result = await ghostService.createPost(operation.data); break; case 'update_post': result = await ghostService.updatePost( operation.data.id, operation.data ); break; case 'create_tag': result = await ghostService.createTag(operation.data); break; case 'fetch_resource': result = await resourceManager.fetchResource(operation.data.uri); break; default: throw new ValidationError(`Unknown operation type: ${operation.type}`); } results[operation.id] = { success: true, data: result }; } catch (error) { results[operation.id] = { success: false, error: ErrorHandler.formatMCPError(error) }; if (input.stopOnError) { break; } } } return { results }; } }); mcpServer.addTool(batchOperationsTool); // Resource Search Tool const searchResourcesTool = new Tool({ name: "ghost_search_resources", description: "Search for Ghost resources with advanced filtering", inputSchema: { type: "object", properties: { resourceType: { type: "string", enum: ["posts", "tags"], description: "Type of resource to search" }, query: { type: "string", description: "Search query" }, filters: { type: "object", properties: { status: { type: "string" }, visibility: { type: "string" }, tag: { type: "string" }, author: { type: "string" }, published_after: { type: "string", format: "date-time" }, published_before: { type: "string", format: "date-time" } } }, sort: { type: "string", default: "published_at desc", description: "Sort order" }, limit: { type: "integer", minimum: 1, maximum: 100, default: 15 }, page: { type: "integer", minimum: 1, default: 1 } }, required: ["resourceType"] }, implementation: async (input) => { const { resourceType, query, filters = {}, sort, limit, page } = input; // Build Ghost filter string let filterParts = []; if (query) { filterParts.push(`title:~'${query}'`); } if (filters.status) { filterParts.push(`status:${filters.status}`); } if (filters.visibility) { filterParts.push(`visibility:${filters.visibility}`); } if (filters.tag) { filterParts.push(`tag:${filters.tag}`); } if (filters.author) { filterParts.push(`author:${filters.author}`); } if (filters.published_after) { filterParts.push(`published_at:>='${filters.published_after}'`); } if (filters.published_before) { filterParts.push(`published_at:<='${filters.published_before}'`); } const ghostFilter = filterParts.join('+'); // Build resource URI const uri = `ghost/${resourceType}?${new URLSearchParams({ filter: ghostFilter, order: sort, limit, page }).toString()}`; // Fetch using ResourceManager return await resourceManager.fetchResource(uri); } }); mcpServer.addTool(searchResourcesTool); // Cache Management Tool const cacheManagementTool = new Tool({ name: "ghost_cache_management", description: "Manage resource cache", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["invalidate", "stats", "prefetch"], description: "Cache action to perform" }, pattern: { type: "string", description: "Pattern for invalidation (optional)" }, uris: { type: "array", items: { type: "string" }, description: "URIs to prefetch (for prefetch action)" } }, required: ["action"] }, implementation: async (input) => { switch (input.action) { case 'invalidate': resourceManager.invalidateCache(input.pattern); return { success: true, message: `Cache invalidated${input.pattern ? ` for pattern: ${input.pattern}` : ''}` }; case 'stats': return resourceManager.getCacheStats(); case 'prefetch': if (!input.uris || input.uris.length === 0) { throw new ValidationError('URIs required for prefetch action'); } return await resourceManager.prefetch(input.uris); default: throw new ValidationError(`Unknown action: ${input.action}`); } } }); mcpServer.addTool(cacheManagementTool); // Resource Subscription Tool const subscriptionTool = new Tool({ name: "ghost_subscribe", description: "Subscribe to resource changes", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["subscribe", "unsubscribe"], description: "Subscription action" }, uri: { type: "string", description: "Resource URI to subscribe to (required for subscribe action)" }, subscriptionId: { type: "string", description: "Subscription ID (required for unsubscribe action)" }, options: { type: "object", properties: { pollingInterval: { type: "integer", minimum: 5000, default: 30000, description: "Polling interval in milliseconds" }, enablePolling: { type: "boolean", default: false, description: "Enable automatic polling" } } } }, required: ["action"], // Add conditional validation using JSON Schema if/then/else if: { properties: { action: { const: "subscribe" } } }, then: { required: ["uri"] }, else: { if: { properties: { action: { const: "unsubscribe" } } }, then: { required: ["subscriptionId"] } } }, implementation: async (input) => { if (input.action === 'subscribe') { if (!input.uri) { throw new ValidationError('URI required for subscribe action'); } const subscriptionId = resourceManager.subscribe( input.uri, (event) => { // In a real implementation, this would send events to the client console.log('Resource update:', event); }, input.options || {} ); return { success: true, subscriptionId, message: `Subscribed to ${input.uri}` }; } else if (input.action === 'unsubscribe') { if (!input.subscriptionId) { throw new ValidationError('Subscription ID required for unsubscribe action'); } resourceManager.unsubscribe(input.subscriptionId); return { success: true, message: `Unsubscribed from ${input.subscriptionId}` }; } } }); mcpServer.addTool(subscriptionTool); // Keep existing tools (create_post, upload_image, etc.) from previous implementation // ... (include the tools from mcp_server_improved.js) // --- Enhanced Transport with Middleware --- const startEnhancedMCPServer = async (transport = 'http', options = {}) => { try { console.log(`Starting Enhanced MCP Server with ${transport} transport...`); switch (transport) { case 'stdio': const stdioTransport = new StdioServerTransport(); await mcpServer.connect(stdioTransport); console.log("Enhanced MCP Server running on stdio transport"); break; case 'http': case 'sse': const port = options.port || 3001; const app = express(); // Apply middleware app.use(gracefulShutdown.middleware()); app.use(rateLimiter.middleware()); app.use(mcpCors(options.allowedOrigins)); // Health check with Ghost status app.get('/health', healthCheck(ghostService)); // Resource endpoints app.get('/resources', async (req, res) => { try { const resources = resourceManager.listResources(req.query); res.json(resources); } catch (error) { const formatted = ErrorHandler.formatHTTPError(error); res.status(formatted.statusCode).json(formatted.body); } }); app.get('/resources/*', async (req, res) => { try { const uri = req.params[0]; // Validate and sanitize the URI to prevent path traversal if (!uri || typeof uri !== 'string') { throw new ValidationError('Invalid resource URI'); } // Ensure the URI doesn't contain path traversal attempts if (uri.includes('..') || uri.includes('//') || uri.includes('\\')) { throw new ValidationError('Invalid resource URI: path traversal detected'); } // Only allow specific URI patterns for Ghost resources const validPatterns = /^ghost\/(post|posts|tag|tags|author|authors|page|pages)/; if (!validPatterns.test(uri)) { throw new ValidationError('Invalid resource type'); } const result = await resourceManager.fetchResource(uri); res.json(result); } catch (error) { const formatted = ErrorHandler.formatHTTPError(error); res.status(formatted.statusCode).json(formatted.body); } }); // Batch endpoint app.post('/batch', async (req, res) => { try { const result = await resourceManager.batchFetch(req.body.uris); res.json(result); } catch (error) { const formatted = ErrorHandler.formatHTTPError(error); res.status(formatted.statusCode).json(formatted.body); } }); // Cache stats endpoint app.get('/cache/stats', (req, res) => { res.json(resourceManager.getCacheStats()); }); // SSE endpoint for MCP const sseTransport = new SSEServerTransport(); app.get('/mcp/sse', sseTransport.handler()); await mcpServer.connect(sseTransport); const server = app.listen(port, () => { console.log(`Enhanced MCP Server (SSE) listening on port ${port}`); console.log(`Health: http://localhost:${port}/health`); console.log(`Resources: http://localhost:${port}/resources`); console.log(`SSE: http://localhost:${port}/mcp/sse`); }); mcpServer._httpServer = server; // Track connections for graceful shutdown server.on('connection', (connection) => { gracefulShutdown.trackConnection(connection); }); break; case 'websocket': const wsPort = options.port || 3001; const wss = new WebSocketServer({ port: wsPort }); wss.on('connection', async (ws) => { console.log('New WebSocket connection'); const wsTransport = new WebSocketServerTransport(ws); await mcpServer.connect(wsTransport); // Handle subscriptions over WebSocket ws.on('message', async (data) => { try { const message = JSON.parse(data); if (message.type === 'subscribe') { const subscriptionId = resourceManager.subscribe( message.uri, (event) => { ws.send(JSON.stringify({ type: 'subscription_update', ...event })); }, message.options || {} ); ws.send(JSON.stringify({ type: 'subscription_created', subscriptionId })); } } catch (error) { ws.send(JSON.stringify({ type: 'error', error: error.message })); } }); }); console.log(`Enhanced MCP Server (WebSocket) listening on port ${wsPort}`); mcpServer._wss = wss; break; default: throw new Error(`Unknown transport type: ${transport}`); } // Log capabilities console.log("Server Capabilities:"); console.log("- Resources:", mcpServer.listResources().map(r => r.name)); console.log("- Tools:", mcpServer.listTools().map(t => t.name)); console.log("- Cache enabled with LRU eviction"); console.log("- Subscription support for real-time updates"); console.log("- Batch operations for efficiency"); } catch (error) { errorLogger.logError(error); console.error("Failed to start Enhanced MCP Server:", error); process.exit(1); } }; // Graceful shutdown const shutdown = async () => { console.log("\nShutting down Enhanced MCP Server..."); // Clear all subscriptions resourceManager.subscriptionManager.subscriptions.clear(); // Close servers if (mcpServer._httpServer) { await gracefulShutdown.shutdown(mcpServer._httpServer); } if (mcpServer._wss) { mcpServer._wss.close(); } await mcpServer.close(); process.exit(0); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); // Export export { mcpServer, startEnhancedMCPServer, resourceManager }; // If running directly if (import.meta.url === `file://${process.argv[1]}`) { const transport = process.env.MCP_TRANSPORT || 'http'; const port = parseInt(process.env.MCP_PORT || '3001'); const allowedOrigins = process.env.MCP_ALLOWED_ORIGINS?.split(',') || ['*']; startEnhancedMCPServer(transport, { port, allowedOrigins }); }

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/jgardner04/Ghost-MCP-Server'

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