#!/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);
});