Skip to main content
Glama
nstrayer
by nstrayer
server.ts7.53 kB
#!/usr/bin/env node /** * Simple Commands MCP Server * * A configuration-driven MCP server for running developer tasks and commands. * Tools are dynamically loaded from a config file, making it easy to add new commands without code changes. */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, TextContent, CallToolResult, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { Config, ToolConfig } from './types.js'; import { logger, logSessionStart } from './logger.js'; import { ToolExecutor } from './toolExecutor.js'; import { processManager } from './processManager.js'; // Get the directory of the current module const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); class SimpleCommandsServer { private server: Server; private config: Config; private projectRoot: string; private toolExecutor: ToolExecutor; constructor() { this.server = new Server( { name: 'simple-commands-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // Set project root from environment or use current working directory this.projectRoot = process.env.MCP_PROJECT_ROOT || process.cwd(); this.config = { tools: [] }; this.toolExecutor = new ToolExecutor(this.projectRoot); // Setup handlers this.setupHandlers(); } private setupHandlers(): void { // Handle tool listing this.server.setRequestHandler(ListToolsRequestSchema, async () => { const tools: Tool[] = []; // Add configured tools for (const tool of this.config.tools) { if (tool.daemon) { // For daemons, create _start tool instead of using original name tools.push({ name: `${tool.name}_start`, description: tool.description, inputSchema: { type: 'object', properties: {}, required: [], }, }); // Auto-generate control tools for daemons tools.push({ name: `${tool.name}_status`, description: `Get status and recent output from ${tool.name}`, inputSchema: { type: 'object', properties: {}, required: [], }, }); tools.push({ name: `${tool.name}_stop`, description: `Stop the ${tool.name} daemon`, inputSchema: { type: 'object', properties: {}, required: [], }, }); tools.push({ name: `${tool.name}_logs`, description: `Get recent logs from ${tool.name} (last 50 lines)`, inputSchema: { type: 'object', properties: { lines: { type: 'number', description: 'Number of lines to retrieve (default: 50)', default: 50 } }, required: [], }, }); } else { // Non-daemon tools keep their original name tools.push({ name: tool.name, description: tool.description, inputSchema: { type: 'object', properties: {}, required: [], }, }); } } return { tools }; }); // Handle tool execution this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; logger.info(`Tool called: ${name}`); // Check if it's a _start tool for a daemon let toolConfig: ToolConfig | null = null; if (name.endsWith('_start')) { const baseName = name.replace('_start', ''); const config = this.config.tools.find(tool => tool.name === baseName && tool.daemon === true); if (config) { toolConfig = config; } } else { // Find non-daemon tool or check if it's an auto-generated control tool toolConfig = this.config.tools.find(tool => tool.name === name && !tool.daemon) || null; } // Check if it's an auto-generated control tool const isAutoGenerated = !toolConfig && ( name.endsWith('_status') || name.endsWith('_stop') || name.endsWith('_logs') || name.endsWith('_start') ); if (!toolConfig && !isAutoGenerated) { logger.warning(`Tool not found: ${name}`); throw new McpError( ErrorCode.MethodNotFound, `Tool '${name}' not found in configuration` ); } try { const result = await this.toolExecutor.executeTool(toolConfig, name, args); return { content: [ { type: 'text', text: result.output, } as TextContent, ], } as CallToolResult; } catch (error) { logger.error(`Error executing tool '${name}': ${error}`); throw new McpError( ErrorCode.InternalError, `Error executing tool: ${error instanceof Error ? error.message : String(error)}` ); } }); } async loadConfig(): Promise<void> { // Get config path from environment variable or fallback to local config.json const configPath = process.env.MCP_CONFIG_PATH || path.join(__dirname, '..', 'config.json'); try { const configData = await fs.readFile(configPath, 'utf-8'); this.config = JSON.parse(configData) as Config; logger.info(`Loaded ${this.config.tools.length} tools from ${configPath}`); } catch (error) { logger.error(`Failed to load config from ${configPath}: ${error}`); throw new Error(`Failed to load configuration from ${configPath}: ${error}`); } } async run(): Promise<void> { // Load configuration await this.loadConfig(); // Create stdio transport const transport = new StdioServerTransport(); // Set up disconnect handling transport.onclose = () => { logger.info('Client disconnected, cleaning up processes...'); processManager.cleanup(); }; transport.onerror = (error) => { logger.error(`Transport error: ${error.message}`); processManager.cleanup(); }; // Connect and run await this.server.connect(transport); logger.info(`Server running with ${this.config.tools.length} tools`); logger.info(`Project root: ${this.projectRoot}`); // Count auto-generated tools const daemonCount = this.config.tools.filter(t => t.daemon).length; if (daemonCount > 0) { logger.info(`Auto-generated ${daemonCount * 4} tools for ${daemonCount} daemon processes (_start, _status, _stop, _logs)`); } } } // Main entry point async function main() { // Initialize logging logSessionStart(); try { const server = new SimpleCommandsServer(); await server.run(); // Keep the process alive process.stdin.resume(); } catch (error) { logger.error(`Server failed to start: ${error}`); console.error('Failed to start server:', error); process.exit(1); } } // Handle graceful shutdown process.on('SIGINT', () => { logger.info('Server interrupted by user'); process.exit(0); }); process.on('SIGTERM', () => { logger.info('Server terminated'); process.exit(0); }); // Handle uncaught errors process.on('uncaughtException', (error) => { logger.error(`Uncaught exception: ${error}`); console.error('Uncaught exception:', error); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { logger.error(`Unhandled rejection at ${promise}: ${reason}`); console.error('Unhandled rejection:', reason); process.exit(1); }); // Run the server main().catch((error) => { logger.error(`Fatal error: ${error}`); console.error('Fatal 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/nstrayer/simple-commands-mcp'

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