Skip to main content
Glama

Super Shell MCP Server

MIT License
429
7
  • Linux
  • Apple
index.ts21.2 kB
#!/usr/bin/env node 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 * as path from 'path'; import * as fs from 'fs'; import { execFile } from 'child_process'; import { promisify } from 'util'; import { randomUUID } from 'crypto'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { CommandService, CommandSecurityLevel } from './services/command-service.js'; import { getLogger, Logger } from './utils/logger.js'; // In ESM, __dirname is not available directly, so we create it const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const execFileAsync = promisify(execFile); // Initialize the logger // Use __dirname to get the directory of the current file const LOG_FILE = path.join(__dirname, '../logs/super-shell-mcp.log'); console.error(`Log file path: ${LOG_FILE}`); const logger = getLogger(LOG_FILE, true); /** * SuperShellMcpServer - MCP server for executing shell commands across multiple platforms */ class SuperShellMcpServer { private server: Server; private commandService: CommandService; private pendingApprovals: Map<string, { command: string; args: string[] }>; constructor(options?: { shell?: string }) { // Initialize the command service with auto-detected or specified shell this.commandService = new CommandService(options?.shell); this.pendingApprovals = new Map(); // Initialize the MCP server this.server = new Server( { name: 'super-shell-mcp', version: '2.0.13', }, { capabilities: { tools: {}, }, } ); // Set up event handlers for command service this.setupCommandServiceEvents(); // Set up MCP request handlers this.setupRequestHandlers(); // Error handling this.server.onerror = (error) => { logger.error(`[MCP Error] ${error}`); console.error('[MCP Error]', error); }; process.on('SIGINT', async () => { logger.info('Received SIGINT signal, shutting down'); await this.server.close(); logger.info('Server closed, exiting process'); logger.close(); process.exit(0); }); } /** * Set up event handlers for the command service */ private setupCommandServiceEvents(): void { this.commandService.on('command:pending', (pendingCommand) => { logger.info(`[Pending Command] ID: ${pendingCommand.id}, Command: ${pendingCommand.command} ${pendingCommand.args.join(' ')}`); console.error(`[Pending Command] ID: ${pendingCommand.id}, Command: ${pendingCommand.command} ${pendingCommand.args.join(' ')}`); this.pendingApprovals.set(pendingCommand.id, { command: pendingCommand.command, args: pendingCommand.args, }); }); this.commandService.on('command:approved', (data) => { logger.info(`[Approved Command] ID: ${data.commandId}`); console.error(`[Approved Command] ID: ${data.commandId}`); this.pendingApprovals.delete(data.commandId); }); this.commandService.on('command:denied', (data) => { logger.info(`[Denied Command] ID: ${data.commandId}, Reason: ${data.reason}`); console.error(`[Denied Command] ID: ${data.commandId}, Reason: ${data.reason}`); this.pendingApprovals.delete(data.commandId); }); this.commandService.on('command:failed', (data) => { logger.error(`[Failed Command] ID: ${data.commandId}, Error: ${data.error.message}`); console.error(`[Failed Command] ID: ${data.commandId}, Error: ${data.error.message}`); this.pendingApprovals.delete(data.commandId); }); // Handle approval timeout events this.commandService.on('command:approval_timeout', (data) => { logger.error(`[Approval Timeout] ID: ${data.commandId}, Message: ${data.message}`); console.error(`[Approval Timeout] ID: ${data.commandId}, Message: ${data.message}`); // Log the timeout but keep the command in the pending queue // The AI assistant will need to use get_pending_commands and approve_command to proceed }); } /** * Set up MCP request handlers */ private setupRequestHandlers(): void { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'get_platform_info', description: 'Get information about the current platform and shell', inputSchema: { type: 'object', properties: {}, }, }, { name: 'execute_command', description: 'Execute a shell command on the current platform', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'The command to execute', }, args: { type: 'array', items: { type: 'string', }, description: 'Command arguments', }, }, required: ['command'], }, }, { name: 'get_whitelist', description: 'Get the list of whitelisted commands', inputSchema: { type: 'object', properties: {}, }, }, { name: 'add_to_whitelist', description: 'Add a command to the whitelist', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'The command to whitelist', }, securityLevel: { type: 'string', enum: ['safe', 'requires_approval', 'forbidden'], description: 'Security level for the command', }, description: { type: 'string', description: 'Description of the command', }, }, required: ['command', 'securityLevel'], }, }, { name: 'update_security_level', description: 'Update the security level of a whitelisted command', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'The command to update', }, securityLevel: { type: 'string', enum: ['safe', 'requires_approval', 'forbidden'], description: 'New security level for the command', }, }, required: ['command', 'securityLevel'], }, }, { name: 'remove_from_whitelist', description: 'Remove a command from the whitelist', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'The command to remove from whitelist', }, }, required: ['command'], }, }, { name: 'get_pending_commands', description: 'Get the list of commands pending approval', inputSchema: { type: 'object', properties: {}, }, }, { name: 'approve_command', description: 'Approve a pending command', inputSchema: { type: 'object', properties: { commandId: { type: 'string', description: 'ID of the command to approve', }, }, required: ['commandId'], }, }, { name: 'deny_command', description: 'Deny a pending command', inputSchema: { type: 'object', properties: { commandId: { type: 'string', description: 'ID of the command to deny', }, reason: { type: 'string', description: 'Reason for denial', }, }, required: ['commandId'], }, }, ], })); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'get_platform_info': return await this.handleGetPlatformInfo(); case 'execute_command': return await this.handleExecuteCommand(args); case 'get_whitelist': return await this.handleGetWhitelist(); case 'add_to_whitelist': return await this.handleAddToWhitelist(args); case 'update_security_level': return await this.handleUpdateSecurityLevel(args); case 'remove_from_whitelist': return await this.handleRemoveFromWhitelist(args); case 'get_pending_commands': return await this.handleGetPendingCommands(); case 'approve_command': return await this.handleApproveCommand(args); case 'deny_command': return await this.handleDenyCommand(args); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { if (error instanceof McpError) { throw error; } if (error instanceof Error) { return { content: [ { type: 'text', text: `Error: ${error.message}`, }, ], isError: true, }; } throw new McpError( ErrorCode.InternalError, 'An unexpected error occurred' ); } }); } /** * Handle execute_command tool */ private async handleExecuteCommand(args: any) { const schema = z.object({ command: z.string(), args: z.array(z.string()).optional(), }); // Log the start of command execution logger.debug(`handleExecuteCommand called with args: ${JSON.stringify(args)}`); const { command, args: commandArgs = [] } = schema.parse(args); // Extract the base command (without path) const baseCommand = path.basename(command); logger.debug(`[Executing Command] Command: ${command} ${commandArgs.join(' ')}`); logger.debug(`Base command: ${baseCommand}`); // Check if the command requires approval before attempting execution const whitelist = this.commandService.getWhitelist(); logger.debug(`Whitelist entries: ${whitelist.length}`); const whitelistEntry = whitelist.find(entry => entry.command === baseCommand); logger.debug(`Whitelist entry found: ${whitelistEntry ? 'yes' : 'no'}`); if (whitelistEntry) { logger.debug(`Security level: ${whitelistEntry.securityLevel}`); } if (whitelistEntry && whitelistEntry.securityLevel === CommandSecurityLevel.REQUIRES_APPROVAL) { logger.debug(`[Command Requires Approval] Command: ${command} ${commandArgs.join(' ')}`); // Use the non-blocking method to queue the command for approval const commandId = this.commandService.queueCommandForApprovalNonBlocking(command, commandArgs); logger.debug(`Command queued for approval with ID: ${commandId}`); // Return immediately with instructions for approval logger.debug(`Returning response to client`); return { content: [ { type: 'text', text: `This command requires approval. It has been queued with ID: ${commandId}\n\nPlease approve this command in the UI or use the 'approve_command' function with this command ID.`, }, ], isError: false, // Not an error, just needs approval }; } // For safe commands or forbidden commands, use the normal execution path try { // Use the CommandService's executeCommand method const result = await this.commandService.executeCommand(command, commandArgs); return { content: [ { type: 'text', text: result.stdout, }, { type: 'text', text: result.stderr ? `Error output: ${result.stderr}` : '', }, ], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; console.error(`[Command Execution Failed] Error: ${errorMessage}`); return { content: [ { type: 'text', text: `Command execution failed: ${errorMessage}`, }, ], isError: true, }; } } /** * Handle get_whitelist tool */ private async handleGetWhitelist() { const whitelist = this.commandService.getWhitelist(); return { content: [ { type: 'text', text: JSON.stringify(whitelist, null, 2), }, ], }; } /** * Handle add_to_whitelist tool */ private async handleAddToWhitelist(args: any) { const schema = z.object({ command: z.string(), securityLevel: z.enum(['safe', 'requires_approval', 'forbidden']), description: z.string().optional(), }); const { command, securityLevel, description } = schema.parse(args); // Map string security level to enum const securityLevelEnum = securityLevel === 'safe' ? CommandSecurityLevel.SAFE : securityLevel === 'requires_approval' ? CommandSecurityLevel.REQUIRES_APPROVAL : CommandSecurityLevel.FORBIDDEN; this.commandService.addToWhitelist({ command, securityLevel: securityLevelEnum, description, }); return { content: [ { type: 'text', text: `Command '${command}' added to whitelist with security level '${securityLevel}'`, }, ], }; } /** * Handle update_security_level tool */ private async handleUpdateSecurityLevel(args: any) { const schema = z.object({ command: z.string(), securityLevel: z.enum(['safe', 'requires_approval', 'forbidden']), }); const { command, securityLevel } = schema.parse(args); // Map string security level to enum const securityLevelEnum = securityLevel === 'safe' ? CommandSecurityLevel.SAFE : securityLevel === 'requires_approval' ? CommandSecurityLevel.REQUIRES_APPROVAL : CommandSecurityLevel.FORBIDDEN; this.commandService.updateSecurityLevel(command, securityLevelEnum); return { content: [ { type: 'text', text: `Security level for command '${command}' updated to '${securityLevel}'`, }, ], }; } /** * Handle remove_from_whitelist tool */ private async handleRemoveFromWhitelist(args: any) { const schema = z.object({ command: z.string(), }); const { command } = schema.parse(args); this.commandService.removeFromWhitelist(command); return { content: [ { type: 'text', text: `Command '${command}' removed from whitelist`, }, ], }; } /** * Handle get_platform_info tool */ private async handleGetPlatformInfo() { const { detectPlatform, getDefaultShell, getShellSuggestions, getCommonShellLocations } = await import('./utils/platform-utils.js'); const platform = detectPlatform(); const currentShell = this.commandService.getShell(); const suggestedShells = getShellSuggestions()[platform]; const commonLocations = getCommonShellLocations(); return { content: [ { type: 'text', text: JSON.stringify({ platform, currentShell, suggestedShells, commonLocations, helpMessage: `Super Shell MCP is running on ${platform} using ${currentShell}` }, null, 2), }, ], }; } /** * Handle get_pending_commands tool */ private async handleGetPendingCommands() { const pendingCommands = this.commandService.getPendingCommands(); return { content: [ { type: 'text', text: JSON.stringify(pendingCommands.map(cmd => ({ id: cmd.id, command: cmd.command, args: cmd.args, requestedAt: cmd.requestedAt, requestedBy: cmd.requestedBy, })), null, 2), }, ], }; } /** * Handle approve_command tool */ private async handleApproveCommand(args: any) { const schema = z.object({ commandId: z.string(), }); logger.debug(`handleApproveCommand called with args: ${JSON.stringify(args)}`); const { commandId } = schema.parse(args); // Log the approval attempt logger.debug(`[Approval Attempt] ID: ${commandId}`); // Check if the command exists in our local pending approvals map const localPending = this.pendingApprovals.has(commandId); logger.debug(`Command found in local pendingApprovals: ${localPending ? 'yes' : 'no'}`); // Check if the command exists in the CommandService's pending queue const pendingCommands = this.commandService.getPendingCommands(); logger.debug(`CommandService pending commands: ${pendingCommands.length}`); const pendingCommand = pendingCommands.find(cmd => cmd.id === commandId); logger.debug(`Command found in CommandService pending queue: ${pendingCommand ? 'yes' : 'no'}`); if (pendingCommand) { logger.debug(`Pending command details: ${JSON.stringify({ id: pendingCommand.id, command: pendingCommand.command, args: pendingCommand.args, requestedAt: pendingCommand.requestedAt })}`); } try { logger.debug(`Calling CommandService.approveCommand with ID: ${commandId}`); // Use the CommandService's approveCommand method directly const result = await this.commandService.approveCommand(commandId); logger.debug(`[Command Approved] ID: ${commandId}, Output length: ${result.stdout.length}`); logger.debug(`Command output: ${result.stdout.substring(0, 100)}${result.stdout.length > 100 ? '...' : ''}`); return { content: [ { type: 'text', text: `Command approved and executed successfully.\nOutput: ${result.stdout}`, }, { type: 'text', text: result.stderr ? `Error output: ${result.stderr}` : '', }, ], }; } catch (error) { logger.error(`[Approval Error] ID: ${commandId}, Error: ${error instanceof Error ? error.message : 'Unknown error'}`); if (error instanceof Error) { return { content: [ { type: 'text', text: `Command approval failed: ${error.message}`, }, ], isError: true, }; } throw error; } } /** * Handle deny_command tool */ private async handleDenyCommand(args: any) { const schema = z.object({ commandId: z.string(), reason: z.string().optional(), }); logger.debug(`handleDenyCommand called with args: ${JSON.stringify(args)}`); const { commandId, reason } = schema.parse(args); logger.debug(`[Denial Attempt] ID: ${commandId}, Reason: ${reason || 'none provided'}`); try { this.commandService.denyCommand(commandId, reason); logger.info(`Command denied: ID: ${commandId}, Reason: ${reason || 'none provided'}`); return { content: [ { type: 'text', text: `Command denied${reason ? `: ${reason}` : ''}`, }, ], }; } catch (error) { logger.error(`[Denial Error] ID: ${commandId}, Error: ${error instanceof Error ? error.message : 'Unknown error'}`); if (error instanceof Error) { return { content: [ { type: 'text', text: `Command denial failed: ${error.message}`, }, ], isError: true, }; } throw error; } } /** * Run the MCP server */ async run() { logger.info('Starting Super Shell MCP server'); const transport = new StdioServerTransport(); await this.server.connect(transport); logger.info('Super Shell MCP server running on stdio'); console.error('Super Shell MCP server running on stdio'); console.error(`Log file: ${LOG_FILE}`); logger.info(`Log file: ${LOG_FILE}`); } } // Create and run the server const server = new SuperShellMcpServer(); server.run().catch(console.error);

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/cfdude/super-shell-mcp'

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