--- task_number: 92 status: open priority: P2 owner: Michael created: 2026-04-05 --- task_number: 92 # Task #92: Desktop MCP + Dispatch Architecture **Created:** April 5, 2026 **Created By:** Chronicler #60 + Gemini AI **Status:** READY FOR IMPLEMENTATION **Priority:** High (Accessibility accommodation) **Assignee:** Michael --- task_number: 92 ## Overview Build a local MCP server on Michael's always-on Windows laptop that enables Claude Desktop to execute SSH commands on all Firefrost servers. Include a mobile dispatch system via Discord bot + n8n + Cloudflare Tunnel. ## The Problem Michael has hand/arm limitations from reconstructive surgery. Copy-pasting commands between Claude and terminals is physically taxing. Claude's web sandbox blocks SSH (port 22) regardless of port number — it detects the protocol, not just the port. ## The Solution 1. **Claude Desktop** on always-on laptop with local MCP server 2. **Local MCP Server** (Node.js) with SSH access to all servers 3. **Cloudflare Tunnel** exposing a webhook listener (no router ports needed) 4. **Discord Ops Bot** for mobile dispatch (separate from Arbiter) 5. **n8n bridge** connecting Discord → Laptop --- task_number: 92 ## Architecture ``` MOBILE DISPATCH: Phone (Discord) → Firefrost Ops Bot → n8n (Command Center) → Cloudflare Tunnel → Express Listener (Laptop) → SSH (Destination Server) DESKTOP DIRECT: Claude Desktop (Laptop) → Local MCP Server → SSH (Destination Server) ``` --- task_number: 92 ## Infrastructure Reference | Server | IP | SSH User | |--------|-----|----------| | Command Center | 63.143.34.217 | root | | Panel VPS | 45.94.168.138 | root | | TX1 Dallas | 38.68.14.26 | root | | NC1 Charlotte | 216.239.104.130 | root | | Services VPS | 38.68.14.188 | root | | Wiki VPS | 64.50.188.14 | architect | --- task_number: 92 ## Implementation Steps ### Step 1: SSH Key Setup (Windows) Open PowerShell and generate a dedicated Ed25519 key: ```powershell ssh-keygen -t ed25519 -C "laptop-mcp-automation" -f "$HOME\.ssh\mcp_rsa" ``` Then append the public key to `~/.ssh/authorized_keys` on each server: - root@63.143.34.217 (Command Center) - root@45.94.168.138 (Panel VPS) - root@38.68.14.26 (TX1) - root@216.239.104.130 (NC1) - root@38.68.14.188 (Services VPS) - architect@64.50.188.14 (Wiki VPS) ### Step 2: MCP Server Setup Create project directory: ```powershell cd C:\Firefrost mkdir mcp-server cd mcp-server npm init -y npm install @modelcontextprotocol/sdk ssh2 dotenv express axios ``` ### Step 3: Create Config File **File: `C:\Firefrost\mcp-server\config.js`** ```javascript // config.js const os = require('os'); const path = require('path'); module.exports = { servers: { command_center: { host: '63.143.34.217', user: 'root' }, panel: { host: '45.94.168.138', user: 'root' }, tx1: { host: '38.68.14.26', user: 'root' }, nc1: { host: '216.239.104.130', user: 'root' }, services: { host: '38.68.14.188', user: 'root' }, wiki: { host: '64.50.188.14', user: 'architect' } }, sshKeyPath: path.join(os.homedir(), '.ssh', 'mcp_rsa') }; ``` ### Step 4: Create SSH Helper **File: `C:\Firefrost\mcp-server\ssh_helper.js`** ```javascript // ssh_helper.js const { Client } = require('ssh2'); const fs = require('fs'); const config = require('./config'); async function executeCommand(serverName, command) { return new Promise((resolve, reject) => { const target = config.servers[serverName]; if (!target) return reject(`Server ${serverName} not found in config.`); const conn = new Client(); conn.on('ready', () => { conn.exec(command, (err, stream) => { if (err) { conn.end(); return reject(err); } let output = ''; stream.on('close', (code, signal) => { conn.end(); resolve({ code, output }); }).on('data', (data) => { output += data; }) .stderr.on('data', (data) => { output += `ERROR: ${data}`; }); }); }).on('error', (err) => { reject(err); }).connect({ host: target.host, port: 22, username: target.user, privateKey: fs.readFileSync(config.sshKeyPath) }); }); } module.exports = { executeCommand }; ``` ### Step 5: Create MCP Server **File: `C:\Firefrost\mcp-server\index.js`** ```javascript // index.js const { Server } = require("@modelcontextprotocol/sdk/server/index.js"); const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js"); const { CallToolRequestSchema, ListToolsRequestSchema } = require("@modelcontextprotocol/sdk/types.js"); const { executeCommand } = require('./ssh_helper'); const axios = require('axios'); const server = new Server( { name: "firefrost-mcp", version: "1.0.0" }, { capabilities: { tools: {} } } ); // Tool Definitions server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "server_status", description: "Returns uptime, memory, and disk usage for a server", inputSchema: { type: "object", properties: { server: { type: "string", enum: ["command_center", "panel", "tx1", "nc1", "services", "wiki"] } }, required: ["server"] } }, { name: "restart_service", description: "Restarts a systemd service", inputSchema: { type: "object", properties: { server: { type: "string" }, service: { type: "string" } }, required: ["server", "service"] } }, { name: "docker_compose_restart", description: "Restarts a docker compose stack", inputSchema: { type: "object", properties: { server: { type: "string" }, stack: { type: "string" } }, required: ["server", "stack"] } }, { name: "git_pull", description: "Pulls latest changes on a repository", inputSchema: { type: "object", properties: { server: { type: "string" }, repo_path: { type: "string" } }, required: ["server", "repo_path"] } }, { name: "tail_logs", description: "Returns last N lines of a service log", inputSchema: { type: "object", properties: { server: { type: "string" }, service: { type: "string" }, lines: { type: "number", default: 50 } }, required: ["server", "service"] } }, { name: "pterodactyl_power", description: "Start/stop/restart a game server via Pterodactyl API", inputSchema: { type: "object", properties: { server_id: { type: "string" }, action: { type: "string", enum: ["start", "stop", "restart", "kill"] } }, required: ["server_id", "action"] } } ] })); ``` **File: `C:\Firefrost\mcp-server\index.js` (continued - execution logic)** ```javascript // Tool Execution server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { let cmd = ''; let result; if (name === "server_status") { cmd = "uptime && free -m && df -h /"; result = await executeCommand(args.server, cmd); } else if (name === "restart_service") { if (!/^[a-zA-Z0-9_-]+$/.test(args.service)) { throw new Error("Invalid service name format"); } cmd = `systemctl restart ${args.service}`; result = await executeCommand(args.server, cmd); } else if (name === "docker_compose_restart") { if (!/^[a-zA-Z0-9_-]+$/.test(args.stack)) { throw new Error("Invalid stack name format"); } cmd = `cd /opt/${args.stack} && docker compose restart`; result = await executeCommand(args.server, cmd); } else if (name === "git_pull") { const safePath = args.repo_path.replace(/[^a-zA-Z0-9_\-\/]/g, ''); cmd = `cd ${safePath} && git pull origin main`; result = await executeCommand(args.server, cmd); } else if (name === "tail_logs") { const lines = parseInt(args.lines) || 50; if (!/^[a-zA-Z0-9_-]+$/.test(args.service)) { throw new Error("Invalid service name format"); } cmd = `journalctl -u ${args.service} -n ${lines} --no-pager`; result = await executeCommand(args.server, cmd); } else if (name === "pterodactyl_power") { const pteroUrl = `https://panel.firefrostgaming.com/api/client/servers/${args.server_id}/power`; await axios.post(pteroUrl, { signal: args.action }, { headers: { 'Authorization': `Bearer ${process.env.PTERO_API_KEY}`, 'Content-Type': 'application/json', 'Accept': 'application/json' } }); return { content: [{ type: "text", text: `Power command '${args.action}' sent to server ${args.server_id}` }] }; } else { throw new Error("Unknown tool"); } return { content: [{ type: "text", text: `Exit Code: ${result.code}\nOutput:\n${result.output}` }] }; } catch (error) { return { content: [{ type: "text", text: `Error: ${error.toString()}` }], isError: true }; } }); const transport = new StdioServerTransport(); server.connect(transport).catch(console.error); ``` ### Step 6: Create Webhook Listener **File: `C:\Firefrost\mcp-server\webhook_listener.js`** ```javascript // webhook_listener.js const express = require('express'); const { executeCommand } = require('./ssh_helper'); const app = express(); app.use(express.json()); const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'CHANGE_ME_IN_PRODUCTION'; // Frostwall: Bearer Token validation app.use((req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader || authHeader !== `Bearer ${WEBHOOK_SECRET}`) { console.log('Frostwall blocked unauthorized request'); return res.status(403).json({ error: 'Frostwall blocked: Unauthorized' }); } next(); }); // Action allowlist const ALLOWED_ACTIONS = [ 'server_status', 'restart_service', 'docker_compose_restart', 'git_pull', 'tail_logs' ]; app.post('/dispatch', async (req, res) => { const { action, server, target, extra } = req.body; // Frostwall: Action allowlist if (!ALLOWED_ACTIONS.includes(action)) { return res.status(400).json({ error: 'Action not in allowlist' }); } try { let cmd = ''; if (action === 'server_status') { cmd = "uptime && free -m && df -h /"; } else if (action === 'restart_service') { if (!/^[a-zA-Z0-9_-]+$/.test(target)) { throw new Error("Invalid service name format"); } cmd = `systemctl restart ${target}`; } else if (action === 'docker_compose_restart') { if (!/^[a-zA-Z0-9_-]+$/.test(target)) { throw new Error("Invalid stack name format"); } cmd = `cd /opt/${target} && docker compose restart`; } else if (action === 'git_pull') { const safePath = target.replace(/[^a-zA-Z0-9_\-\/]/g, ''); cmd = `cd ${safePath} && git pull origin main`; } else if (action === 'tail_logs') { const lines = parseInt(extra?.lines) || 50; if (!/^[a-zA-Z0-9_-]+$/.test(target)) { throw new Error("Invalid service name format"); } cmd = `journalctl -u ${target} -n ${lines} --no-pager`; } const result = await executeCommand(server, cmd); res.json({ success: true, code: result.code, output: result.output }); } catch (error) { console.error('Dispatch error:', error); res.status(500).json({ success: false, error: error.message }); } }); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Firefrost Ops Listener running on port ${PORT}`); }); ``` ### Step 7: Environment Variables **File: `C:\Firefrost\mcp-server\.env`** ```env WEBHOOK_SECRET=your-secure-32-char-token-from-vaultwarden PTERO_API_KEY=your-pterodactyl-client-api-key DIFY_API_KEY=your-dify-api-key ``` ### Step 8: Claude Desktop Configuration **File: `%APPDATA%\Claude\claude_desktop_config.json`** ```json { "mcpServers": { "firefrost-admin": { "command": "node", "args": [ "C:\\Firefrost\\mcp-server\\index.js" ] } } } ``` ### Step 9: Cloudflare Tunnel Setup 1. Download `cloudflared-windows-amd64.msi` from Cloudflare 2. Install and authenticate via browser 3. In Cloudflare Zero Trust dashboard, create tunnel for `ops.firefrostgaming.com` 4. Run the provided command to install as Windows Service 5. Tunnel auto-starts on boot ### Step 10: Windows Power Settings 1. Settings → System → Power & battery 2. Set "Screen and sleep" to **Never** when plugged in 3. Optional: Install Microsoft PowerToys → Enable **Awake** tool ### Step 11: Discord Ops Bot (Separate from Arbiter) Create a new Discord bot for server operations: - Restrict to hidden `#ops-dispatch` channel - Trinity-only permissions - Commands trigger n8n webhooks --- task_number: 92 ## Testing & Validation ### Test MCP Server Locally ```powershell npx @modelcontextprotocol/inspector node index.js ``` This opens a web UI to test tools before connecting Claude. ### SSH Dry Run Mode In `ssh_helper.js`, temporarily change: ```javascript conn.exec(command, ...) ``` to: ```javascript conn.exec('echo DRY RUN: ' + command, ...) ``` ### Test Cloudflare Tunnel ```bash curl -X POST https://ops.firefrostgaming.com/health \ -H "Authorization: Bearer YOUR_TOKEN" ``` --- task_number: 92 ## Frostwall Security Rules 1. **Bearer Token Authentication** — Every request must include valid token 2. **Action Allowlist** — Only predefined actions accepted, no raw bash 3. **Input Sanitization** — Regex validation on all service/stack names 4. **Audit Logging** — All requests logged with timestamp 5. **Separate Bot** — Ops commands isolated from public Arbiter bot --- task_number: 92 ## Fallback Procedures **Condition Red (Tunnel/Laptop Offline):** - Fall back to manual SSH via Termius on mobile - Document in FFG-STD-005 (Emergency Operations) --- task_number: 92 ## Additional MCP Servers Claude Desktop supports multiple MCP servers simultaneously. When Task #92 is complete, consider adding these: ### Buffer MCP (Social Media) Buffer provides an official MCP server for managing social media posts via natural language. **Covers:** Bluesky, X (Twitter), TikTok **Does NOT cover:** Facebook, Instagram (use Meta Business Suite) **Configuration (add to claude_desktop_config.json):** ```json { "mcpServers": { "firefrost": { "command": "node", "args": ["C:\\Firefrost\\mcp-server\\index.js"] }, "buffer": { "command": "cmd", "args": [ "/c", "npx", "-y", "mcp-remote", "https://mcp.buffer.com/mcp", "--header", "Authorization: Bearer YOUR_BUFFER_API_KEY" ] } } } ``` **Example prompts:** - "Show me all my scheduled Buffer posts for this week" - "Create a draft post for X that says 'Server maintenance complete!'" - "List my Buffer channels" **Setup:** 1. Generate API key at https://publish.buffer.com/settings/api 2. Add to claude_desktop_config.json as shown above 3. Restart Claude Desktop **Documentation:** https://developers.buffer.com/guides/getting-started.html --- task_number: 92 ## Vaultwarden Storage Create folder: **Firefrost Ops Infrastructure** | Item | Type | Notes | |------|------|-------| | Laptop MCP Ed25519 Key | Secure Note | Private key text | | Ops Webhook Bearer Token | Password | Random 32-char string | | Cloudflare Tunnel Secret | Password | From Zero Trust dashboard | | Pterodactyl Client API Key | Password | From panel settings | | Buffer API Key | Password | From Buffer developer settings | --- task_number: 92 ## Dependencies - Node.js (Windows) - npm packages: `@modelcontextprotocol/sdk`, `ssh2`, `express`, `axios`, `dotenv` - Cloudflare account with firefrostgaming.com - Claude Desktop app --- task_number: 92 ## Files Created | File | Purpose | |------|---------| | `config.js` | Server definitions | | `ssh_helper.js` | SSH execution wrapper | | `index.js` | MCP server with tool definitions | | `webhook_listener.js` | Express app for mobile dispatch | | `.env` | Environment variables | --- task_number: 92 ## Related Tasks - Task #48: n8n Rebuild (Buffer MCP integration for automated posting) - Task #93: Trinity Codex (shared knowledge base) - Task #97: Trinity Console Social Hub (parked — use MCP approach instead) --- task_number: 92 **Fire + Frost + Foundation = Where Love Builds Legacy** 🔥❄️