Skip to main content
Glama
portel-dev

NCP - Natural Context Provider

by portel-dev
analytics.ts11.9 kB
/** * Analytics Internal MCP * * Provides visual analytics with ASCII charts for AI consumption. * Returns data wrapped in markdown code blocks for rendering. */ import { InternalMCP, InternalTool, InternalToolResult } from './types.js'; import { NCPLogParser, AnalyticsReport } from '../analytics/log-parser.js'; import { VisualAnalyticsFormatter } from '../analytics/visual-formatter.js'; import { TokenMetricsTracker } from '../analytics/token-metrics.js'; import { logger } from '../utils/logger.js'; export class AnalyticsMCP implements InternalMCP { name = 'analytics'; description = 'View NCP usage analytics, token savings, and performance metrics'; private parser: NCPLogParser; private tokenTracker: TokenMetricsTracker; constructor() { this.parser = new NCPLogParser(); this.tokenTracker = new TokenMetricsTracker(); } tools: InternalTool[] = [ { name: 'overview', description: 'Visual analytics dashboard with ASCII charts showing usage stats, token savings, performance metrics, and trends. Returns formatted in markdown code blocks for rendering.', inputSchema: { type: 'object', properties: { period: { type: 'number', description: 'Number of days to analyze (e.g., 7 for last week, 30 for last month). Default: 7' }, today: { type: 'boolean', description: 'Show only today\'s data. Default: false' } } } }, { name: 'performance', description: 'Visual performance report with gauges and charts showing fastest MCPs, reliability metrics, and response times. Returns formatted in markdown code blocks.', inputSchema: { type: 'object', properties: { period: { type: 'number', description: 'Number of days to analyze. Default: 7' }, today: { type: 'boolean', description: 'Show only today\'s data. Default: false' } } } }, { name: 'usage', description: 'Detailed usage statistics with properly spaced markdown tables: most used MCPs, tool counts, hourly patterns, peak usage times. Tables are readable in plain text without rendering.', inputSchema: { type: 'object', properties: { period: { type: 'number', description: 'Number of days to analyze. Default: 7' }, today: { type: 'boolean', description: 'Show only today\'s data. Default: false' } } } }, { name: 'tokens', description: 'Token usage and savings from Code-Mode vs find+run. Shows efficiency gains.', inputSchema: { type: 'object', properties: { period: { type: 'number', description: 'Days to analyze (default: 7)' } } } } ]; async executeTool(toolName: string, args: any): Promise<InternalToolResult> { try { // Handle token analytics separately (uses different data source) if (toolName === 'tokens') { const period = args.period || 7; return await this.formatTokenMetrics(period); } // Parse time range options for MCP log-based analytics const parseOptions: any = {}; if (args.today) { parseOptions.today = true; } else if (args.period) { parseOptions.period = parseInt(args.period) || 7; } const report = await this.parser.parseAllLogs(parseOptions); if (report.totalSessions === 0) { return { success: true, content: [{ type: 'text', text: '📊 No analytics data available for the specified time range.\n\n' + 'MCPs need to be used through NCP to generate analytics data.' }] }; } switch (toolName) { case 'overview': return await this.formatDashboard(report); case 'performance': return await this.formatPerformance(report); case 'usage': return this.formatUsage(report); default: throw new Error(`Unknown tool: ${toolName}`); } } catch (error) { logger.error(`[AnalyticsMCP] Error executing ${toolName}:`, error); return { success: false, error: error instanceof Error ? error.message : String(error) }; } } /** * Format dashboard with visual charts in markdown code blocks */ private async formatDashboard(report: AnalyticsReport): Promise<InternalToolResult> { const visualOutput = await VisualAnalyticsFormatter.formatVisualDashboard(report); const markdown = `\`\`\`txt\n${visualOutput}\n\`\`\``; return { success: true, content: [{ type: 'text', text: markdown }] }; } /** * Format performance metrics with visual charts in markdown code blocks */ private async formatPerformance(report: AnalyticsReport): Promise<InternalToolResult> { const visualOutput = await VisualAnalyticsFormatter.formatVisualPerformance(report); const markdown = `\`\`\`txt\n${visualOutput}\n\`\`\``; return { success: true, content: [{ type: 'text', text: markdown }] }; } /** * Format usage statistics with properly spaced markdown tables */ private formatUsage(report: AnalyticsReport): InternalToolResult { const days = Math.ceil((report.timeRange.end.getTime() - report.timeRange.start.getTime()) / (1000 * 60 * 60 * 24)); const output: string[] = []; output.push('# 📈 NCP Usage Statistics'); output.push(''); output.push(`**Period**: ${days === 1 ? 'Today' : `Last ${days} days`}`); output.push(''); // Most Used MCPs - properly spaced markdown table output.push('## 🔥 Most Used MCPs'); output.push(''); const topMCPs = (report.topMCPsByUsage || []).slice(0, 10); const maxNameLen = topMCPs.length > 0 ? Math.max(...topMCPs.map(m => m?.name?.length || 0), 15) : 15; // Header output.push(`| ${'#'.padEnd(3)} | ${'MCP'.padEnd(maxNameLen)} | ${'Sessions'.padStart(8)} | ${'% Total'.padStart(9)} |`); output.push(`|${'-'.repeat(5)}|${'-'.repeat(maxNameLen + 2)}|${'-'.repeat(10)}|${'-'.repeat(11)}|`); // Rows topMCPs.forEach((mcp, i) => { const rank = `${i + 1}`.padEnd(3); const name = (mcp?.name || 'unknown').padEnd(maxNameLen); const sessions = (mcp?.sessions || 0).toString().padStart(8); const pct = `${(((mcp?.sessions || 0) / (report.totalSessions || 1)) * 100).toFixed(1)}%`.padStart(9); output.push(`| ${rank} | ${name} | ${sessions} | ${pct} |`); }); output.push(''); // Tool-Rich MCPs output.push('## 🛠️ Tool-Rich MCPs'); output.push(''); const toolRich = (report.topMCPsByTools || []).slice(0, 10); const maxToolNameLen = toolRich.length > 0 ? Math.max(...toolRich.map(m => m?.name?.length || 0), 15) : 15; // Header output.push(`| ${'#'.padEnd(3)} | ${'MCP'.padEnd(maxToolNameLen)} | ${'Tools'.padStart(6)} |`); output.push(`|${'-'.repeat(5)}|${'-'.repeat(maxToolNameLen + 2)}|${'-'.repeat(8)}|`); // Rows toolRich.forEach((mcp, i) => { const rank = `${i + 1}`.padEnd(3); const name = (mcp?.name || 'unknown').padEnd(maxToolNameLen); const tools = (mcp?.toolCount || 0).toString().padStart(6); output.push(`| ${rank} | ${name} | ${tools} |`); }); output.push(''); // Hourly Usage Pattern output.push('## ⏰ Hourly Usage Pattern'); output.push(''); const hourlyEntries = Object.entries(report.hourlyUsage || {}) .sort(([a], [b]) => parseInt(a) - parseInt(b)); // Header output.push(`| ${'Hour'.padEnd(6)} | ${'Sessions'.padStart(8)} |`); output.push(`|${'-'.repeat(8)}|${'-'.repeat(10)}|`); // Rows hourlyEntries.forEach(([hour, count]) => { const hourStr = `${hour}:00`.padEnd(6); const countStr = count.toString().padStart(8); output.push(`| ${hourStr} | ${countStr} |`); }); output.push(''); // Summary output.push('## 📊 Summary'); output.push(''); output.push(`| ${'Metric'.padEnd(20)} | ${'Value'.padEnd(15)} |`); output.push(`|${'-'.repeat(22)}|${'-'.repeat(17)}|`); output.push(`| ${'Total Sessions'.padEnd(20)} | ${report.totalSessions.toLocaleString().padEnd(15)} |`); output.push(`| ${'Unique MCPs'.padEnd(20)} | ${report.uniqueMCPs.toString().padEnd(15)} |`); output.push(`| ${'Peak Hour'.padEnd(20)} | ${this.getPeakHour(report.hourlyUsage).padEnd(15)} |`); output.push(`| ${'Avg Sessions/Day'.padEnd(20)} | ${(report.totalSessions / days).toFixed(0).padEnd(15)} |`); return { success: true, content: [{ type: 'text', text: output.join('\n') }] }; } /** * Get peak usage hour */ private getPeakHour(hourlyUsage: Record<string, number>): string { const entries = Object.entries(hourlyUsage); if (entries.length === 0) return 'N/A'; const peak = entries.reduce((max, curr) => curr[1] > max[1] ? curr : max ); return `${peak[0]}:00 (${peak[1]} sessions)`; } /** * Format token metrics and savings report */ private async formatTokenMetrics(daysBack: number): Promise<InternalToolResult> { const report = await this.tokenTracker.generateReport(daysBack); if (report.totalExecutions === 0) { return { success: true, content: [{ type: 'text', text: '📊 No token usage data available.\n\n' + 'Token tracking starts after you use find, run, or code tools.' }] }; } let output = `## 🎯 Token Usage & Savings Report (Last ${daysBack} days)\n\n`; // Summary stats output += `### Summary\n\n`; output += `- **Total Executions**: ${report.totalExecutions}\n`; output += ` - find: ${report.breakdown.find}\n`; output += ` - run: ${report.breakdown.run}\n`; output += ` - code: ${report.breakdown.code}\n`; output += `- **Total Tokens Used**: ${report.totalTokensUsed.toLocaleString()}\n`; output += `- **Total Tokens Saved**: ${report.totalTokensSaved.toLocaleString()} (via Code-Mode)\n\n`; // Code-Mode efficiency if (report.codeModeEfficiency.executions > 0) { output += `### ⚡ Code-Mode Efficiency\n\n`; output += `- **Executions**: ${report.codeModeEfficiency.executions}\n`; output += `- **Avg Tools per Execution**: ${report.codeModeEfficiency.avgToolsPerExecution.toFixed(1)}\n`; output += `- **Avg Tokens per Execution**: ${Math.round(report.codeModeEfficiency.avgTokensPerExecution)}\n`; output += `- **Total Savings**: ${report.codeModeEfficiency.totalSavings.toLocaleString()} tokens\n`; output += `- **Savings Rate**: ${report.codeModeEfficiency.percentSavings.toFixed(1)}%\n\n`; // Explain the savings output += `*Code-Mode executes multiple tools in one call, saving token overhead from repeated find+run cycles.*\n\n`; } // Average tokens per execution type output += `### 📊 Average Tokens per Execution\n\n`; output += `| Type | Avg Tokens |\n`; output += `|------|------------|\n`; output += `| find | ${Math.round(report.avgTokensPerExecution.find)} |\n`; output += `| run | ${Math.round(report.avgTokensPerExecution.run)} |\n`; output += `| code | ${Math.round(report.avgTokensPerExecution.code)} |\n\n`; // Time range output += `*Data from ${new Date(report.timeRange.start).toLocaleDateString()} to ${new Date(report.timeRange.end).toLocaleDateString()}*\n`; return { success: true, content: [{ type: 'text', text: output }] }; } }

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/portel-dev/ncp'

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