Long-term fix for mobile task index - task numbers now in frontmatter. Numbers added from BACKLOG.md cross-reference: #2 rank-system-deployment #3 fire-frost-holdings-restructuring #14 vaultwarden-ssh-setup #22 netdata-deployment #23 department-structure #26 modpack-version-checker #32 terraria-branding-training-arc #35 pokerole-wikijs-deployment #36 notebooklm-integration #40 world-backup-automation #44 nc1-node-usage-stats #45 steam-and-state-server #48 n8n-rebuild #51 ignis-protocol #55 discord-invite-setup #65 claude-infrastructure-access #67 nc1-security-monitoring #82 plane-decommissioning #87 arbiter-2-1-cancellation-flow #89 staff-portal-consolidation #90 decap-tasks-collection #91 server-matrix-node-fix #92 desktop-mcp #93 trinity-codex #94 global-restart-scheduler #98 discord-channel-automation #99 claude-projects-architecture Chronicler #69
task_number, status, priority, owner, created
| task_number | status | priority | owner | created |
|---|---|---|---|---|
| 92 | open | P2 | Michael | 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
- Claude Desktop on always-on laptop with local MCP server
- Local MCP Server (Node.js) with SSH access to all servers
- Cloudflare Tunnel exposing a webhook listener (no router ports needed)
- Discord Ops Bot for mobile dispatch (separate from Arbiter)
- 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:
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:
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
// 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
// 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
// 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)
// 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
// 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
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
{
"mcpServers": {
"firefrost-admin": {
"command": "node",
"args": [
"C:\\Firefrost\\mcp-server\\index.js"
]
}
}
}
Step 9: Cloudflare Tunnel Setup
- Download
cloudflared-windows-amd64.msifrom Cloudflare - Install and authenticate via browser
- In Cloudflare Zero Trust dashboard, create tunnel for
ops.firefrostgaming.com - Run the provided command to install as Windows Service
- Tunnel auto-starts on boot
Step 10: Windows Power Settings
- Settings → System → Power & battery
- Set "Screen and sleep" to Never when plugged in
- 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-dispatchchannel - Trinity-only permissions
- Commands trigger n8n webhooks
task_number: 92
Testing & Validation
Test MCP Server Locally
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:
conn.exec(command, ...)
to:
conn.exec('echo DRY RUN: ' + command, ...)
Test Cloudflare Tunnel
curl -X POST https://ops.firefrostgaming.com/health \
-H "Authorization: Bearer YOUR_TOKEN"
task_number: 92
Frostwall Security Rules
- Bearer Token Authentication — Every request must include valid token
- Action Allowlist — Only predefined actions accepted, no raw bash
- Input Sanitization — Regex validation on all service/stack names
- Audit Logging — All requests logged with timestamp
- 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):
{
"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:
- Generate API key at https://publish.buffer.com/settings/api
- Add to claude_desktop_config.json as shown above
- 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 🔥❄️