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
605 lines
18 KiB
Markdown
605 lines
18 KiB
Markdown
# 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:
|
|
|
|
```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
|
|
|
|
---
|
|
|
|
## 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"
|
|
```
|
|
|
|
---
|
|
|
|
## 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):**
|
|
|
|
```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 |
|
|
|
|
---
|
|
|
|
## 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)
|
|
|
|
---
|
|
|
|
**Fire + Frost + Foundation = Where Love Builds Legacy** 🔥❄️
|