diff --git a/services/trinity-core/.gitignore b/services/trinity-core/.gitignore new file mode 100644 index 0000000..53842f6 --- /dev/null +++ b/services/trinity-core/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +command.log +*.bak +*.backup +package-lock.json diff --git a/services/trinity-core/index.js b/services/trinity-core/index.js new file mode 100644 index 0000000..f0ff3ef --- /dev/null +++ b/services/trinity-core/index.js @@ -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}`)); diff --git a/services/trinity-core/package.json b/services/trinity-core/package.json new file mode 100644 index 0000000..bcafedb --- /dev/null +++ b/services/trinity-core/package.json @@ -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" + } +}