Files
firefrost-operations-manual/docs/tasks/task-109-mcp-logging
Claude f184c514f0 Add Trinity Core tasks and Gemini MCP consultation
Tasks Added:
- Task #109: MCP Logging in Trinity Console (full spec)
- Task #110: Uptime Kuma monitor cleanup
- Task #111: Claude Desktop MCP integration

Consultations:
- gemini-mcp-connector-2026-04-11.md - Full MCP protocol guidance
- gemini-social-api-strategy-2026-04-10.md - Social sync strategy

Key insights from Gemini:
- Claude.ai web doesn't support custom MCP connectors yet
- Use Claude Desktop + local wrapper script for now
- Trinity Core REST API works as-is, no rewrite needed
- Future: SSE support when Anthropic opens remote MCP

Chronicler #76
2026-04-11 07:28:48 +00:00
..

Task #109: Trinity Core MCP Logging in Trinity Console

Status: Planned
Priority: P2-Medium
Owner: Michael
Created: April 11, 2026 by Chronicler #76
Estimated Hours: 3-4


Overview

Add MCP command logging to Trinity Console instead of Discord notifications. All commands executed through Trinity Core will be logged to Arbiter's PostgreSQL database and viewable in a new Trinity Console page with filtering.


Architecture

Trinity Core (Pi) 
    ↓ POST /api/internal/mcp/log
Arbiter (Command Center)
    ↓ INSERT
PostgreSQL (mcp_logs table)
    ↓ SELECT
Trinity Console /admin/mcp-logs

Implementation

1. Database Schema

Run on Command Center:

CREATE TABLE mcp_logs (
    id SERIAL PRIMARY KEY,
    server VARCHAR(50) NOT NULL,
    command TEXT NOT NULL,
    success BOOLEAN NOT NULL,
    stdout TEXT,
    stderr TEXT,
    error TEXT,
    executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    execution_time_ms INTEGER
);

CREATE INDEX idx_mcp_logs_server ON mcp_logs(server);
CREATE INDEX idx_mcp_logs_executed_at ON mcp_logs(executed_at DESC);
CREATE INDEX idx_mcp_logs_success ON mcp_logs(success);

2. Arbiter Internal API Endpoint

Add to /home/claude/firefrost-services/services/arbiter-3.0/src/routes/api.js:

// MCP Log endpoint
router.post('/internal/mcp/log', authenticateInternal, async (req, res) => {
    try {
        const { server, command, success, stdout, stderr, error, execution_time_ms } = req.body;
        
        if (!server || !command || success === undefined) {
            return res.status(400).json({ error: 'Missing required fields' });
        }
        
        const result = await pool.query(
            `INSERT INTO mcp_logs (server, command, success, stdout, stderr, error, execution_time_ms)
             VALUES ($1, $2, $3, $4, $5, $6, $7)
             RETURNING id`,
            [server, command, success, stdout || '', stderr || '', error || null, execution_time_ms || null]
        );
        
        res.json({ success: true, id: result.rows[0].id });
    } catch (err) {
        console.error('MCP log error:', err);
        res.status(500).json({ error: 'Failed to log command' });
    }
});

// MCP Logs list endpoint (for Trinity Console)
router.get('/internal/mcp/logs', authenticateInternal, async (req, res) => {
    try {
        const { server, success, limit = 100, offset = 0 } = req.query;
        
        let query = 'SELECT * FROM mcp_logs WHERE 1=1';
        const params = [];
        let paramCount = 0;
        
        if (server) {
            paramCount++;
            query += ` AND server = $${paramCount}`;
            params.push(server);
        }
        
        if (success !== undefined) {
            paramCount++;
            query += ` AND success = $${paramCount}`;
            params.push(success === 'true');
        }
        
        query += ' ORDER BY executed_at DESC';
        
        paramCount++;
        query += ` LIMIT $${paramCount}`;
        params.push(parseInt(limit));
        
        paramCount++;
        query += ` OFFSET $${paramCount}`;
        params.push(parseInt(offset));
        
        const result = await pool.query(query, params);
        
        // Get total count for pagination
        let countQuery = 'SELECT COUNT(*) FROM mcp_logs WHERE 1=1';
        const countParams = [];
        let countParamNum = 0;
        
        if (server) {
            countParamNum++;
            countQuery += ` AND server = $${countParamNum}`;
            countParams.push(server);
        }
        
        if (success !== undefined) {
            countParamNum++;
            countQuery += ` AND success = $${countParamNum}`;
            countParams.push(success === 'true');
        }
        
        const countResult = await pool.query(countQuery, countParams);
        
        res.json({
            logs: result.rows,
            total: parseInt(countResult.rows[0].count),
            limit: parseInt(limit),
            offset: parseInt(offset)
        });
    } catch (err) {
        console.error('MCP logs fetch error:', err);
        res.status(500).json({ error: 'Failed to fetch logs' });
    }
});

3. Update Trinity Core MCP Server

Replace /home/claude_executor/mcp-server/index.js on the Pi:

const express = require('express');
const { exec } = require('child_process');
const fs = require('fs');
const app = express();

app.use(express.json());

const API_TOKEN = 'FFG-Trinity-2026-Core-Access';
const LOG_FILE = '/home/claude_executor/mcp-server/command.log';
const ARBITER_URL = 'https://discord-bot.firefrostgaming.com/api/internal/mcp/log';
const ARBITER_TOKEN = '6fYF1akCRW6pM2F8n3S3RxeIod4YgRniUJNEQurvBP4=';

const SERVERS = {
  'command-center': { host: '63.143.34.217', user: 'root' },
  'tx1-dallas': { host: '38.68.14.26', user: 'root' },
  'nc1-charlotte': { host: '216.239.104.130', user: 'root' },
  'panel-vps': { host: '45.94.168.138', user: 'root' },
  'dev-panel': { host: '64.50.188.128', user: 'root' },
  'wiki-vps': { host: '64.50.188.14', user: 'architect' },
  'services-vps': { host: '38.68.14.188', user: 'root' }
};

function log(msg) {
  const line = `[${new Date().toISOString()}] ${msg}\n`;
  fs.appendFileSync(LOG_FILE, line);
  console.log(line.trim());
}

async function logToArbiter(data) {
  try {
    await fetch(ARBITER_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Internal-Token': ARBITER_TOKEN
      },
      body: JSON.stringify(data)
    });
  } catch (err) {
    log(`Failed to log to Arbiter: ${err.message}`);
  }
}

function auth(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (token !== API_TOKEN) {
    log(`AUTH FAILED from ${req.ip}`);
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
}

app.get('/', (req, res) => {
  res.json({ status: 'Trinity Core Online', timestamp: new Date().toISOString() });
});

app.get('/servers', auth, (req, res) => {
  res.json({ servers: Object.entries(SERVERS).map(([name, info]) => ({ name, ...info })) });
});

app.post('/exec', auth, (req, res) => {
  const { command, server } = req.body;
  
  if (!command || !server) {
    return res.status(400).json({ error: 'Missing command or server' });
  }
  
  const target = SERVERS[server];
  if (!target) {
    return res.status(400).json({ error: `Unknown server: ${server}` });
  }
  
  log(`EXEC [${server}] ${command}`);
  
  const sshCmd = `ssh -o ConnectTimeout=10 ${target.user}@${target.host} "${command.replace(/"/g, '\\"')}"`;
  const startTime = Date.now();
  
  exec(sshCmd, { timeout: 30000 }, async (error, stdout, stderr) => {
    const success = !error;
    const executionTime = Date.now() - startTime;
    
    log(`RESULT [${server}] success=${success} time=${executionTime}ms`);
    
    // Log to Arbiter (async, don't block response)
    logToArbiter({
      server,
      command,
      success,
      stdout: stdout.trim(),
      stderr: stderr.trim(),
      error: error ? error.message : null,
      execution_time_ms: executionTime
    });
    
    res.json({
      server,
      command,
      success,
      stdout: stdout.trim(),
      stderr: stderr.trim(),
      error: error ? error.message : null
    });
  });
});

const PORT = 3000;
app.listen(PORT, () => {
  log('Trinity Core MCP Server started');
});

4. Trinity Console UI

Create /home/claude/firefrost-services/services/arbiter-3.0/src/views/admin/mcp-logs.ejs:

<%- include('../partials/header', { title: 'MCP Logs' }) %>

<div class="container mt-4">
    <div class="d-flex justify-content-between align-items-center mb-4">
        <h1>🖥️ MCP Command Logs</h1>
        <a href="/admin" class="btn btn-outline-secondary">← Back to Dashboard</a>
    </div>
    
    <!-- Filters -->
    <div class="card mb-4">
        <div class="card-body">
            <form method="GET" class="row g-3">
                <div class="col-md-3">
                    <label class="form-label">Server</label>
                    <select name="server" class="form-select">
                        <option value="">All Servers</option>
                        <option value="command-center" <%= query.server === 'command-center' ? 'selected' : '' %>>Command Center</option>
                        <option value="tx1-dallas" <%= query.server === 'tx1-dallas' ? 'selected' : '' %>>TX1 Dallas</option>
                        <option value="nc1-charlotte" <%= query.server === 'nc1-charlotte' ? 'selected' : '' %>>NC1 Charlotte</option>
                        <option value="panel-vps" <%= query.server === 'panel-vps' ? 'selected' : '' %>>Panel VPS</option>
                        <option value="dev-panel" <%= query.server === 'dev-panel' ? 'selected' : '' %>>Dev Panel</option>
                        <option value="wiki-vps" <%= query.server === 'wiki-vps' ? 'selected' : '' %>>Wiki VPS</option>
                        <option value="services-vps" <%= query.server === 'services-vps' ? 'selected' : '' %>>Services VPS</option>
                    </select>
                </div>
                <div class="col-md-3">
                    <label class="form-label">Status</label>
                    <select name="success" class="form-select">
                        <option value="">All</option>
                        <option value="true" <%= query.success === 'true' ? 'selected' : '' %>>Success</option>
                        <option value="false" <%= query.success === 'false' ? 'selected' : '' %>>Failed</option>
                    </select>
                </div>
                <div class="col-md-3 d-flex align-items-end">
                    <button type="submit" class="btn btn-primary">Filter</button>
                    <a href="/admin/mcp-logs" class="btn btn-outline-secondary ms-2">Reset</a>
                </div>
            </form>
        </div>
    </div>
    
    <!-- Stats -->
    <div class="row mb-4">
        <div class="col-md-3">
            <div class="card bg-light">
                <div class="card-body text-center">
                    <div class="text-muted small">Total Commands</div>
                    <div class="fs-3 fw-bold"><%= total %></div>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card bg-success bg-opacity-10">
                <div class="card-body text-center">
                    <div class="text-muted small">Successful</div>
                    <div class="fs-3 fw-bold text-success"><%= successCount %></div>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card bg-danger bg-opacity-10">
                <div class="card-body text-center">
                    <div class="text-muted small">Failed</div>
                    <div class="fs-3 fw-bold text-danger"><%= failCount %></div>
                </div>
            </div>
        </div>
        <div class="col-md-3">
            <div class="card bg-info bg-opacity-10">
                <div class="card-body text-center">
                    <div class="text-muted small">Avg Execution</div>
                    <div class="fs-3 fw-bold text-info"><%= avgTime %>ms</div>
                </div>
            </div>
        </div>
    </div>
    
    <!-- Logs Table -->
    <div class="card">
        <div class="card-body p-0">
            <table class="table table-hover mb-0">
                <thead class="table-light">
                    <tr>
                        <th>Time</th>
                        <th>Server</th>
                        <th>Command</th>
                        <th>Status</th>
                        <th>Duration</th>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    <% logs.forEach(log => { %>
                    <tr>
                        <td class="text-muted small"><%= new Date(log.executed_at).toLocaleString() %></td>
                        <td><span class="badge bg-secondary"><%= log.server %></span></td>
                        <td><code class="small"><%= log.command.substring(0, 50) %><%= log.command.length > 50 ? '...' : '' %></code></td>
                        <td>
                            <% if (log.success) { %>
                                <span class="badge bg-success">✓ Success</span>
                            <% } else { %>
                                <span class="badge bg-danger">✗ Failed</span>
                            <% } %>
                        </td>
                        <td class="text-muted small"><%= log.execution_time_ms || '-' %>ms</td>
                        <td>
                            <button class="btn btn-sm btn-outline-secondary" 
                                    data-bs-toggle="modal" 
                                    data-bs-target="#logModal<%= log.id %>">
                                Details
                            </button>
                        </td>
                    </tr>
                    
                    <!-- Modal for details -->
                    <div class="modal fade" id="logModal<%= log.id %>" tabindex="-1">
                        <div class="modal-dialog modal-lg">
                            <div class="modal-content">
                                <div class="modal-header">
                                    <h5 class="modal-title">Command Details</h5>
                                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                                </div>
                                <div class="modal-body">
                                    <p><strong>Server:</strong> <%= log.server %></p>
                                    <p><strong>Command:</strong></p>
                                    <pre class="bg-dark text-light p-3 rounded"><%= log.command %></pre>
                                    <p><strong>STDOUT:</strong></p>
                                    <pre class="bg-light p-3 rounded" style="max-height: 200px; overflow: auto;"><%= log.stdout || '(empty)' %></pre>
                                    <% if (log.stderr) { %>
                                    <p><strong>STDERR:</strong></p>
                                    <pre class="bg-warning bg-opacity-10 p-3 rounded"><%= log.stderr %></pre>
                                    <% } %>
                                    <% if (log.error) { %>
                                    <p><strong>Error:</strong></p>
                                    <pre class="bg-danger bg-opacity-10 p-3 rounded"><%= log.error %></pre>
                                    <% } %>
                                </div>
                            </div>
                        </div>
                    </div>
                    <% }) %>
                    
                    <% if (logs.length === 0) { %>
                    <tr>
                        <td colspan="6" class="text-center text-muted py-4">No logs found</td>
                    </tr>
                    <% } %>
                </tbody>
            </table>
        </div>
    </div>
    
    <!-- Pagination -->
    <% if (total > limit) { %>
    <nav class="mt-4">
        <ul class="pagination justify-content-center">
            <% const totalPages = Math.ceil(total / limit); %>
            <% const currentPage = Math.floor(offset / limit) + 1; %>
            <% for (let i = 1; i <= totalPages && i <= 10; i++) { %>
            <li class="page-item <%= currentPage === i ? 'active' : '' %>">
                <a class="page-link" href="?<%= new URLSearchParams({...query, offset: (i-1) * limit}).toString() %>"><%= i %></a>
            </li>
            <% } %>
        </ul>
    </nav>
    <% } %>
</div>

<%- include('../partials/footer') %>

5. Add Route

Add to /home/claude/firefrost-services/services/arbiter-3.0/src/routes/admin/index.js:

// MCP Logs page
router.get('/mcp-logs', requireAuth, async (req, res) => {
    try {
        const { server, success, limit = 50, offset = 0 } = req.query;
        
        // Build query
        let query = 'SELECT * FROM mcp_logs WHERE 1=1';
        const params = [];
        let paramCount = 0;
        
        if (server) {
            paramCount++;
            query += ` AND server = $${paramCount}`;
            params.push(server);
        }
        
        if (success !== undefined && success !== '') {
            paramCount++;
            query += ` AND success = $${paramCount}`;
            params.push(success === 'true');
        }
        
        // Get logs
        const logsQuery = query + ` ORDER BY executed_at DESC LIMIT $${paramCount + 1} OFFSET $${paramCount + 2}`;
        const logsResult = await pool.query(logsQuery, [...params, parseInt(limit), parseInt(offset)]);
        
        // Get total count
        const countResult = await pool.query(query.replace('SELECT *', 'SELECT COUNT(*)'), params);
        const total = parseInt(countResult.rows[0].count);
        
        // Get success/fail counts
        const statsResult = await pool.query(`
            SELECT 
                COUNT(*) FILTER (WHERE success = true) as success_count,
                COUNT(*) FILTER (WHERE success = false) as fail_count,
                AVG(execution_time_ms) as avg_time
            FROM mcp_logs
        `);
        
        res.render('admin/mcp-logs', {
            logs: logsResult.rows,
            total,
            limit: parseInt(limit),
            offset: parseInt(offset),
            query: req.query,
            successCount: statsResult.rows[0].success_count || 0,
            failCount: statsResult.rows[0].fail_count || 0,
            avgTime: Math.round(statsResult.rows[0].avg_time || 0)
        });
    } catch (err) {
        console.error('MCP logs error:', err);
        res.status(500).send('Error loading MCP logs');
    }
});

6. Add to Dashboard Navigation

Add link in dashboard sidebar/nav to /admin/mcp-logs.


Deployment Steps

  1. Database: Run SQL schema on Command Center
  2. Arbiter: Add API endpoints, add route, add view, deploy
  3. Trinity Core: Update index.js on Pi, restart mcp-server
  4. Test: Execute a command, verify it appears in Trinity Console

Testing Checklist

  • Database table created
  • POST /api/internal/mcp/log works
  • GET /api/internal/mcp/logs works with filters
  • Trinity Core sends logs to Arbiter
  • Trinity Console shows logs
  • Filters work (server, success/fail)
  • Details modal shows full output
  • Pagination works

Dependencies

  • Trinity Core must be online and have network access to discord-bot.firefrostgaming.com
  • INTERNAL_API_TOKEN must be configured

Notes

  • Local file logging on Pi remains as backup
  • Arbiter logging is async (doesn't slow down command execution)
  • Logs are kept indefinitely (add retention policy later if needed)

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