UNPKG

tak-server-mcp-skyfi

Version:

Model Context Protocol (MCP) server for TAK Server integration with geospatial support

216 lines 8.09 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SSEServerTransport = void 0; exports.createSSETransport = createSSETransport; const express_1 = __importDefault(require("express")); const pino_1 = __importDefault(require("pino")); const uuid_1 = require("uuid"); const logger = (0, pino_1.default)({ level: process.env.LOG_LEVEL || 'info' }); class SSEServerTransport { server; app; config; clients = new Map(); cleanupInterval; constructor(server, config) { this.server = server; this.config = config; this.app = (0, express_1.default)(); this.setupMiddleware(); this.setupRoutes(); this.startCleanup(); } setupMiddleware() { this.app.use(express_1.default.json({ limit: '10mb' })); // CORS this.app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Client-ID'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }); // Request logging this.app.use((req, res, next) => { logger.info({ method: req.method, path: req.path }, 'SSE request'); next(); }); } setupRoutes() { // Health check this.app.get('/health', (req, res) => { res.json({ status: 'ok', service: 'tak-server-mcp-sse', version: '0.1.0', clients: this.clients.size }); }); // SSE endpoint this.app.get('/sse', (req, res) => { const clientId = req.headers['x-client-id'] || (0, uuid_1.v4)(); // Set SSE headers res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Client-ID': clientId }); // Register client const client = { id: clientId, response: res, lastActivity: new Date() }; this.clients.set(clientId, client); // Send initial connection event this.sendEvent(client, 'connected', { clientId }); // Keep connection alive const keepAlive = setInterval(() => { res.write(':keepalive\n\n'); }, 30000); // Handle client disconnect req.on('close', () => { clearInterval(keepAlive); this.clients.delete(clientId); logger.info(`SSE client disconnected: ${clientId}`); }); logger.info(`SSE client connected: ${clientId}`); }); // Request endpoint this.app.post('/sse/request', async (req, res) => { const clientId = req.headers['x-client-id']; if (!clientId || !this.clients.has(clientId)) { return res.status(400).json({ error: 'Invalid or missing client ID' }); } try { const request = req.body; const client = this.clients.get(clientId); client.lastActivity = new Date(); logger.debug({ method: request.method, clientId }, 'Processing SSE request'); // Process request const response = await this.handleRequest(request); // Send response via SSE this.sendEvent(client, 'response', response); // Also return in HTTP response for acknowledgment res.json({ success: true, id: request.id }); } catch (error) { logger.error('Error handling SSE request:', error); res.status(500).json({ error: error instanceof Error ? error.message : 'Internal error' }); } }); // Subscribe to specific events (for real-time TAK updates) this.app.post('/sse/subscribe', (req, res) => { const clientId = req.headers['x-client-id']; if (!clientId || !this.clients.has(clientId)) { return res.status(400).json({ error: 'Invalid or missing client ID' }); } const { events } = req.body; const client = this.clients.get(clientId); // Store subscription preferences (would be used by TAK event handlers) client.subscriptions = events; res.json({ success: true, subscribed: events }); }); } sendEvent(client, event, data) { try { const message = [ `event: ${event}`, `data: ${JSON.stringify(data)}`, `id: ${Date.now()}`, '', '' ].join('\n'); client.response.write(message); } catch (error) { logger.error(`Failed to send SSE event to client ${client.id}:`, error); this.clients.delete(client.id); } } async handleRequest(request) { return new Promise((resolve) => { const mockConnection = { send: (response) => { resolve(response); } }; this.server.handleRequest(request, mockConnection); }); } // Broadcast to all clients (useful for TAK events) broadcast(event, data) { for (const client of this.clients.values()) { this.sendEvent(client, event, data); } } // Broadcast to specific clients based on subscriptions broadcastToSubscribers(event, data, filter) { for (const client of this.clients.values()) { if (!filter || filter(client)) { this.sendEvent(client, event, data); } } } startCleanup() { // Clean up inactive clients every 5 minutes this.cleanupInterval = setInterval(() => { const now = new Date(); const timeout = 5 * 60 * 1000; // 5 minutes for (const [id, client] of this.clients.entries()) { if (now.getTime() - client.lastActivity.getTime() > timeout) { logger.info(`Cleaning up inactive SSE client: ${id}`); try { client.response.end(); } catch (error) { // Client might already be disconnected } this.clients.delete(id); } } }, 60000); // Check every minute } async start() { const port = this.config.mcp.port || 3001; return new Promise((resolve, reject) => { this.app.listen(port, () => { logger.info(`SSE transport listening on port ${port}`); resolve(); }).on('error', reject); }); } async close() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } // Close all client connections for (const client of this.clients.values()) { try { this.sendEvent(client, 'shutdown', { message: 'Server shutting down' }); client.response.end(); } catch (error) { // Ignore errors during shutdown } } this.clients.clear(); logger.info('SSE transport closed'); } } exports.SSEServerTransport = SSEServerTransport; async function createSSETransport(server, config) { const transport = new SSEServerTransport(server, config); await transport.start(); return transport; } //# sourceMappingURL=sse.js.map