auto-publishing-mcp-server
Version:
Enterprise-grade MCP Server for Auto-Publishing with pre-publish validation, multi-cloud deployment, and monitoring
440 lines (387 loc) • 11 kB
JavaScript
/**
* Loki Log Monitoring Tool
* Query and analyze logs through Loki API
*/
import https from 'https';
import { URL } from 'url';
export class LokiMonitorTool {
constructor(config = {}) {
this.lokiUrl = config.lokiUrl || process.env.LOKI_URL || 'http://192.168.101.1:3100';
this.defaultLimit = config.defaultLimit || 100;
this.timeout = config.timeout || 30000;
}
/**
* Execute Loki monitoring queries
*/
async execute(args, context) {
const { action, ...params } = args;
switch (action) {
case 'query':
return await this.queryLogs(params);
case 'query_range':
return await this.queryLogsRange(params);
case 'labels':
return await this.getLabels(params);
case 'label_values':
return await this.getLabelValues(params);
case 'tail':
return await this.tailLogs(params);
case 'series':
return await this.getSeries(params);
default:
throw new Error(`Unknown Loki action: ${action}`);
}
}
/**
* Query logs for a specific time point
*/
async queryLogs(params) {
const {
query = '{job=~".+"}',
time,
direction = 'backward',
limit = this.defaultLimit
} = params;
const url = new URL(`${this.lokiUrl}/loki/api/v1/query`);
url.searchParams.set('query', query);
url.searchParams.set('direction', direction);
url.searchParams.set('limit', limit.toString());
if (time) {
url.searchParams.set('time', time);
}
try {
const response = await this.makeRequest(url);
const logs = this.formatLogResponse(response.data);
return {
output: `Found ${logs.length} log entries`,
data: {
query,
logs,
resultType: response.resultType,
stats: response.stats || {}
}
};
} catch (error) {
throw new Error(`Loki query failed: ${error.message}`);
}
}
/**
* Query logs over a time range
*/
async queryLogsRange(params) {
const {
query = '{job=~".+"}',
start,
end,
direction = 'backward',
limit = this.defaultLimit,
step = '1m'
} = params;
const url = new URL(`${this.lokiUrl}/loki/api/v1/query_range`);
url.searchParams.set('query', query);
url.searchParams.set('direction', direction);
url.searchParams.set('limit', limit.toString());
url.searchParams.set('step', step);
if (start) url.searchParams.set('start', start);
if (end) url.searchParams.set('end', end);
try {
const response = await this.makeRequest(url);
const logs = this.formatLogResponse(response.data);
return {
output: `Found ${logs.length} log entries in range`,
data: {
query,
logs,
resultType: response.resultType,
stats: response.stats || {},
timeRange: { start, end }
}
};
} catch (error) {
throw new Error(`Loki range query failed: ${error.message}`);
}
}
/**
* Get available labels
*/
async getLabels(params) {
const { start, end } = params;
const url = new URL(`${this.lokiUrl}/loki/api/v1/labels`);
if (start) url.searchParams.set('start', start);
if (end) url.searchParams.set('end', end);
try {
const response = await this.makeRequest(url);
return {
output: `Found ${response.data.length} labels`,
data: {
labels: response.data,
timeRange: { start, end }
}
};
} catch (error) {
throw new Error(`Failed to get labels: ${error.message}`);
}
}
/**
* Get values for a specific label
*/
async getLabelValues(params) {
const { label, start, end } = params;
if (!label) {
throw new Error('Label parameter is required');
}
const url = new URL(`${this.lokiUrl}/loki/api/v1/label/${label}/values`);
if (start) url.searchParams.set('start', start);
if (end) url.searchParams.set('end', end);
try {
const response = await this.makeRequest(url);
return {
output: `Found ${response.data.length} values for label '${label}'`,
data: {
label,
values: response.data,
timeRange: { start, end }
}
};
} catch (error) {
throw new Error(`Failed to get label values: ${error.message}`);
}
}
/**
* Tail logs (simulate real-time streaming)
*/
async tailLogs(params) {
const {
query = '{job=~".+"}',
delayFor = 0,
limit = this.defaultLimit,
start
} = params;
const url = new URL(`${this.lokiUrl}/loki/api/v1/tail`);
url.searchParams.set('query', query);
url.searchParams.set('delay_for', delayFor.toString());
url.searchParams.set('limit', limit.toString());
if (start) url.searchParams.set('start', start);
try {
// Note: This is a simplified version. Real tailing would use WebSocket
const response = await this.makeRequest(url);
const logs = this.formatLogResponse(response.streams || []);
return {
output: `Tailing logs with query: ${query}`,
data: {
query,
logs,
isStreaming: true
}
};
} catch (error) {
throw new Error(`Failed to tail logs: ${error.message}`);
}
}
/**
* Get series information
*/
async getSeries(params) {
const { match, start, end } = params;
const url = new URL(`${this.lokiUrl}/loki/api/v1/series`);
if (match) {
if (Array.isArray(match)) {
match.forEach(m => url.searchParams.append('match[]', m));
} else {
url.searchParams.set('match[]', match);
}
}
if (start) url.searchParams.set('start', start);
if (end) url.searchParams.set('end', end);
try {
const response = await this.makeRequest(url);
return {
output: `Found ${response.data.length} series`,
data: {
series: response.data,
timeRange: { start, end }
}
};
} catch (error) {
throw new Error(`Failed to get series: ${error.message}`);
}
}
/**
* Make HTTP request to Loki API
*/
async makeRequest(url) {
return new Promise((resolve, reject) => {
const options = {
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
timeout: this.timeout
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
if (res.statusCode >= 200 && res.statusCode < 300) {
const response = JSON.parse(data);
resolve(response);
} else {
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
}
} catch (error) {
reject(new Error(`Failed to parse response: ${error.message}`));
}
});
});
req.on('error', (error) => {
reject(error);
});
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
req.end();
});
}
/**
* Format log response for consistent output
*/
formatLogResponse(data) {
if (!data) return [];
if (Array.isArray(data)) {
// Handle streams format
return data.flatMap(stream =>
stream.values?.map(([timestamp, line]) => ({
timestamp: new Date(parseInt(timestamp) / 1000000).toISOString(),
line: line,
labels: stream.stream || {}
})) || []
);
}
return [];
}
/**
* Get input schema for MCP tool registration
*/
getInputSchema() {
return {
type: "object",
properties: {
action: {
type: "string",
enum: ["query", "query_range", "labels", "label_values", "tail", "series"],
description: "Loki API action to perform"
},
query: {
type: "string",
description: "LogQL query string (e.g., '{job=\"webapp\"}')",
default: '{job=~".+"}'
},
time: {
type: "string",
description: "Query time point (RFC3339 or Unix timestamp)"
},
start: {
type: "string",
description: "Start time for range queries (RFC3339 or Unix timestamp)"
},
end: {
type: "string",
description: "End time for range queries (RFC3339 or Unix timestamp)"
},
direction: {
type: "string",
enum: ["forward", "backward"],
description: "Query direction",
default: "backward"
},
limit: {
type: "number",
description: "Maximum number of entries to return",
default: 100
},
step: {
type: "string",
description: "Query resolution step (e.g., '1m', '5s')",
default: "1m"
},
label: {
type: "string",
description: "Label name for label_values action"
},
match: {
type: ["string", "array"],
description: "Series selector for series action"
},
delayFor: {
type: "number",
description: "Delay for tail operation (seconds)",
default: 0
}
},
required: ["action"]
};
}
/**
* Helper methods for common queries
*/
async getErrorLogs(params = {}) {
const {
timeRange = '1h',
jobs = [],
limit = 50
} = params;
let query = '{job=~".+"}';
if (jobs.length > 0) {
query = `{job=~"${jobs.join('|')}"}`;
}
query += ' |~ "(?i)error|exception|fail|panic"';
return await this.queryLogsRange({
query,
start: `${Date.now() - (this.parseTimeRange(timeRange) * 1000)}000000`,
end: `${Date.now()}000000`,
limit
});
}
async getDeploymentLogs(params = {}) {
const {
deploymentId,
timeRange = '1h',
limit = 100
} = params;
let query = '{job="deployments"}';
if (deploymentId) {
query += ` |= "${deploymentId}"`;
}
return await this.queryLogsRange({
query,
start: `${Date.now() - (this.parseTimeRange(timeRange) * 1000)}000000`,
end: `${Date.now()}000000`,
limit
});
}
/**
* Parse time range string to seconds
*/
parseTimeRange(timeRange) {
const match = timeRange.match(/^(\d+)([smhd])$/);
if (!match) return 3600; // Default 1 hour
const value = parseInt(match[1]);
const unit = match[2];
switch (unit) {
case 's': return value;
case 'm': return value * 60;
case 'h': return value * 3600;
case 'd': return value * 86400;
default: return 3600;
}
}
}
export default LokiMonitorTool;