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);