Skip to main content
Glama

Claude Desktop Commander MCP

capture.ts10.9 kB
import {platform} from 'os'; import {randomUUID} from 'crypto'; import * as https from 'https'; import {configManager} from '../config-manager.js'; import { currentClient } from '../server.js'; let VERSION = 'unknown'; try { const versionModule = await import('../version.js'); VERSION = versionModule.VERSION; } catch { // Continue without version info if not available } // Will be initialized when needed let uniqueUserId = 'unknown'; // Function to get or create a persistent UUID async function getOrCreateUUID(): Promise<string> { try { // Try to get the UUID from the config let clientId = await configManager.getValue('clientId'); // If it doesn't exist, create a new one and save it if (!clientId) { clientId = randomUUID(); await configManager.setValue('clientId', clientId); } return clientId; } catch (error) { // Fallback to a random UUID if config operations fail return randomUUID(); } } /** * Sanitizes error objects to remove potentially sensitive information like file paths * @param error Error object or string to sanitize * @returns An object with sanitized message and optional error code */ export function sanitizeError(error: any): { message: string, code?: string } { let errorMessage = ''; let errorCode = undefined; if (error instanceof Error) { // Extract just the error name and message without stack trace errorMessage = error.name + ': ' + error.message; // Extract error code if available (common in Node.js errors) if ('code' in error) { errorCode = (error as any).code; } } else if (typeof error === 'string') { errorMessage = error; } else { errorMessage = 'Unknown error'; } // Remove any file paths using regex // This pattern matches common path formats including Windows and Unix-style paths errorMessage = errorMessage.replace(/(?:\/|\\)[\w\d_.-\/\\]+/g, '[PATH]'); errorMessage = errorMessage.replace(/[A-Za-z]:\\[\w\d_.-\/\\]+/g, '[PATH]'); return { message: errorMessage, code: errorCode }; } /** * Send an event to Google Analytics * @param event Event name * @param properties Optional event properties */ export const captureBase = async (captureURL: string, event: string, properties?: any) => { try { // Check if telemetry is enabled in config (defaults to true if not set) const telemetryEnabled = await configManager.getValue('telemetryEnabled'); // If telemetry is explicitly disabled or GA credentials are missing, don't send if (telemetryEnabled === false || !captureURL) { return; } // Get or create the client ID if not already initialized if (uniqueUserId === 'unknown') { uniqueUserId = await getOrCreateUUID(); } // Get current client information for all events let clientContext = {}; if (currentClient) { clientContext = { client_name: currentClient.name, client_version: currentClient.version, }; } // Create a deep copy of properties to avoid modifying the original objects // This ensures we don't alter error objects that are also returned to the AI let sanitizedProperties; try { sanitizedProperties = properties ? JSON.parse(JSON.stringify(properties)) : {}; } catch (e) { sanitizedProperties = {} } // Sanitize error objects if present if (sanitizedProperties.error) { // Handle different types of error objects if (typeof sanitizedProperties.error === 'object' && sanitizedProperties.error !== null) { const sanitized = sanitizeError(sanitizedProperties.error); sanitizedProperties.error = sanitized.message; if (sanitized.code) { sanitizedProperties.errorCode = sanitized.code; } } else if (typeof sanitizedProperties.error === 'string') { sanitizedProperties.error = sanitizeError(sanitizedProperties.error).message; } } // Remove any properties that might contain paths const sensitiveKeys = ['path', 'filePath', 'directory', 'file_path', 'sourcePath', 'destinationPath', 'fullPath', 'rootPath']; for (const key of Object.keys(sanitizedProperties)) { const lowerKey = key.toLowerCase(); if (sensitiveKeys.some(sensitiveKey => lowerKey.includes(sensitiveKey)) && lowerKey !== 'fileextension') { // keep fileExtension as it's safe delete sanitizedProperties[key]; } } // Is MCP installed with DXT let isDXT: string = 'false'; if (process.env.MCP_DXT) { isDXT = 'true'; } // Is MCP running in a container - use robust detection const { getSystemInfo } = await import('./system-info.js'); const systemInfo = getSystemInfo(); const isContainer: string = systemInfo.docker.isContainer ? 'true' : 'false'; const containerType: string = systemInfo.docker.containerType || 'none'; const orchestrator: string = systemInfo.docker.orchestrator || 'none'; // Add container metadata (with privacy considerations) let containerName: string = 'none'; let containerImage: string = 'none'; if (systemInfo.docker.isContainer && systemInfo.docker.containerEnvironment) { const env = systemInfo.docker.containerEnvironment; // Container name - sanitize to remove potentially sensitive info if (env.containerName) { // Keep only alphanumeric chars, dashes, and underscores // Remove random IDs and UUIDs for privacy containerName = env.containerName .replace(/[0-9a-f]{8,}/gi, 'ID') // Replace long hex strings with 'ID' .replace(/[0-9]{8,}/g, 'ID') // Replace long numeric IDs with 'ID' .substring(0, 50); // Limit length } // Docker image - sanitize registry info for privacy if (env.dockerImage) { // Remove registry URLs and keep just image:tag format containerImage = env.dockerImage .replace(/^[^/]+\/[^/]+\//, '') // Remove registry.com/namespace/ prefix .replace(/^[^/]+\//, '') // Remove simple registry.com/ prefix .replace(/@sha256:.*$/, '') // Remove digest hashes .substring(0, 100); // Limit length } } // Detect if we're running through Smithery at runtime let runtimeSource: string = 'unknown'; const processArgs = process.argv.join(' '); try { if (processArgs.includes('@smithery/cli') || processArgs.includes('smithery')) { runtimeSource = 'smithery-runtime'; } else if (processArgs.includes('npx')) { runtimeSource = 'npx-runtime'; } else { runtimeSource = 'direct-runtime'; } } catch (error) { // Ignore detection errors } // Prepare standard properties const baseProperties = { timestamp: new Date().toISOString(), platform: platform(), isContainer, containerType, orchestrator, containerName, containerImage, runtimeSource, isDXT, app_version: VERSION, engagement_time_msec: "100" }; // Combine with sanitized properties and client context const eventProperties = { ...baseProperties, ...clientContext, ...sanitizedProperties }; // Prepare GA4 payload const payload = { client_id: uniqueUserId, non_personalized_ads: false, timestamp_micros: Date.now() * 1000, events: [{ name: event, params: eventProperties }] }; // Send data to Google Analytics const postData = JSON.stringify(payload); const options = { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } }; const req = https.request(captureURL, options, (res) => { // Response handling (optional) let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { const success = res.statusCode === 200 || res.statusCode === 204; if (!success) { // Optional debug logging // console.debug(`GA tracking error: ${res.statusCode} ${data}`); } }); }); req.on('error', (error) => { // Silently fail - we don't want analytics issues to break functionality }); // Set timeout to prevent blocking the app req.setTimeout(3000, () => { req.destroy(); }); // Send data req.write(postData); req.end(); } catch (error) { // Silently fail - we don't want analytics issues to break functionality } }; export const capture_call_tool = async (event: string, properties?:any) => { const GA_MEASUREMENT_ID = 'G-35YKFM782B'; // Replace with your GA4 Measurement ID const GA_API_SECRET = 'qM5VNk6aQy6NN5s-tCppZw'; // Replace with your GA4 API Secret const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`; const GA_DEBUG_BASE_URL = `https://www.google-analytics.com/debug/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`; return await captureBase(GA_BASE_URL, event, properties); } export const capture = async (event: string, properties?: any) => { const GA_MEASUREMENT_ID = 'G-NGGDNL0K4L'; // Replace with your GA4 Measurement ID const GA_API_SECRET = '5M0mC--2S_6t94m8WrI60A'; // Replace with your GA4 API Secret const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`; const GA_DEBUG_BASE_URL = `https://www.google-analytics.com/debug/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`; return await captureBase(GA_BASE_URL, event, properties); }

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/wonderwhy-er/DesktopCommanderMCP'

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