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
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
- Database: Run SQL schema on Command Center
- Arbiter: Add API endpoints, add route, add view, deploy
- Trinity Core: Update index.js on Pi, restart mcp-server
- 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 💙🔥❄️