UNPKG

@koinunopochi/danger-zone-mcp

Version:

MCP server for executing dangerous commands with safety checks

240 lines 9.79 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 { promises as fs } from 'fs'; import { exec, spawn } from 'child_process'; import { promisify } from 'util'; import * as path from 'path'; import { parse as parseJSONC } from 'jsonc-parser'; const execAsync = promisify(exec); // 設定ファイルのスキーマ定義 const ConfigSchema = z.object({ commands: z.array(z.object({ name: z.string(), description: z.string(), command: z.string(), args: z.array(z.string()).optional(), confirm: z.boolean().optional(), })).optional(), dangerZone: z.array(z.object({ name: z.string(), description: z.string(), command: z.string(), preAuthorized: z.boolean().optional().default(false), })).optional(), }); class DangerZoneMcpServer { server; config = null; constructor() { this.server = new Server({ name: 'danger-zone-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, }); this.setupHandlers(); } async loadConfig() { const localConfigFileNames = [ '.danger-zone-exec.local.jsonc', '.danger-zone-exec.local.json' ]; const globalConfigFileNames = [ '.danger-zone-exec.jsonc', '.danger-zone-exec.json' ]; try { // まずプロセスの実行ディレクトリから設定ファイルを探す const cwd = process.cwd(); for (const fileName of localConfigFileNames) { const localConfigPath = path.join(cwd, '.claude', fileName); try { const configData = await fs.readFile(localConfigPath, 'utf-8'); const rawConfig = parseJSONC(configData); this.config = ConfigSchema.parse(rawConfig); console.error(`Loaded config from: ${localConfigPath}`); return; } catch (localError) { // このファイルが見つからない場合は次を試す } } // ローカル設定が見つからない場合、ホームディレクトリを確認 const homeDir = process.env.HOME || process.env.USERPROFILE || ''; for (const fileName of globalConfigFileNames) { const globalConfigPath = path.join(homeDir, '.claude', fileName); try { const configData = await fs.readFile(globalConfigPath, 'utf-8'); const rawConfig = parseJSONC(configData); this.config = ConfigSchema.parse(rawConfig); console.error(`Loaded config from: ${globalConfigPath}`); return; } catch (globalError) { // このファイルが見つからない場合は次を試す } } console.error('No config found in project or home directory'); } catch (error) { console.error('Failed to load config:', error); } this.config = { commands: [], dangerZone: [] }; } setupHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { await this.loadConfig(); const tools = []; // 通常のコマンド if (this.config?.commands) { for (const cmd of this.config.commands) { tools.push({ name: cmd.name, description: cmd.description, inputSchema: { type: 'object', properties: { args: { type: 'array', items: { type: 'string' }, description: 'Additional arguments to pass to the command', }, }, }, }); } } // Danger Zoneコマンド if (this.config?.dangerZone) { for (const cmd of this.config.dangerZone) { tools.push({ name: cmd.name, description: `[DANGER ZONE] ${cmd.description}`, inputSchema: { type: 'object', properties: { confirm: { type: 'boolean', description: 'Must be true to execute this dangerous command', }, }, required: ['confirm'], }, }); } } return { tools }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; await this.loadConfig(); // 通常のコマンドを探す const cmd = this.config?.commands?.find(c => c.name === name); if (cmd) { try { const cmdArgs = args?.args || []; const fullCommand = [cmd.command, ...(cmd.args || []), ...cmdArgs].join(' '); const { stdout, stderr } = await execAsync(fullCommand, { cwd: process.cwd(), }); return { content: [ { type: 'text', text: stdout || stderr || 'Command executed successfully', }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error executing command: ${error.message}`, }, ], }; } } // Danger Zoneコマンドを探す const dangerCmd = this.config?.dangerZone?.find(c => c.name === name); if (dangerCmd) { if (!dangerCmd.preAuthorized) { // 事前許可がない場合のみ、インタラクティブな確認プロンプトを表示 const confirmed = await this.promptConfirmation(dangerCmd); if (!confirmed) { return { content: [ { type: 'text', text: 'Command execution cancelled by user', }, ], }; } } try { const { stdout, stderr } = await execAsync(dangerCmd.command, { cwd: process.cwd(), }); return { content: [ { type: 'text', text: stdout || stderr || 'Danger command executed successfully', }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error executing danger command: ${error.message}`, }, ], }; } } throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); }); } async promptConfirmation(cmd) { return new Promise((resolve) => { // AppleScriptを使用してmacOSのダイアログを表示 const script = ` display dialog "⚠️ DANGER ZONE ⚠️\\n\\nYou are about to execute:\\n${cmd.command}\\n\\n${cmd.description}\\n\\nAre you sure you want to continue?" ¬ buttons {"Cancel", "Execute"} ¬ default button "Cancel" ¬ cancel button "Cancel" ¬ with icon caution ¬ with title "Danger Zone Confirmation" `; const osascript = spawn('osascript', ['-e', script]); osascript.on('close', (code) => { // code 0 = Execute was clicked // code 1 = Cancel was clicked or dialog was closed resolve(code === 0); }); osascript.on('error', (err) => { console.error('Failed to show confirmation dialog:', err); resolve(false); }); }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Danger Zone MCP server running'); } } const server = new DangerZoneMcpServer(); server.run().catch(console.error); //# sourceMappingURL=index.js.map