# 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: ```sql 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`: ```javascript // 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: ```javascript 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`: ```html <%- include('../partials/header', { title: 'MCP Logs' }) %>

🖥️ MCP Command Logs

← Back to Dashboard
Reset
Total Commands
<%= total %>
Successful
<%= successCount %>
Failed
<%= failCount %>
Avg Execution
<%= avgTime %>ms
<% logs.forEach(log => { %> <% }) %> <% if (logs.length === 0) { %> <% } %>
Time Server Command Status Duration
<%= new Date(log.executed_at).toLocaleString() %> <%= log.server %> <%= log.command.substring(0, 50) %><%= log.command.length > 50 ? '...' : '' %> <% if (log.success) { %> ✓ Success <% } else { %> ✗ Failed <% } %> <%= log.execution_time_ms || '-' %>ms
No logs found
<% if (total > limit) { %> <% } %>
<%- include('../partials/footer') %> ``` ### 5. Add Route Add to `/home/claude/firefrost-services/services/arbiter-3.0/src/routes/admin/index.js`: ```javascript // 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** 💙🔥❄️