UNPKG

otak-mcp-shell

Version:

Windows PowerShell MCP server with system directory protection and SSE/HTTP streaming support

241 lines 8.75 kB
#!/usr/bin/env node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = __importDefault(require("express")); const child_process_1 = require("child_process"); const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); const dotenv_1 = __importDefault(require("dotenv")); dotenv_1.default.config(); // デフォルトディレクトリ const DEFAULT_DIR = path_1.default.join(os_1.default.homedir(), 'Desktop', 'Otak'); // 許可されたディレクトリ(環境変数またはデフォルト) let allowedDirectory = process.env.ALLOWED_DIRECTORY ? path_1.default.resolve(expandTilde(process.env.ALLOWED_DIRECTORY)) : DEFAULT_DIR; // チルダ展開を処理する関数 function expandTilde(filepath) { if (filepath.startsWith('~/') || filepath === '~') { return filepath.replace('~', os_1.default.homedir()); } return filepath; } // Windows専用 - 保護されたディレクトリ const PROTECTED_DIRECTORIES = [ 'C:\\', 'C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)', 'C:\\Users', '~', '~/Desktop', process.env.USERPROFILE || '', process.env.USERPROFILE ? path_1.default.join(process.env.USERPROFILE, 'Desktop') : '', process.env.SYSTEMROOT || 'C:\\Windows', process.env.PROGRAMFILES || 'C:\\Program Files', process.env.PROGRAMFILES_X86 || 'C:\\Program Files (x86)' ].filter(dir => dir); // 空文字列を除外 // コマンドが保護されたディレクトリに影響しないかチェック function isCommandSafe(command) { const lowerCommand = command.toLowerCase().trim(); // 削除、移動、リネーム系のコマンドをチェック const destructivePatterns = [ /(?:remove-item|rm|del|erase)\s+.*[c-z]:\\/i, // ドライブルートの削除 /(?:remove-item|rm|del|erase)\s+.*windows/i, // Windowsディレクトリ /(?:remove-item|rm|del|erase)\s+.*program\s*files/i, // Program Files /(?:move-item|mv|move|ren|rename)\s+.*[c-z]:\\/i, // ドライブルートの移動 /(?:move-item|mv|move|ren|rename)\s+.*windows/i, // Windowsディレクトリ /(?:move-item|mv|move|ren|rename)\s+.*program\s*files/i, // Program Files ]; for (const pattern of destructivePatterns) { if (pattern.test(command)) { return false; } } // 基本的には全てのコマンドを許可(保護されたパスへの操作以外) return true; } // コマンドを実行する関数(Windows専用) function executeCommand(command, workingDir) { return new Promise((resolve) => { const startTime = Date.now(); // Windows専用 - PowerShellを使用 const shell = 'powershell.exe'; const shellArgs = ['-Command', command]; const child = (0, child_process_1.spawn)(shell, shellArgs, { cwd: workingDir, stdio: ['pipe', 'pipe', 'pipe'], env: process.env }); let stdout = ''; let stderr = ''; child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { const duration = Date.now() - startTime; resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code || 0, command, duration }); }); child.on('error', (error) => { const duration = Date.now() - startTime; resolve({ stdout: '', stderr: error.message, exitCode: 1, command, duration }); }); setTimeout(() => { child.kill('SIGTERM'); const duration = Date.now() - startTime; resolve({ stdout: stdout.trim(), stderr: 'Command timed out after 30 seconds', exitCode: 124, command, duration }); }, 30000); }); } // 初期化処理 async function initialize() { try { await promises_1.default.mkdir(allowedDirectory, { recursive: true }); console.log(`Working directory: ${allowedDirectory}`); } catch (error) { console.error('Failed to create directory:', error); } } const app = (0, express_1.default)(); app.use(express_1.default.json({ limit: '10mb' })); // CORS設定 app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (req.method === 'OPTIONS') { res.sendStatus(200); } else { next(); } }); // ヘルスチェック app.get('/health', (req, res) => { res.json({ status: 'healthy', service: 'otak-mcp-shell-http', workingDirectory: allowedDirectory.replace(/\\/g, '/'), timestamp: new Date().toISOString() }); }); // コマンド実行エンドポイント app.post('/execute', async (req, res) => { try { const { command, workingDir } = req.body; if (!command) { return res.status(400).json({ error: 'Command is required' }); } if (!isCommandSafe(command)) { return res.status(403).json({ error: 'Command not allowed for security reasons', command }); } const executeDir = workingDir ? path_1.default.resolve(allowedDirectory, workingDir) : allowedDirectory; const result = await executeCommand(command, executeDir); res.json({ command: result.command, workingDirectory: executeDir.replace(/\\/g, '/'), exitCode: result.exitCode, duration: `${result.duration}ms`, stdout: result.stdout || '(no output)', stderr: result.stderr || '(no errors)', success: result.exitCode === 0, timestamp: new Date().toISOString() }); } catch (error) { res.status(500).json({ error: error instanceof Error ? error.message : String(error), timestamp: new Date().toISOString() }); } }); // 作業ディレクトリ情報 app.get('/pwd', (req, res) => { res.json({ workingDirectory: allowedDirectory.replace(/\\/g, '/'), platform: process.platform, architecture: process.arch }); }); // 許可されたコマンドリスト app.get('/commands', (req, res) => { const commands = { file: [ 'ls -la', 'dir', 'pwd', 'cd dirname', 'mkdir dirname', 'rmdir dirname', 'touch filename', 'cat filename', 'head filename', 'tail filename', 'cp source dest', 'mv source dest', 'rm filename' ], text: [ 'grep pattern file', 'sed s/old/new/ file', 'awk {print $1} file', 'sort file', 'uniq file', 'wc file', 'echo text', 'find . -name pattern' ], system: [ 'whoami', 'date', 'uname -a', 'ps aux', 'top', 'df -h', 'du -sh', 'free -h', 'uptime', 'which command' ], network: [ 'ping hostname', 'curl url', 'wget url', 'nslookup hostname', 'dig hostname' ], dev: [ 'git status', 'git log --oneline', 'npm list', 'node --version', 'python --version', 'java -version', 'gcc --version' ] }; res.json({ description: 'Common safe commands by category', note: 'These are examples of allowed commands. Dangerous operations are blocked.', commands }); }); // サーバー起動 const PORT = process.env.PORT || 8768; async function main() { await initialize(); app.listen(PORT, () => { console.log(`Shell MCP HTTP server running on port ${PORT}`); console.log(`Working directory: ${allowedDirectory}`); console.log(`Health check: http://localhost:${PORT}/health`); console.log(`Command execution: POST http://localhost:${PORT}/execute`); }); } main().catch((error) => { console.error('Server error:', error); process.exit(1); }); //# sourceMappingURL=http-server.js.map