Skip to main content
Glama
SailingCoder

grafana-mcp-analyzer

by SailingCoder
config-manager.ts13.1 kB
import fs from 'fs'; import path from 'path'; import { createRequire } from 'module'; import axios from 'axios'; import os from 'os'; import crypto from 'crypto'; import type { QueryConfig } from '../types/index.js'; // 配置缓存存储目录 const CACHE_DIR = path.join(os.homedir(), '.grafana-mcp-analyzer', 'config-cache'); // 确保缓存目录存在 function ensureCacheDir() { if (!fs.existsSync(CACHE_DIR)) { fs.mkdirSync(CACHE_DIR, { recursive: true }); } } /** * 获取缓存最大年龄(毫秒) */ function getCacheMaxAge(): number { const cacheMaxAge = parseInt(process.env.CONFIG_MAX_AGE || '300', 10); // 默认5分钟 return cacheMaxAge * 1000; // 转换为毫秒 } /** * 检查是否禁用缓存 */ function isCacheDisabled(): boolean { return getCacheMaxAge() === 0; } /** * 清理过期的缓存文件 */ function cleanupExpiredCache(): void { if (!fs.existsSync(CACHE_DIR)) { return; } const maxAge = getCacheMaxAge(); if (maxAge === 0) { // 如果禁用缓存,清空所有缓存文件 const files = fs.readdirSync(CACHE_DIR); files.forEach(file => { const filePath = path.join(CACHE_DIR, file); if (fs.statSync(filePath).isFile()) { fs.unlinkSync(filePath); } }); console.error('🗑️ 缓存已禁用,清空所有缓存文件'); return; } // 清理过期的缓存文件 const files = fs.readdirSync(CACHE_DIR); let cleanedCount = 0; files.forEach(file => { const filePath = path.join(CACHE_DIR, file); if (fs.statSync(filePath).isFile()) { const stats = fs.statSync(filePath); const cacheAge = Date.now() - stats.mtime.getTime(); if (cacheAge > maxAge) { fs.unlinkSync(filePath); cleanedCount++; } } }); if (cleanedCount > 0) { console.error(`🗑️ 清理了 ${cleanedCount} 个过期缓存文件`); } } /** * 启动时清理缓存 */ export function initializeCacheCleanup(): void { try { const cacheMaxAge = parseInt(process.env.CONFIG_MAX_AGE || '300', 10); if (cacheMaxAge === 0) { console.error('⚠️ 配置缓存已禁用 (CONFIG_MAX_AGE=0)'); } else { console.error(`⏰ 配置缓存时间: ${cacheMaxAge}秒`); } cleanupExpiredCache(); } catch (error) { console.error('❌ 缓存清理失败:', error); } } /** * 检查是否为远程URL */ function isRemoteUrl(configPath: string): boolean { return configPath.startsWith('http://') || configPath.startsWith('https://'); } /** * 验证远程URL的安全性 */ function validateRemoteUrl(url: string): void { // 安全检查:只允许HTTPS if (!url.startsWith('https://')) { throw new Error('远程配置只支持HTTPS URL,请使用https://协议'); } // 检查URL格式 try { new URL(url); } catch (error) { throw new Error(`无效的URL格式: ${url}`); } } /** * 获取缓存文件路径 */ function getCacheFilePath(configPath: string): string { let hash: string; if (isRemoteUrl(configPath)) { // 远程URL使用URL作为hash源 hash = crypto.createHash('md5').update(configPath).digest('hex'); } else { // 本地路径使用绝对路径作为hash源 const absolutePath = path.resolve(configPath); hash = crypto.createHash('md5').update(absolutePath).digest('hex'); } return path.join(CACHE_DIR, `config-${hash}.js`); } /** * 检查缓存是否有效 */ function isCacheValid(cacheFilePath: string): boolean { // 如果缓存被禁用,返回false if (isCacheDisabled()) { return false; } if (!fs.existsSync(cacheFilePath)) { return false; } const stats = fs.statSync(cacheFilePath); const cacheAge = Date.now() - stats.mtime.getTime(); const maxAge = getCacheMaxAge(); return cacheAge < maxAge; } /** * 检查源文件是否比缓存新 */ function isSourceNewer(sourcePath: string, cacheFilePath: string): boolean { if (!fs.existsSync(sourcePath) || !fs.existsSync(cacheFilePath)) { return true; } const sourceStats = fs.statSync(sourcePath); const cacheStats = fs.statSync(cacheFilePath); return sourceStats.mtime.getTime() > cacheStats.mtime.getTime(); } /** * 从远程URL获取配置 */ async function fetchRemoteConfig(url: string): Promise<QueryConfig> { validateRemoteUrl(url); const cacheFilePath = getCacheFilePath(url); // 检查缓存 if (isCacheValid(cacheFilePath)) { console.error('📦 使用缓存的远程配置'); return loadConfigFromCache(cacheFilePath); } try { console.error('🌐 正在从远程URL获取配置...'); const response = await axios.get(url, { timeout: 30000, // 30秒超时 headers: { 'User-Agent': 'grafana-mcp-analyzer', 'Accept': 'application/javascript, text/javascript, */*' }, // 限制响应大小(10MB) maxContentLength: 10 * 1024 * 1024, maxBodyLength: 10 * 1024 * 1024 }); if (response.status !== 200) { throw new Error(`HTTP请求失败: ${response.status} ${response.statusText}`); } const configContent = response.data; if (typeof configContent !== 'string') { throw new Error('远程配置必须是JavaScript文本格式'); } // 验证配置内容是否看起来像JavaScript if (!configContent.includes('module.exports') && !configContent.includes('export')) { throw new Error('远程配置文件格式无效,必须是有效的JavaScript文件'); } // 如果缓存未禁用,保存到缓存 if (!isCacheDisabled()) { ensureCacheDir(); fs.writeFileSync(cacheFilePath, configContent, 'utf-8'); console.error('✅ 远程配置获取成功,已缓存'); } else { console.error('✅ 远程配置获取成功(缓存已禁用)'); } // 加载配置 return loadConfigFromCache(cacheFilePath); } catch (error: any) { // 如果获取失败,尝试使用缓存(即使过期) if (fs.existsSync(cacheFilePath)) { console.warn('⚠️ 远程配置获取失败,使用过期缓存:', error.message); return loadConfigFromCache(cacheFilePath); } throw new Error(`远程配置获取失败: ${error.message}`); } } /** * 从本地路径获取配置(使用缓存) */ async function fetchLocalConfig(configPath: string): Promise<QueryConfig> { const resolvedPath = path.resolve(process.cwd(), configPath); if (!fs.existsSync(resolvedPath)) { throw new Error(`配置文件不存在: ${resolvedPath}`); } const cacheFilePath = getCacheFilePath(configPath); // 检查缓存是否有效且源文件未更新 if (isCacheValid(cacheFilePath) && !isSourceNewer(resolvedPath, cacheFilePath)) { console.error('📦 使用缓存的本地配置'); return loadConfigFromCache(cacheFilePath); } try { console.error('📁 正在从本地路径加载配置...'); // 读取源文件内容 const configContent = fs.readFileSync(resolvedPath, 'utf-8'); // 如果缓存未禁用,保存到缓存并从缓存加载 if (!isCacheDisabled()) { ensureCacheDir(); fs.writeFileSync(cacheFilePath, configContent, 'utf-8'); console.error('✅ 本地配置加载成功,已缓存'); return loadConfigFromCache(cacheFilePath); } else { // 如果缓存被禁用,直接从源文件加载 console.error('✅ 本地配置加载成功(缓存已禁用)'); return loadConfigFromSource(resolvedPath); } } catch (error: any) { throw new Error(`本地配置加载失败: ${error.message}`); } } /** * 从源文件直接加载配置 */ async function loadConfigFromSource(sourcePath: string): Promise<QueryConfig> { // 使用require加载配置文件(支持用户的CommonJS格式配置) // 在ES模块中创建require函数 const require = createRequire(import.meta.url); // 清除require缓存,确保获取最新配置 delete require.cache[sourcePath]; let loadedConfig; try { loadedConfig = require(sourcePath); } catch (error: any) { throw new Error(`配置文件格式错误: ${error.message}。请确保配置文件使用 CommonJS 格式 (module.exports = config)`); } if (!loadedConfig || typeof loadedConfig !== 'object') { throw new Error('配置文件格式无效'); } return loadedConfig; } /** * 从缓存文件加载配置 */ async function loadConfigFromCache(cacheFilePath: string): Promise<QueryConfig> { // 使用require加载配置文件(支持用户的CommonJS格式配置) // 在ES模块中创建require函数 const require = createRequire(import.meta.url); // 清除require缓存,确保获取最新配置 delete require.cache[cacheFilePath]; let loadedConfig; try { loadedConfig = require(cacheFilePath); } catch (error: any) { throw new Error(`配置文件格式错误: ${error.message}。请确保配置文件使用 CommonJS 格式 (module.exports = config)`); } if (!loadedConfig || typeof loadedConfig !== 'object') { throw new Error('配置文件格式无效'); } return loadedConfig; } /** * 加载配置 */ export async function loadConfig(configPath?: string): Promise<QueryConfig> { try { // 优先使用传入的路径,其次使用环境变量 const configFilePath = configPath || process.env['CONFIG_PATH']; if (!configFilePath) { throw new Error('请指定配置文件路径。使用参数传入或设置 CONFIG_PATH 环境变量'); } let loadedConfig: QueryConfig; // 检查是否为远程URL if (isRemoteUrl(configFilePath)) { loadedConfig = await fetchRemoteConfig(configFilePath); } else { // 本地路径也使用缓存机制 loadedConfig = await fetchLocalConfig(configFilePath); } console.error('✅ 配置加载成功,包含查询:', Object.keys(loadedConfig.queries || {}).length, '个'); return loadedConfig; } catch (error: any) { // 如果是配置路径相关的错误,直接抛出 if (error.message.includes('请指定配置文件路径') || error.message.includes('配置文件不存在') || error.message.includes('远程配置获取失败') || error.message.includes('本地配置加载失败') || error.message.includes('远程配置只支持HTTPS')) { throw error; } // 其他错误(如解析错误)使用默认配置 console.warn('⚠️ 配置文件解析失败,使用默认配置:', error.message); return { baseUrl: 'https://your-grafana-instance.com', defaultHeaders: { 'Content-Type': 'application/json' }, queries: {} }; } } /** * 保存配置 */ export async function saveConfig(config: QueryConfig, configPath?: string): Promise<boolean> { try { const configFilePath = configPath || process.env['CONFIG_PATH']; if (!configFilePath) { throw new Error('请指定配置文件路径。使用参数传入或设置 CONFIG_PATH 环境变量'); } const resolvedPath = path.resolve(process.cwd(), configFilePath); // 创建配置文件内容 const configContent = `const config = ${JSON.stringify(config, null, 2)};\n\nmodule.exports = config;`; // 确保目录存在 const configDir = path.dirname(resolvedPath); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } // 写入配置文件 await fs.promises.writeFile(resolvedPath, configContent, 'utf-8'); // 清理对应的缓存文件,强制重新加载 const cacheFilePath = getCacheFilePath(configFilePath); if (fs.existsSync(cacheFilePath)) { fs.unlinkSync(cacheFilePath); } console.error(`✅ 配置已保存至: ${resolvedPath}`); return true; } catch (error) { console.error('❌ 保存配置失败:', error); return false; } } /** * 获取最大块大小配置(字节) */ export function getMaxChunkSize(): number { const maxChunkSizeKB = parseInt(process.env.MAX_CHUNK_SIZE || '100', 10); return maxChunkSizeKB * 1024; // 转换为字节 } /** * 获取配置文件路径 */ export function getConfigPath(): string { const configPath = process.env['CONFIG_PATH']; if (!configPath) { throw new Error('请设置 CONFIG_PATH 环境变量指定配置文件路径'); } return configPath; } /** * 验证配置 */ export function validateConfig(config: QueryConfig): { valid: boolean; errors: string[] } { const errors: string[] = []; // 基本验证 if (!config) { errors.push('配置不能为空'); return { valid: false, errors }; } // 检查必要字段 if (!config.baseUrl) { errors.push('缺少baseUrl配置'); } // 检查查询配置 if (config.queries) { Object.entries(config.queries).forEach(([name, query]) => { if (!query.url) { errors.push(`查询 "${name}" 缺少url配置`); } }); } return { valid: errors.length === 0, errors }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SailingCoder/grafana-mcp-analyzer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server