Files
firefrost-services/services/trinity-core/index.js
Claude 86f87e71e6 feat(trinity-core): v2.5.0 — resilient session handling, /health endpoint
- Add unauthenticated /health endpoint (kills AUTH FAILED spam from health checks)
- Stale session + initialize request: ignore stale header, create fresh session
- Stale session + tool call: return 400 instead of 404 so client reads JSON-RPC payload
- Gemini-consulted architecture (gemini-trinity-core-mcp-sessions-2026-04-14.md)
2026-04-15 00:38:12 +00:00

320 lines
12 KiB
JavaScript

import express from 'express';
import { randomUUID } from 'node:crypto';
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 { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest, 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 ARBITER_LOG_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' },
'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());
}
async function logToArbiter(data) {
try {
await fetch(ARBITER_LOG_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ARBITER_TOKEN}`
},
body: JSON.stringify(data)
});
} catch (err) {
log(`Arbiter log failed: ${err.message}`);
}
}
const app = express();
app.use(cors({ origin: '*', credentials: true }));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// ─── Health endpoint (no auth required) ───
app.get('/health', (req, res) => {
res.status(200).json({
status: 'ok',
version: '2.5.0',
uptime: Math.floor(process.uptime()),
sessions: activeSessions.size
});
});
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 startTime = Date.now();
const result = target.local
? await executeLocal(command)
: await executeSSH(server, command);
const executionTime = Date.now() - startTime;
logToArbiter({ server, command, success: result.success, stdout: result.stdout, stderr: result.stderr, error: result.error, execution_time_ms: executionTime });
let output = result.stdout || '(no output)';
if (result.stderr) output += `\nSTDERR: ${result.stderr}`;
return { content: [{ type: "text", text: output }], isError: !result.success };
}
});
}
// Track all active transports (both SSE and Streamable HTTP)
const activeSessions = new Map();
// Create a fresh MCP server instance with tool handlers
function createMcpServer() {
const mcpServer = new Server(
{ name: "trinity-core", version: "2.5.0" },
{ capabilities: { tools: {} } }
);
setupToolHandlers(mcpServer);
return mcpServer;
}
// ─── REST API (for Arbiter / internal services) ───
app.get('/servers', auth, (req, res) => {
log(`REST /servers`);
res.json({ servers: Object.keys(SERVERS) });
});
app.post('/exec', auth, async (req, res) => {
const { server, command } = req.body;
if (!server || !command) {
return res.status(400).json({ error: 'Missing server or command' });
}
if (!SERVERS[server]) {
return res.status(400).json({ error: `Unknown server: ${server}` });
}
const target = SERVERS[server];
const startTime = Date.now();
const result = target.local
? await executeLocal(command)
: await executeSSH(server, command);
const executionTime = Date.now() - startTime;
log(`REST /exec [${server}] -> ${result.success ? 'OK' : 'FAIL'} (${executionTime}ms)`);
logToArbiter({ server, command, success: result.success, stdout: result.stdout, stderr: result.stderr, error: result.error, execution_time_ms: executionTime });
res.json({
success: result.success,
output: result.stdout || '',
stderr: result.stderr || '',
error: result.error
});
});
// ─── Streamable HTTP Transport (protocol 2025-11-25) ───
app.all('/mcp', auth, async (req, res) => {
const sessionId = req.headers['mcp-session-id'];
log(`StreamableHTTP ${req.method} from ${req.ip} session=${sessionId || 'none'}`);
try {
let transport;
if (sessionId && activeSessions.has(sessionId)) {
const existing = activeSessions.get(sessionId);
if (existing instanceof StreamableHTTPServerTransport) {
transport = existing;
} else {
return res.status(400).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Session uses different transport' },
id: null
});
}
} else if (req.method === 'POST' && isInitializeRequest(req.body)) {
// Accept initialize requests even if they carry a stale session header
if (sessionId) {
log(`StreamableHTTP re-initialize (stale session ${sessionId} ignored)`);
} else {
log(`StreamableHTTP new session (initialize)`);
}
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (sid) => {
log(`StreamableHTTP session ready: ${sid}`);
activeSessions.set(sid, transport);
}
});
transport.onclose = () => {
const sid = transport.sessionId;
if (sid && activeSessions.has(sid)) {
log(`StreamableHTTP closed: ${sid}`);
activeSessions.delete(sid);
}
};
const mcpServer = createMcpServer();
await mcpServer.connect(transport);
} else if (sessionId && !activeSessions.has(sessionId)) {
// Stale session with non-initialize request — return 400 so client reads the JSON-RPC payload
log(`StreamableHTTP stale session ${sessionId} — returning 400`);
return res.status(400).json({
jsonrpc: '2.0',
error: { code: -32001, message: 'Session not found. Please re-initialize.' },
id: req.body?.id || null
});
} else if (!sessionId && req.method === 'GET') {
// Legacy SSE client connecting via GET /mcp
return legacySSE(req, res);
} else {
return res.status(400).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Bad request: no valid session' },
id: null
});
}
await transport.handleRequest(req, res, req.body);
} catch (err) {
log(`StreamableHTTP ERROR: ${err.message}`);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: { code: -32603, message: 'Internal server error' },
id: null
});
}
}
});
// ─── Legacy SSE Transport (protocol 2024-11-05) ───
async function legacySSE(req, res) {
log(`Legacy SSE connection from ${req.ip}`);
const mcpServer = createMcpServer();
const transport = new SSEServerTransport(`${BASE_URL}/mcp/messages`, res);
await mcpServer.connect(transport);
activeSessions.set(transport.sessionId, transport);
log(`Legacy SSE session ${transport.sessionId} ready`);
res.on('close', () => {
log(`Legacy 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(`Legacy POST ${method} for ${sessionId}`);
const transport = activeSessions.get(sessionId);
if (!transport || !(transport instanceof SSEServerTransport)) {
log(`Legacy session not found: ${sessionId}`);
return res.status(400).json({ error: "Session not found. Please re-initialize." });
}
try {
await transport.handlePostMessage(req, res, req.body);
log(`Legacy POST ${method} handled OK`);
} catch (err) {
log(`Legacy POST ${method} ERROR: ${err.message}`);
console.error(err);
}
});
app.listen(PORT, () => log(`Trinity Core MCP v2.5.0 started on port ${PORT}`));