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
This commit is contained in:
544
docs/tasks/task-109-mcp-logging/README.md
Normal file
544
docs/tasks/task-109-mcp-logging/README.md
Normal file
@@ -0,0 +1,544 @@
|
||||
# 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** 💙🔥❄️
|
||||
Reference in New Issue
Block a user