tak-server-mcp-skyfi
Version:
Model Context Protocol (MCP) server for TAK Server integration with geospatial support
413 lines • 16.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TAKServerClient = void 0;
const axios_1 = __importDefault(require("axios"));
const https_1 = __importDefault(require("https"));
const fs_1 = __importDefault(require("fs"));
const events_1 = require("events");
const ws_1 = __importDefault(require("ws"));
const fast_xml_parser_1 = require("fast-xml-parser");
class TAKServerClient extends events_1.EventEmitter {
axios;
ws;
config;
logger;
constructor(config) {
super();
this.config = config;
this.logger = config.logger;
// Configure axios instance
const axiosConfig = {
baseURL: config.url,
timeout: config.timeout || 30000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
};
// Add authentication
if (config.apiToken) {
axiosConfig.headers['Authorization'] = `Bearer ${config.apiToken}`;
}
// Configure HTTPS agent for client certificates
if (config.clientCert && config.clientKey) {
axiosConfig.httpsAgent = new https_1.default.Agent({
cert: fs_1.default.readFileSync(config.clientCert),
key: fs_1.default.readFileSync(config.clientKey),
rejectUnauthorized: config.verifySsl !== false
});
}
else if (config.verifySsl === false) {
axiosConfig.httpsAgent = new https_1.default.Agent({
rejectUnauthorized: false
});
}
this.axios = axios_1.default.create(axiosConfig);
// Add request/response interceptors for logging
if (this.logger) {
this.axios.interceptors.request.use((config) => {
this.logger.debug({ method: config.method, url: config.url }, 'TAK Server request');
return config;
}, (error) => {
this.logger.error({ error }, 'TAK Server request error');
return Promise.reject(error);
});
this.axios.interceptors.response.use((response) => {
this.logger.debug({ status: response.status, url: response.config.url }, 'TAK Server response');
return response;
}, (error) => {
this.logger.error({ error: error.message, status: error.response?.status }, 'TAK Server response error');
return Promise.reject(error);
});
}
}
async testConnection() {
try {
// TAK Server version endpoint
const response = await this.axios.get('/Marti/api/version');
this.logger?.info('Connected to TAK Server', { version: response.data });
}
catch (error) {
throw new Error(`Failed to connect to TAK Server: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
// CoT Event Management
async getCotEvents(params) {
const events = [];
const xmlParser = new fast_xml_parser_1.XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
parseAttributeValue: true
});
// If specific UIDs provided, fetch each one
if (params?.uids && params.uids.length > 0) {
for (const uid of params.uids) {
try {
const response = await this.axios.get(`/api/cot/xml/${uid}`);
const parsed = xmlParser.parse(response.data);
if (parsed.event) {
events.push(this.parseCotEvent(parsed.event));
}
}
catch (error) {
this.logger?.warn(`Failed to fetch CoT event for UID ${uid}:`, error);
}
}
}
else {
// For history/search, TAK Server uses different endpoint
for (const uid of params?.uids || ['*']) {
let url = `/api/cot/xml/${uid}/all`;
const queryParams = new URLSearchParams();
if (params?.start) {
queryParams.append('start', this.formatTAKDate(params.start));
}
if (params?.end) {
queryParams.append('end', this.formatTAKDate(params.end));
}
if (queryParams.toString()) {
url += `?${queryParams.toString()}`;
}
try {
const response = await this.axios.get(url, {
headers: { 'Accept': 'application/xml' }
});
// Parse multiple events from response
const eventMatches = response.data.match(/<event[^>]*>[\s\S]*?<\/event>/g);
if (eventMatches) {
for (const eventXml of eventMatches) {
const parsed = xmlParser.parse(eventXml);
if (parsed.event) {
const event = this.parseCotEvent(parsed.event);
// Filter by type if specified
if (!params?.types || params.types.some(t => event.type.startsWith(t))) {
events.push(event);
}
}
}
}
}
catch (error) {
this.logger?.error('Failed to fetch CoT history:', error);
}
}
}
// Apply limit if specified
if (params?.limit && events.length > params.limit) {
return events.slice(0, params.limit);
}
return events;
}
formatTAKDate(date) {
// TAK Server expects ISO format with milliseconds
return date.toISOString();
}
parseCotEvent(eventData) {
return {
uid: eventData.uid,
type: eventData.type,
time: new Date(eventData.time),
start: new Date(eventData.start),
stale: new Date(eventData.stale),
how: eventData.how,
point: {
lat: parseFloat(eventData.point.lat),
lon: parseFloat(eventData.point.lon),
hae: parseFloat(eventData.point.hae) || 999999,
ce: parseFloat(eventData.point.ce) || 999999,
le: parseFloat(eventData.point.le) || 999999
},
detail: eventData.detail
};
}
async sendCotEvent(event) {
const xmlBuilder = new fast_xml_parser_1.XMLBuilder({
ignoreAttributes: false,
attributeNamePrefix: '@_',
format: true
});
// Build XML from CotMessage
const xml = xmlBuilder.build(event);
// TAK Server expects XML in the body
await this.axios.post('/Marti/api/cot/submit', xml, {
headers: {
'Content-Type': 'application/xml'
}
});
}
async subscribeToCotEvents(filter, onEvent) {
// TAK Server uses different WebSocket endpoint patterns
const wsUrl = this.config.url.replace(/^https?/, 'wss') + '/Marti/api/takcl/ws';
const wsOptions = {
rejectUnauthorized: this.config.verifySsl !== false
};
// Add authentication headers
if (this.config.apiToken) {
wsOptions.headers = {
'Authorization': `Bearer ${this.config.apiToken}`
};
}
// Add client certificate if configured
if (this.config.clientCert && this.config.clientKey) {
wsOptions.cert = fs_1.default.readFileSync(this.config.clientCert);
wsOptions.key = fs_1.default.readFileSync(this.config.clientKey);
}
this.ws = new ws_1.default(wsUrl, wsOptions);
const xmlParser = new fast_xml_parser_1.XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
parseAttributeValue: true
});
this.ws.on('open', () => {
this.logger?.info('WebSocket connection established');
// TAK Server may expect subscription message
if (filter?.types) {
const subscribeMsg = {
type: 'subscribe',
filters: filter.types
};
this.ws.send(JSON.stringify(subscribeMsg));
}
});
this.ws.on('message', (data) => {
try {
const message = data.toString();
// TAK Server sends CoT events as XML
if (message.includes('<event')) {
const parsed = xmlParser.parse(message);
if (parsed.event) {
const event = this.parseCotEvent(parsed.event);
this.emit('cot-event', event);
if (onEvent)
onEvent(event);
}
}
else {
// Handle other message types (JSON status messages, etc.)
try {
const jsonMsg = JSON.parse(message);
this.logger?.debug('Received JSON message:', jsonMsg);
}
catch {
this.logger?.debug('Received non-JSON message:', message);
}
}
}
catch (error) {
this.logger?.error('Failed to parse WebSocket message:', error);
}
});
this.ws.on('error', (error) => {
this.logger?.error('WebSocket error:', error);
this.emit('error', error);
});
this.ws.on('close', () => {
this.logger?.info('WebSocket connection closed');
this.emit('disconnected');
});
}
async unsubscribe() {
if (this.ws) {
this.ws.close();
this.ws = undefined;
}
}
// Entity Management
async getEntities(params) {
// TAK Server doesn't have a direct "entities" endpoint
// We need to get CoT events and transform them to entities
const events = await this.getCotEvents({
types: params?.types,
bbox: params?.bbox,
limit: 1000 // Get recent events
});
// Transform CoT events to TAK entities
const entitiesMap = new Map();
for (const event of events) {
// Use the most recent event for each UID
if (!entitiesMap.has(event.uid) ||
new Date(event.time) > new Date(entitiesMap.get(event.uid).lastUpdate)) {
const entity = {
uid: event.uid,
callsign: event.detail?.contact?.callsign || event.uid,
type: event.type,
team: event.detail?.group?.name || 'Unknown',
role: event.detail?.group?.role || 'Unknown',
location: {
lat: event.point.lat,
lon: event.point.lon,
alt: event.point.hae !== 999999 ? event.point.hae : undefined
},
lastUpdate: event.time,
status: {
online: new Date(event.stale) > new Date(),
battery: event.detail?.status?.battery,
speed: event.detail?.track?.speed,
course: event.detail?.track?.course,
readiness: event.detail?.status?.readiness ? 'ready' : 'unknown'
},
attributes: event.detail
};
// Apply filters
if (params?.teams && !params.teams.includes(entity.team))
continue;
if (params?.roles && !params.roles.includes(entity.role))
continue;
entitiesMap.set(event.uid, entity);
}
}
return Array.from(entitiesMap.values());
}
async getEntity(uid) {
// Get the specific CoT event for this UID
const response = await this.axios.get(`/api/cot/xml/${uid}`);
const xmlParser = new fast_xml_parser_1.XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
parseAttributeValue: true
});
const parsed = xmlParser.parse(response.data);
if (!parsed.event) {
throw new Error(`Entity not found: ${uid}`);
}
const event = this.parseCotEvent(parsed.event);
// Transform to TAKEntity
return {
uid: event.uid,
callsign: event.detail?.contact?.callsign || event.uid,
type: event.type,
team: event.detail?.group?.name || 'Unknown',
role: event.detail?.group?.role || 'Unknown',
location: {
lat: event.point.lat,
lon: event.point.lon,
alt: event.point.hae !== 999999 ? event.point.hae : undefined
},
lastUpdate: event.time,
status: {
online: new Date(event.stale) > new Date(),
battery: event.detail?.status?.battery,
speed: event.detail?.track?.speed,
course: event.detail?.track?.course,
readiness: event.detail?.status?.readiness ? 'ready' : 'unknown'
},
attributes: event.detail
};
}
// Mission Management
async getMissions() {
const response = await this.axios.get('/Marti/api/missions');
return response.data;
}
async getMission(name) {
const response = await this.axios.get(`/Marti/api/missions/${name}`);
return response.data;
}
async createMission(mission) {
const response = await this.axios.post('/Marti/api/missions', mission);
return response.data;
}
async updateMission(name, updates) {
const response = await this.axios.put(`/Marti/api/missions/${name}`, updates);
return response.data;
}
async deleteMission(name) {
await this.axios.delete(`/Marti/api/missions/${name}`);
}
// Data Package Management
async getDataPackages() {
const response = await this.axios.get('/Marti/api/datapackages');
return response.data;
}
async uploadDataPackage(file, metadata) {
const formData = new FormData();
formData.append('file', new Blob([file]));
formData.append('metadata', JSON.stringify(metadata));
const response = await this.axios.post('/Marti/api/datapackages', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return response.data;
}
async downloadDataPackage(id) {
const response = await this.axios.get(`/Marti/api/datapackages/${id}`, {
responseType: 'arraybuffer'
});
return Buffer.from(response.data);
}
async deleteDataPackage(id) {
await this.axios.delete(`/Marti/api/datapackages/${id}`);
}
// Geospatial Operations
async spatialQuery(params) {
const response = await this.axios.post('/Marti/api/spatial/query', params);
return response.data;
}
async calculateDistance(point1, point2) {
const response = await this.axios.post('/Marti/api/spatial/distance', { point1, point2 });
return response.data.distance;
}
async findNearest(params) {
const response = await this.axios.post('/Marti/api/spatial/nearest', params);
return response.data;
}
// Alert Management
async getAlerts(active) {
const response = await this.axios.get(`/Marti/api/alerts${active !== undefined ? `?active=${active}` : ''}`);
return response.data;
}
async sendAlert(alert) {
await this.axios.post('/Marti/api/alerts', alert);
}
// Cleanup
async disconnect() {
await this.unsubscribe();
this.removeAllListeners();
}
}
exports.TAKServerClient = TAKServerClient;
//# sourceMappingURL=tak-server.js.map