Files
firefrost-operations-manual/docs/tasks/task-092-desktop-mcp/README.md
Claude 592dd77b07 docs: Add Buffer MCP integration to Tasks #48 and #92
Task #92 (Desktop MCP):
- Added Buffer MCP server config for Claude Desktop
- Example prompts for social media management
- Added Buffer API Key to Vaultwarden storage list
- Cross-referenced Task #48 and #97

Task #48 (n8n Rebuild):
- Created task folder with README
- Buffer MCP Client node configuration
- Automation workflow ideas (subscriber posts, server alerts, etc.)
- Priority workflows to build

Buffer MCP covers Bluesky, X, TikTok via single API.
Facebook/Instagram still require Meta Business Suite.

Chronicler #65
2026-04-07 16:32:47 +00:00

18 KiB

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


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

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)

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

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:

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

  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

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"

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

Fallback Procedures

Condition Red (Tunnel/Laptop Offline):

  • Fall back to manual SSH via Termius on mobile
  • Document in FFG-STD-005 (Emergency Operations)

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:

  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


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

Dependencies

  • Node.js (Windows)
  • npm packages: @modelcontextprotocol/sdk, ssh2, express, axios, dotenv
  • Cloudflare account with firefrostgaming.com
  • Claude Desktop app

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 #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)

Fire + Frost + Foundation = Where Love Builds Legacy 🔥❄️