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
545 lines
19 KiB
Markdown
545 lines
19 KiB
Markdown
# 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' }) %>
|
|
|
|
<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`:
|
|
|
|
```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** 💙🔥❄️
|