mcp_server_enhanced.js•19.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 });
}