Skip to main content
Glama
index.ts7.49 kB
/** * RPG-MCP Server - Dynamic Loader Pattern Implementation * * Token reduction: ~50K → ~6-8K (85%+ reduction) * * Meta-tools (search_tools, load_tool_schema) enable: * - Tool discovery by keyword/category * - On-demand schema loading */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; // Meta-tools and registry import { MetaTools, handleSearchTools, handleLoadToolSchema } from './meta-tools.js'; import { buildToolRegistry } from './tool-registry.js'; // MINIMAL_SCHEMA removed - must pass actual schema for MCP SDK to pass arguments // PubSub and utilities import { PubSub } from '../engine/pubsub.js'; import { registerEventTools } from './events.js'; import { AuditLogger } from './audit.js'; import { withSession } from './types.js'; import { closeDb, getDbPath } from '../storage/index.js'; // PubSub setters (needed for world/combat tools) import { setWorldPubSub } from './tools.js'; import { setCombatPubSub } from './combat-tools.js'; /** * Setup graceful shutdown handlers to ensure database is properly closed. */ function setupShutdownHandlers(): void { let isShuttingDown = false; const shutdown = (signal: string) => { if (isShuttingDown) return; isShuttingDown = true; console.error(`[Server] Received ${signal}, shutting down gracefully...`); try { closeDb(); console.error('[Server] Shutdown complete'); process.exit(0); } catch (e) { console.error('[Server] Error during shutdown:', (e as Error).message); process.exit(1); } }; process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGHUP', () => shutdown('SIGHUP')); if (process.platform === 'win32') { process.on('SIGBREAK', () => shutdown('SIGBREAK')); } process.on('uncaughtException', (error) => { console.error('[Server] Uncaught exception:', error); shutdown('uncaughtException'); }); process.on('unhandledRejection', (reason) => { console.error('[Server] Unhandled rejection:', reason); shutdown('unhandledRejection'); }); process.on('exit', (code) => { if (!isShuttingDown) { console.error(`[Server] Process exiting with code ${code}`); closeDb(); } }); } async function main() { setupShutdownHandlers(); console.error(`[Server] Database path: ${getDbPath()}`); const server = new McpServer({ name: 'rpg-mcp', version: '1.1.0' }); // Initialize PubSub const pubsub = new PubSub(); setCombatPubSub(pubsub); setWorldPubSub(pubsub); // Register Event Tools registerEventTools(server, pubsub); // Initialize AuditLogger const auditLogger = new AuditLogger(); // ========================================================================= // META-TOOLS: Register with FULL schemas (they're the discovery mechanism) // ========================================================================= server.tool( MetaTools.SEARCH_TOOLS.name, MetaTools.SEARCH_TOOLS.description, MetaTools.SEARCH_TOOLS.inputSchema.extend({ sessionId: z.string().optional() }).shape, auditLogger.wrapHandler(MetaTools.SEARCH_TOOLS.name, withSession(MetaTools.SEARCH_TOOLS.inputSchema, handleSearchTools)) ); server.tool( MetaTools.LOAD_TOOL_SCHEMA.name, MetaTools.LOAD_TOOL_SCHEMA.description, MetaTools.LOAD_TOOL_SCHEMA.inputSchema.extend({ sessionId: z.string().optional() }).shape, auditLogger.wrapHandler(MetaTools.LOAD_TOOL_SCHEMA.name, withSession(MetaTools.LOAD_TOOL_SCHEMA.inputSchema, handleLoadToolSchema)) ); // ========================================================================= // ALL OTHER TOOLS: Register with MINIMAL schemas (85%+ token reduction) // ========================================================================= const registry = buildToolRegistry(); const toolCount = Object.keys(registry).length; const sessionIdSchema = z.object({ sessionId: z.string().optional() }); for (const [toolName, entry] of Object.entries(registry)) { // Handle all Zod schema types (object, omit, pick, etc.) // .extend() only works on z.object(), so we use .and() which works universally let extendedSchema: any; if (typeof entry.schema.extend === 'function') { // Standard z.object() - use .extend() for best performance extendedSchema = entry.schema.extend({ sessionId: z.string().optional() }); } else if (typeof entry.schema.and === 'function') { // .omit(), .pick(), or other transformed schemas - use .and() extendedSchema = entry.schema.and(sessionIdSchema); } else { // Fallback: wrap in intersection extendedSchema = z.intersection(entry.schema, sessionIdSchema); } server.tool( toolName, entry.metadata.description, extendedSchema.shape || extendedSchema._def?.schema?.shape || {}, auditLogger.wrapHandler( toolName, withSession(entry.schema, entry.handler as any) ) ); } console.error(`[Server] Registered ${toolCount} tools with minimal schemas`); console.error(`[Server] Meta-tools: search_tools, load_tool_schema`); // ========================================================================= // TRANSPORT SETUP // ========================================================================= const args = process.argv.slice(2); const transportType = args.includes('--tcp') ? 'tcp' : (args.includes('--unix') || args.includes('--socket')) ? 'unix' : (args.includes('--ws') || args.includes('--websocket')) ? 'websocket' : 'stdio'; if (transportType === 'tcp') { const { TCPServerTransport } = await import('./transport/tcp.js'); const portIndex = args.indexOf('--port'); const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 3000; const transport = new TCPServerTransport(port); await server.connect(transport); console.error(`RPG MCP Server running on TCP port ${port}`); } else if (transportType === 'unix') { const { UnixServerTransport } = await import('./transport/unix.js'); let socketPath = ''; const unixIndex = args.indexOf('--unix'); const socketIndex = args.indexOf('--socket'); if (unixIndex !== -1 && args[unixIndex + 1]) { socketPath = args[unixIndex + 1]; } else if (socketIndex !== -1 && args[socketIndex + 1]) { socketPath = args[socketIndex + 1]; } if (!socketPath) { socketPath = process.platform === 'win32' ? '\\\\.\\pipe\\rpg-mcp' : '/tmp/rpg-mcp.sock'; } const transport = new UnixServerTransport(socketPath); await server.connect(transport); console.error(`RPG MCP Server running on Unix socket ${socketPath}`); } else if (transportType === 'websocket') { const { WebSocketServerTransport } = await import('./transport/websocket.js'); const portIndex = args.indexOf('--port'); const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : 3001; const transport = new WebSocketServerTransport(port); await server.connect(transport); console.error(`RPG MCP Server running on WebSocket port ${port}`); } else { const transport = new StdioServerTransport(); await server.connect(transport); console.error('RPG MCP Server running on stdio'); } } main().catch((error) => { console.error('Server error:', error); process.exit(1); });

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/Mnehmos/rpg-mcp'

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