Skip to main content
Glama

Theneo MCP Server

by atombreak
telemetry.service.tsβ€’5.8 kB
/** * Privacy-first telemetry service * * Principles: * - Completely opt-in (disabled by default) * - Anonymous - no user identification * - Local-first - data stored locally, optionally shared * - Transparent - users see exactly what's collected * - Minimal - only essential metrics */ import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import { TelemetryEvent, TelemetryConfig, TelemetryStorage } from './types.js'; import { logger } from '../utils/logger.js'; const DEFAULT_STORAGE_DIR = path.join(os.homedir(), '.theneo-mcp'); const STORAGE_FILE = 'telemetry.json'; const STORAGE_VERSION = '1.0'; export class TelemetryService { private config: TelemetryConfig; constructor(enabled: boolean = false, endpoint?: string) { this.config = { enabled, endpoint, storagePath: path.join(DEFAULT_STORAGE_DIR, STORAGE_FILE), }; } /** * Check if telemetry is enabled */ isEnabled(): boolean { // Check environment variable as well if (process.env.THENEO_TELEMETRY_DISABLED === 'true') { return false; } return this.config.enabled; } /** * Record a telemetry event (non-blocking) */ async recordEvent( tool: string, duration_ms: number, success: boolean ): Promise<void> { if (!this.isEnabled()) { return; } try { const event: TelemetryEvent = { timestamp: new Date().toISOString(), tool, duration_ms, success, sdk_version: this.getSDKVersion(), node_version: process.version, os_type: this.getOSType(), }; await this.storeEvent(event); } catch (error) { // Telemetry failures should never affect normal operation logger.debug('Failed to record telemetry event', { error }); } } /** * Get stored telemetry data */ async getStoredData(): Promise<TelemetryStorage | null> { try { const data = await fs.readFile(this.config.storagePath, 'utf-8'); return JSON.parse(data); } catch (error) { if ((error as any).code === 'ENOENT') { return null; } throw error; } } /** * Clear all stored telemetry data */ async clearData(): Promise<void> { try { await fs.unlink(this.config.storagePath); logger.info('Telemetry data cleared'); } catch (error) { if ((error as any).code !== 'ENOENT') { throw error; } } } /** * Get telemetry statistics */ async getStats(): Promise<{ totalEvents: number; successRate: number; toolUsage: Record<string, number>; averageDuration: number; } | null> { const storage = await this.getStoredData(); if (!storage || storage.events.length === 0) { return null; } const events = storage.events; const successCount = events.filter(e => e.success).length; const toolUsage = events.reduce((acc, e) => { acc[e.tool] = (acc[e.tool] || 0) + 1; return acc; }, {} as Record<string, number>); const averageDuration = events.reduce((sum, e) => sum + e.duration_ms, 0) / events.length; return { totalEvents: events.length, successRate: successCount / events.length, toolUsage, averageDuration: Math.round(averageDuration), }; } /** * Store an event locally */ private async storeEvent(event: TelemetryEvent): Promise<void> { // Ensure directory exists const dir = path.dirname(this.config.storagePath); await fs.mkdir(dir, { recursive: true }); let storage: TelemetryStorage; try { const existing = await this.getStoredData(); if (existing) { storage = existing; storage.events.push(event); storage.eventCount = storage.events.length; } else { storage = { version: STORAGE_VERSION, createdAt: new Date().toISOString(), eventCount: 1, events: [event], }; } } catch { storage = { version: STORAGE_VERSION, createdAt: new Date().toISOString(), eventCount: 1, events: [event], }; } // Keep only last 1000 events to prevent unbounded growth if (storage.events.length > 1000) { storage.events = storage.events.slice(-1000); storage.eventCount = storage.events.length; } await fs.writeFile( this.config.storagePath, JSON.stringify(storage, null, 2), 'utf-8' ); } /** * Get SDK version from package.json */ private getSDKVersion(): string { try { // This will be the Theneo SDK version const pkg = require('@theneo/sdk/package.json'); return pkg.version || 'unknown'; } catch { return 'unknown'; } } /** * Get generic OS type (no specific details for privacy) */ private getOSType(): 'darwin' | 'linux' | 'win32' | 'other' { const platform = os.platform(); if (platform === 'darwin' || platform === 'linux' || platform === 'win32') { return platform; } return 'other'; } } /** * Create a telemetry wrapper for handler functions */ export function withTelemetry<T extends (...args: any[]) => Promise<any>>( toolName: string, handler: T, telemetry: TelemetryService ): T { return (async (...args: any[]) => { const startTime = Date.now(); let success = true; try { const result = await handler(...args); success = !result.isError; return result; } catch (error) { success = false; throw error; } finally { const duration = Date.now() - startTime; // Non-blocking telemetry recording telemetry.recordEvent(toolName, duration, success).catch(() => { // Silently ignore telemetry errors }); } }) as T; }

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/atombreak/theneo-mcp'

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