feat: Add Trinity Core MCP server to version control (v2.1.0)
- Added services/trinity-core/ with index.js, package.json, .gitignore - v2.1.0 adds local self-execution (trinity-core can audit itself) - Added executeLocal() function for localhost commands - SERVERS object now includes trinity-core with local: true flag - Version bumped from 2.0.0 to 2.1.0 - Previously lived only on Pi at ~/mcp-server/ with no backup Deployment: Edit here, push to Gitea, curl raw file to Pi, restart service Chronicler #78 | firefrost-services
This commit is contained in:
5
services/trinity-core/.gitignore
vendored
Normal file
5
services/trinity-core/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
command.log
|
||||
*.bak
|
||||
*.backup
|
||||
package-lock.json
|
||||
174
services/trinity-core/index.js
Normal file
174
services/trinity-core/index.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import express from 'express';
|
||||
import { spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import cors from 'cors';
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
const API_TOKEN = 'FFG-Trinity-2026-Core-Access';
|
||||
const LOG_FILE = '/home/claude_executor/mcp-server/command.log';
|
||||
const PORT = 3000;
|
||||
const BASE_URL = 'https://mcp.firefrostgaming.com';
|
||||
|
||||
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' },
|
||||
'trinity-core': { host: 'localhost', user: 'claude_executor', local: true }
|
||||
};
|
||||
|
||||
function log(msg) {
|
||||
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
||||
fs.appendFileSync(LOG_FILE, line);
|
||||
console.log(line.trim());
|
||||
}
|
||||
|
||||
const app = express();
|
||||
app.use(cors({ origin: '*', credentials: true }));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
function auth(req, res, next) {
|
||||
if (req.method === 'OPTIONS') return next();
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
if (token !== API_TOKEN) {
|
||||
log(`AUTH FAILED from ${req.ip}`);
|
||||
res.setHeader('WWW-Authenticate', 'Bearer realm="TrinityCore"');
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
app.get('/.well-known/oauth-protected-resource', (req, res) => {
|
||||
log(`OAuth discovery`);
|
||||
res.json({ authorization_server: BASE_URL, authorization_endpoint: `${BASE_URL}/authorize`, token_endpoint: `${BASE_URL}/token` });
|
||||
});
|
||||
|
||||
app.get('/authorize', (req, res) => {
|
||||
const { redirect_uri, state } = req.query;
|
||||
log(`OAUTH authorize -> ${redirect_uri}`);
|
||||
res.redirect(`${redirect_uri}?code=trinity-auth-code-123&state=${state}`);
|
||||
});
|
||||
|
||||
app.post('/token', (req, res) => {
|
||||
log(`OAUTH token issued`);
|
||||
res.json({ access_token: API_TOKEN, token_type: 'Bearer', expires_in: 31536000 });
|
||||
});
|
||||
|
||||
function executeLocal(command) {
|
||||
return new Promise((resolve) => {
|
||||
log(`EXEC [trinity-core] (local) ${command}`);
|
||||
const proc = spawn('bash', ['-c', command]);
|
||||
let stdout = '', stderr = '';
|
||||
proc.stdout.on('data', (d) => stdout += d.toString());
|
||||
proc.stderr.on('data', (d) => stderr += d.toString());
|
||||
const timeout = setTimeout(() => {
|
||||
proc.kill();
|
||||
resolve({ success: false, stdout: stdout.trim(),
|
||||
stderr: 'Timeout', error: 'Timeout' });
|
||||
}, 30000);
|
||||
proc.on('close', (code) => {
|
||||
clearTimeout(timeout);
|
||||
resolve({ success: code === 0,
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
error: code === 0 ? null : `Exit ${code}` });
|
||||
});
|
||||
proc.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
resolve({ success: false, stdout: '',
|
||||
stderr: err.message, error: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function executeSSH(server, command) {
|
||||
return new Promise((resolve) => {
|
||||
const target = SERVERS[server];
|
||||
log(`EXEC [${server}] ${command}`);
|
||||
const ssh = spawn('ssh', ['-o', 'ConnectTimeout=10', '-o', 'StrictHostKeyChecking=no', `${target.user}@${target.host}`, command]);
|
||||
let stdout = '', stderr = '';
|
||||
ssh.stdout.on('data', (data) => stdout += data.toString());
|
||||
ssh.stderr.on('data', (data) => stderr += data.toString());
|
||||
const timeout = setTimeout(() => { ssh.kill(); resolve({ success: false, stdout: stdout.trim(), stderr: 'Timeout', error: 'Timeout' }); }, 30000);
|
||||
ssh.on('close', (code) => { clearTimeout(timeout); resolve({ success: code === 0, stdout: stdout.trim(), stderr: stderr.trim(), error: code === 0 ? null : `Exit ${code}` }); });
|
||||
ssh.on('error', (err) => { clearTimeout(timeout); resolve({ success: false, stdout: '', stderr: err.message, error: err.message }); });
|
||||
});
|
||||
}
|
||||
|
||||
function setupToolHandlers(mcpServer) {
|
||||
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
log(`>>> ListTools request`);
|
||||
return { tools: [
|
||||
{ name: "list_servers", description: "Get available Firefrost servers.", inputSchema: { type: "object", properties: {} } },
|
||||
{ name: "run_command", description: "Execute SSH command on a Firefrost server.", inputSchema: { type: "object", properties: { server: { type: "string" }, command: { type: "string" } }, required: ["server", "command"] }}
|
||||
]};
|
||||
});
|
||||
|
||||
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
log(`>>> CallTool: ${request.params.name}`);
|
||||
if (request.params.name === "list_servers") {
|
||||
return { content: [{ type: "text", text: `Available: ${Object.keys(SERVERS).join(", ")}` }] };
|
||||
}
|
||||
if (request.params.name === "run_command") {
|
||||
const { server, command } = request.params.arguments;
|
||||
if (!SERVERS[server]) return { content: [{ type: "text", text: `Unknown server ${server}` }], isError: true };
|
||||
const target = SERVERS[server];
|
||||
const result = target.local
|
||||
? await executeLocal(command)
|
||||
: await executeSSH(server, command);
|
||||
let output = result.stdout || '(no output)';
|
||||
if (result.stderr) output += `\nSTDERR: ${result.stderr}`;
|
||||
return { content: [{ type: "text", text: output }], isError: !result.success };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const activeSessions = new Map();
|
||||
|
||||
app.get('/mcp', auth, async (req, res) => {
|
||||
log(`SSE connection from ${req.ip}`);
|
||||
|
||||
const mcpServer = new Server({ name: "trinity-core", version: "2.1.0" }, { capabilities: { tools: {} } });
|
||||
setupToolHandlers(mcpServer);
|
||||
|
||||
mcpServer.onmessage = (msg) => log(`SERVER MSG: ${JSON.stringify(msg)}`);
|
||||
|
||||
const transport = new SSEServerTransport(`${BASE_URL}/mcp/messages`, res);
|
||||
|
||||
await mcpServer.connect(transport);
|
||||
activeSessions.set(transport.sessionId, transport);
|
||||
log(`Session ${transport.sessionId} ready`);
|
||||
|
||||
res.on('close', () => {
|
||||
log(`SSE closed: ${transport.sessionId}`);
|
||||
activeSessions.delete(transport.sessionId);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/mcp/messages', auth, async (req, res) => {
|
||||
const sessionId = req.query.sessionId;
|
||||
const method = req.body?.method || 'unknown';
|
||||
log(`POST ${method} for ${sessionId}`);
|
||||
|
||||
const transport = activeSessions.get(sessionId);
|
||||
if (!transport) {
|
||||
log(`Session not found: ${sessionId}`);
|
||||
return res.status(404).json({ error: "Session not found" });
|
||||
}
|
||||
|
||||
try {
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
log(`POST ${method} handled OK`);
|
||||
} catch (err) {
|
||||
log(`POST ${method} ERROR: ${err.message}`);
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => log(`Trinity Core MCP v2.1.0 started on port ${PORT}`));
|
||||
11
services/trinity-core/package.json
Normal file
11
services/trinity-core/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "trinity-core",
|
||||
"version": "2.1.0",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user