Modes MCP Server

by ccc0168
Verified
#!/usr/bin/env node /** * Modes MCP Server * * This server provides tools for managing Roo's custom operational modes through the Model Context Protocol. * It handles creation, updating, deletion, and validation of mode configurations, with support for: * - Schema validation using Zod * - File system watching for config changes * - Atomic file operations * - Error handling with standard MCP error codes */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import fs from 'fs-extra'; import path from 'path'; import chokidar from 'chokidar'; /** * Schema for mode groups. Groups can be either: * 1. Simple string (e.g., "read", "edit") * 2. Tuple of [string, {fileRegex, description}] for file-specific permissions */ const GroupSchema = z.union([ z.string(), z.tuple([ z.string(), z.object({ fileRegex: z.string(), description: z.string(), }), ]), ]); /** * Schema for custom modes. Each mode must have: * - slug: Unique identifier (lowercase letters, numbers, hyphens) * - name: Display name * - roleDefinition: Detailed description of the mode's capabilities * - groups: Array of allowed tool groups * - customInstructions: Optional additional instructions */ const CustomModeSchema = z.object({ slug: z.string().regex(/^[a-z0-9-]+$/), name: z.string().min(1), roleDefinition: z.string().min(1), groups: z.array(GroupSchema), customInstructions: z.string().optional(), }); /** * Schema for the complete modes configuration file */ const CustomModesConfigSchema = z.object({ customModes: z.array(CustomModeSchema), }); class ModesServer { private server: Server; private configPath: string; private watcher: chokidar.FSWatcher | null = null; constructor() { this.server = new Server( { name: 'modes-mcp-server', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); // Default config path - can be overridden via environment variable this.configPath = process.env.MODES_CONFIG_PATH || path.join(process.env.APPDATA || '', 'Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_custom_modes.json'); // Ensure config directory exists const configDir = path.dirname(this.configPath); if (!fs.existsSync(configDir)) { fs.mkdirpSync(configDir); } this.setupToolHandlers(); this.watchConfigFile(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.cleanup(); process.exit(0); }); } /** * Clean up resources when shutting down */ private async cleanup() { if (this.watcher) { await this.watcher.close(); } await this.server.close(); } /** * Set up file system watcher for config changes */ private watchConfigFile() { this.watcher = chokidar.watch(this.configPath, { persistent: true, ignoreInitial: true, }); this.watcher.on('change', (filePath: string) => { console.error(`[MCP Modes] Config file changed: ${filePath}`); }); } /** * Read and parse the modes configuration file * @throws {McpError} If file read or parse fails */ private async readConfig() { try { // Create default config if file doesn't exist if (!fs.existsSync(this.configPath)) { await fs.writeFile(this.configPath, JSON.stringify({ customModes: [] }, null, 2), 'utf-8'); } const content = await fs.readFile(this.configPath, 'utf-8'); const config = JSON.parse(content); return CustomModesConfigSchema.parse(config); } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to read config: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Write configuration to file atomically * @param config The configuration to write * @throws {McpError} If write fails */ private async writeConfig(config: z.infer<typeof CustomModesConfigSchema>) { try { await fs.writeFile( this.configPath, JSON.stringify(config, null, 2), 'utf-8' ); } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to write config: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Set up MCP tool handlers for mode management operations */ private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'list_modes', description: 'List all custom modes', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_mode', description: 'Get details of a specific mode', inputSchema: { type: 'object', properties: { slug: { type: 'string', description: 'Slug of the mode to retrieve', }, }, required: ['slug'], }, }, { name: 'create_mode', description: 'Create a new custom mode', inputSchema: { type: 'object', properties: { slug: { type: 'string', description: 'Unique slug for the mode (lowercase letters, numbers, and hyphens)', }, name: { type: 'string', description: 'Display name for the mode', }, roleDefinition: { type: 'string', description: 'Detailed description of the mode\'s role and capabilities', }, groups: { type: 'array', items: { oneOf: [ { type: 'string' }, { type: 'array', items: [ { type: 'string' }, { type: 'object', properties: { fileRegex: { type: 'string' }, description: { type: 'string' }, }, required: ['fileRegex', 'description'], }, ], }, ], }, description: 'Array of allowed tool groups', }, customInstructions: { type: 'string', description: 'Optional additional instructions for the mode', }, }, required: ['slug', 'name', 'roleDefinition', 'groups'], }, }, { name: 'update_mode', description: 'Update an existing custom mode', inputSchema: { type: 'object', properties: { slug: { type: 'string', description: 'Slug of the mode to update', }, updates: { type: 'object', properties: { name: { type: 'string' }, roleDefinition: { type: 'string' }, groups: { type: 'array', items: { oneOf: [ { type: 'string' }, { type: 'array', items: [ { type: 'string' }, { type: 'object', properties: { fileRegex: { type: 'string' }, description: { type: 'string' }, }, required: ['fileRegex', 'description'], }, ], }, ], }, }, customInstructions: { type: 'string' }, }, }, }, required: ['slug', 'updates'], }, }, { name: 'delete_mode', description: 'Delete a custom mode', inputSchema: { type: 'object', properties: { slug: { type: 'string', description: 'Slug of the mode to delete', }, }, required: ['slug'], }, }, { name: 'validate_mode', description: 'Validate a mode configuration without saving it', inputSchema: { type: 'object', properties: { mode: { type: 'object', properties: { slug: { type: 'string' }, name: { type: 'string' }, roleDefinition: { type: 'string' }, groups: { type: 'array' }, customInstructions: { type: 'string' }, }, required: ['slug', 'name', 'roleDefinition', 'groups'], }, }, required: ['mode'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case 'list_modes': { const config = await this.readConfig(); return { content: [ { type: 'text', text: JSON.stringify(config.customModes, null, 2), }, ], }; } case 'get_mode': { const { slug } = request.params.arguments as { slug: string }; const config = await this.readConfig(); const mode = config.customModes.find((m) => m.slug === slug); if (!mode) { throw new McpError(ErrorCode.InvalidParams, `Mode not found: ${slug}`); } return { content: [ { type: 'text', text: JSON.stringify(mode, null, 2), }, ], }; } case 'create_mode': { const mode = request.params.arguments as z.infer<typeof CustomModeSchema>; const config = await this.readConfig(); if (config.customModes.some((m) => m.slug === mode.slug)) { throw new McpError( ErrorCode.InvalidParams, `Mode with slug "${mode.slug}" already exists` ); } try { CustomModeSchema.parse(mode); } catch (error) { throw new McpError( ErrorCode.InvalidParams, `Invalid mode configuration: ${error instanceof Error ? error.message : String(error)}` ); } config.customModes.push(mode); await this.writeConfig(config); return { content: [ { type: 'text', text: `Mode "${mode.name}" created successfully`, }, ], }; } case 'update_mode': { const { slug, updates } = request.params.arguments as { slug: string; updates: Partial<z.infer<typeof CustomModeSchema>>; }; const config = await this.readConfig(); const index = config.customModes.findIndex((m) => m.slug === slug); if (index === -1) { throw new McpError(ErrorCode.InvalidParams, `Mode not found: ${slug}`); } const updatedMode = { ...config.customModes[index], ...updates, }; try { CustomModeSchema.parse(updatedMode); } catch (error) { throw new McpError( ErrorCode.InvalidParams, `Invalid mode configuration: ${error instanceof Error ? error.message : String(error)}` ); } config.customModes[index] = updatedMode; await this.writeConfig(config); return { content: [ { type: 'text', text: `Mode "${updatedMode.name}" updated successfully`, }, ], }; } case 'delete_mode': { const { slug } = request.params.arguments as { slug: string }; const config = await this.readConfig(); const index = config.customModes.findIndex((m) => m.slug === slug); if (index === -1) { throw new McpError(ErrorCode.InvalidParams, `Mode not found: ${slug}`); } config.customModes.splice(index, 1); await this.writeConfig(config); return { content: [ { type: 'text', text: `Mode "${slug}" deleted successfully`, }, ], }; } case 'validate_mode': { const { mode } = request.params.arguments as { mode: z.infer<typeof CustomModeSchema>; }; try { CustomModeSchema.parse(mode); return { content: [ { type: 'text', text: 'Mode configuration is valid', }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Invalid mode configuration: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } }); } /** * Start the MCP server */ async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Modes MCP server running on stdio'); } } const server = new ModesServer(); server.run().catch(console.error);