Skip to main content
Glama
enhanced-logging-server.ts33.3 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import { UserSessionManager, UserCredentials } from './user-session-manager.js'; import { AuthCredentials, QueryParams, QueryParamsSchema, UMBRELLA_ENDPOINTS } from './types.js'; import * as fs from 'fs'; import * as path from 'path'; // Enhanced logging utilities class MCPLogger { private logFile: string; constructor() { this.logFile = path.join(process.cwd(), 'mcp-server.log'); this.log('INFO', '=== MCP Server Starting ==='); } private log(level: string, message: string, data?: any) { const timestamp = new Date().toISOString(); const logEntry = { timestamp, level, message, data: data ? JSON.stringify(data, null, 2) : undefined, pid: process.pid }; const logLine = `[${timestamp}] ${level}: ${message}${data ? '\nData: ' + JSON.stringify(data, null, 2) : ''}\n`; // Write to file try { fs.appendFileSync(this.logFile, logLine); } catch (error) { console.error('Failed to write to log file:', error); } // Also write to stderr for Claude Desktop logs console.error(logLine); } info(message: string, data?: any) { this.log('INFO', message, data); } warn(message: string, data?: any) { this.log('WARN', message, data); } error(message: string, data?: any) { this.log('ERROR', message, data); } debug(message: string, data?: any) { this.log('DEBUG', message, data); } request(method: string, params: any) { this.log('REQUEST', `Method: ${method}`, { method, params }); } response(method: string, result: any) { this.log('RESPONSE', `Method: ${method}`, { method, result }); } auth(message: string, username?: string, success?: boolean) { this.log('AUTH', message, { username, success, timestamp: new Date().toISOString() }); } } export class EnhancedUmbrellaMcpServer { private server: Server; private sessionManager: UserSessionManager; private baseURL: string; private logger: MCPLogger; constructor(baseURL: string) { this.baseURL = baseURL; this.logger = new MCPLogger(); this.sessionManager = new UserSessionManager(baseURL); this.logger.info('Initializing Enhanced Umbrella MCP Server', { baseURL: this.baseURL, nodeVersion: process.version, platform: process.platform, cwd: process.cwd() }); this.server = new Server( { name: process.env.MCP_SERVER_NAME || 'Umbrella MCP Enhanced', version: process.env.MCP_SERVER_VERSION || '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); this.setupErrorHandling(); } private setupErrorHandling(): void { process.on('uncaughtException', (error) => { this.logger.error('Uncaught Exception', { error: error.message, stack: error.stack, name: error.name }); }); process.on('unhandledRejection', (reason, promise) => { this.logger.error('Unhandled Rejection', { reason: reason instanceof Error ? reason.message : String(reason), stack: reason instanceof Error ? reason.stack : undefined, promise: String(promise) }); }); } private setupToolHandlers(): void { this.logger.info('Setting up tool handlers'); // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async (request) => { this.logger.request('listTools', request.params); try { const authTool: Tool = { name: 'authenticate_user', description: 'Authenticate with Umbrella Cost API using username and password (multi-tenant)', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Your Umbrella Cost username (email address)', }, password: { type: 'string', description: 'Your Umbrella Cost password', }, sessionId: { type: 'string', description: 'Optional: Custom session identifier (defaults to username-based ID)', }, }, required: ['username', 'password'], }, }; const logoutTool: Tool = { name: 'logout', description: 'Log out and remove user session', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Username to logout (optional - will logout current session if not provided)', }, }, required: [], }, }; const sessionStatusTool: Tool = { name: 'session_status', description: 'Check authentication status and session information', inputSchema: { type: 'object', properties: { username: { type: 'string', description: 'Username to check status for (optional)', }, }, required: [], }, }; const apiTools: Tool[] = UMBRELLA_ENDPOINTS.map((endpoint) => ({ name: `api_${endpoint.path.replace(/\//g, '_').replace(/[-]/g, '_')}`, description: `${endpoint.description} (${endpoint.category})`, inputSchema: { type: 'object', properties: { ...(endpoint.parameters && Object.keys(endpoint.parameters).length > 0 ? Object.entries(endpoint.parameters).reduce((acc, [key, desc]) => { acc[key] = { type: 'string', description: desc, }; return acc; }, {} as Record<string, any>) : {}), }, required: [], }, })); const listEndpointsTool: Tool = { name: 'list_endpoints', description: 'List all available Umbrella Cost API endpoints with their descriptions', inputSchema: { type: 'object', properties: {}, required: [], }, }; const helpTool: Tool = { name: 'help', description: 'Get help and usage information for the Umbrella MCP server', inputSchema: { type: 'object', properties: { topic: { type: 'string', description: 'Specific help topic (authentication, endpoints, parameters)', }, }, required: [], }, }; const result = { tools: [authTool, logoutTool, sessionStatusTool, listEndpointsTool, helpTool, ...apiTools], }; this.logger.response('listTools', { toolCount: result.tools.length }); return result; } catch (error: any) { this.logger.error('Error in listTools', { error: error.message, stack: error.stack }); throw error; } }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; this.logger.request('callTool', { toolName: name, arguments: args }); try { // Authentication tools if (name === 'authenticate_user') { const result = await this.handleAuthenticateUser(args as any); this.logger.response('callTool:authenticate_user', result); return result; } if (name === 'logout') { const result = this.handleLogout(args as any); this.logger.response('callTool:logout', result); return result; } if (name === 'session_status') { const result = this.handleSessionStatus(args as any); this.logger.response('callTool:session_status', result); return result; } // List endpoints tool if (name === 'list_endpoints') { const result = this.handleListEndpoints(); this.logger.response('callTool:list_endpoints', result); return result; } // Help tool if (name === 'help') { const result = this.handleHelp(args?.topic as string); this.logger.response('callTool:help', result); return result; } // API endpoint tools if (name.startsWith('api_')) { const endpointPath = name.replace('api_', '').replace(/_/g, '/').replace(/^/, '/'); const endpoint = UMBRELLA_ENDPOINTS.find(ep => ep.path === endpointPath); if (!endpoint) { const errorResult = { content: [ { type: 'text', text: `Error: Unknown endpoint "${endpointPath}". Use "list_endpoints" to see available endpoints.`, }, ], }; this.logger.error('Unknown endpoint', { toolName: name, endpointPath }); return errorResult; } const result = await this.handleApiCall(endpoint.path, args); this.logger.response('callTool:api_call', { endpoint: endpoint.path, success: !result.content[0].text.includes('Error') }); return result; } const errorResult = { content: [ { type: 'text', text: `Error: Unknown tool "${name}". Use "help" to see available tools.`, }, ], }; this.logger.error('Unknown tool', { toolName: name }); return errorResult; } catch (error: any) { this.logger.error('Error in callTool', { toolName: name, error: error.message, stack: error.stack, arguments: args }); return { content: [ { type: 'text', text: `Error: ${error.message}`, }, ], }; } }); } private async handleAuthenticateUser(args: { username: string; password: string; sessionId?: string }) { this.logger.auth('Authentication attempt started', args.username); try { // Validate that credentials are provided if (!args.username || !args.password) { this.logger.auth('Authentication failed: Missing credentials', args.username, false); return { content: [ { type: 'text', text: `❌ Authentication failed: Both username and password are required.\n\nPlease provide your Umbrella Cost credentials:\n- Username: Your email address\n- Password: Your account password`, }, ], }; } this.logger.auth('Validating credentials with Umbrella API', args.username); const credentials: UserCredentials = { username: args.username, password: args.password }; const result = await this.sessionManager.authenticateUser(credentials, args.sessionId); if (result.success) { const stats = this.sessionManager.getStats(); this.logger.auth('Authentication successful', args.username, true); this.logger.info('Session created', { username: args.username, sessionId: result.sessionId, activeSessions: stats.activeSessions }); return { content: [ { type: 'text', text: `✅ Successfully authenticated as ${args.username}\n\n**Session ID:** ${result.sessionId}\n\nYour credentials have been securely processed and a temporary access token has been obtained.\n\nYou can now use the API tools to query your Umbrella Cost data. Use "list_endpoints" to see available endpoints.\n\n📊 **Server Status:** ${stats.activeSessions} active session(s)`, }, ], }; } else { this.logger.auth('Authentication failed', args.username, false); this.logger.error('Authentication error details', { username: args.username, error: result.error, sessionId: result.sessionId }); return { content: [ { type: 'text', text: `❌ Authentication failed: ${result.error}\n\nPlease verify your credentials and try again:\n- Ensure your username is your full email address\n- Check your password is correct\n- Confirm your account has API access permissions\n\n**Note:** Your credentials are only used to obtain a secure token and are not stored.`, }, ], }; } } catch (error: any) { this.logger.auth('Authentication exception', args.username, false); this.logger.error('Authentication exception details', { username: args.username, error: error.message, stack: error.stack }); return { content: [ { type: 'text', text: `❌ Authentication error: ${error.message}`, }, ], }; } } private handleLogout(args: { username?: string }) { this.logger.info('Logout request', { username: args.username }); try { const stats = this.sessionManager.getStats(); if (args.username) { // Logout specific user const removed = this.sessionManager.removeUserSession(args.username); if (removed) { this.logger.info('User logged out successfully', { username: args.username }); return { content: [ { type: 'text', text: `✅ Successfully logged out user: ${args.username}\n\n📊 **Server Status:** ${stats.activeSessions - 1} active session(s) remaining`, }, ], }; } else { this.logger.warn('No active session found for logout', { username: args.username }); return { content: [ { type: 'text', text: `⚠️ No active session found for user: ${args.username}`, }, ], }; } } else { // Show active sessions for admin purposes const activeSessions = this.sessionManager.getActiveSessions(); this.logger.info('Listing active sessions', { count: activeSessions.length }); if (activeSessions.length === 0) { return { content: [ { type: 'text', text: `ℹ️ No active sessions to logout.`, }, ], }; } else { let output = `📊 **Active Sessions (${activeSessions.length}):**\n\n`; activeSessions.forEach(session => { output += `- **${session.username}** (${session.id})\n`; output += ` Last activity: ${session.lastActivity.toISOString()}\n\n`; }); output += `Use \`logout(username="user@email.com")\` to logout a specific user.`; return { content: [ { type: 'text', text: output, }, ], }; } } } catch (error: any) { this.logger.error('Logout error', { username: args.username, error: error.message, stack: error.stack }); return { content: [ { type: 'text', text: `❌ Logout error: ${error.message}`, }, ], }; } } private handleSessionStatus(args: { username?: string }) { this.logger.info('Session status request', { username: args.username }); try { const stats = this.sessionManager.getStats(); if (args.username) { // Check specific user session const session = this.sessionManager.getUserSessionByUsername(args.username); if (session) { this.logger.info('Session status found', { username: args.username, sessionId: session.id }); return { content: [ { type: 'text', text: `✅ **Session Status for ${args.username}:**\n\n- **Status:** Authenticated\n- **Session ID:** ${session.id}\n- **Created:** ${session.createdAt.toISOString()}\n- **Last Activity:** ${session.lastActivity.toISOString()}\n- **Active:** Yes`, }, ], }; } else { this.logger.warn('Session not found for user', { username: args.username }); return { content: [ { type: 'text', text: `❌ **No active session found for:** ${args.username}\n\nUse \`authenticate_user\` to create a session.`, }, ], }; } } else { // Show overall server status const activeSessions = this.sessionManager.getActiveSessions(); this.logger.info('Server status requested', { totalSessions: stats.totalSessions, activeSessions: stats.activeSessions, expiredSessions: stats.expiredSessions }); let output = `📊 **Multi-Tenant Server Status:**\n\n`; output += `- **Total Sessions:** ${stats.totalSessions}\n`; output += `- **Active Sessions:** ${stats.activeSessions}\n`; output += `- **Expired Sessions:** ${stats.expiredSessions}\n\n`; if (activeSessions.length > 0) { output += `**Active Users:**\n`; activeSessions.forEach(session => { output += `- ${session.username} (active ${Math.round((new Date().getTime() - session.lastActivity.getTime()) / 1000 / 60)} min ago)\n`; }); } else { output += `**No active user sessions.**\n\nUse \`authenticate_user\` to create a session.`; } return { content: [ { type: 'text', text: output, }, ], }; } } catch (error: any) { this.logger.error('Session status error', { username: args.username, error: error.message, stack: error.stack }); return { content: [ { type: 'text', text: `❌ Session status error: ${error.message}`, }, ], }; } } private handleListEndpoints() { this.logger.info('List endpoints request'); const endpointsByCategory = UMBRELLA_ENDPOINTS.reduce((acc, endpoint) => { if (!acc[endpoint.category]) { acc[endpoint.category] = []; } acc[endpoint.category].push(endpoint); return acc; }, {} as Record<string, typeof UMBRELLA_ENDPOINTS>); let output = '# Available Umbrella Cost API Endpoints\n\n'; Object.entries(endpointsByCategory).forEach(([category, endpoints]) => { output += `## ${category}\n\n`; endpoints.forEach((endpoint) => { const toolName = `api_${endpoint.path.replace(/\//g, '_').replace(/[-]/g, '_')}`; output += `### ${toolName}\n`; output += `**Path:** \`${endpoint.method} ${endpoint.path}\`\n`; output += `**Description:** ${endpoint.description}\n`; if (endpoint.parameters && Object.keys(endpoint.parameters).length > 0) { output += `**Parameters:**\n`; Object.entries(endpoint.parameters).forEach(([param, desc]) => { output += `- \`${param}\`: ${desc}\n`; }); } output += '\n'; }); }); return { content: [ { type: 'text', text: output, }, ], }; } private handleHelp(topic?: string) { this.logger.info('Help request', { topic }); let helpText = ''; if (!topic || topic === 'general') { helpText = `# Umbrella MCP Server Help\n\n`; helpText += `This MCP server provides read-only access to the Umbrella Cost finops SaaS platform.\n\n`; helpText += `## Getting Started\n`; helpText += `1. First, authenticate using your Umbrella Cost credentials:\n`; helpText += ' ```\n authenticate_user(username="your-email@domain.com", password="your-password")\n ```\n\n'; helpText += `2. List available endpoints:\n`; helpText += ' ```\n list_endpoints()\n ```\n\n'; helpText += `3. Make API calls to retrieve your cloud cost data.\n\n`; helpText += `## Available Help Topics\n`; helpText += `- \`authentication\`: How to authenticate and manage credentials\n`; helpText += `- \`endpoints\`: Information about available API endpoints\n`; helpText += `- \`parameters\`: How to use query parameters\n\n`; } else if (topic === 'authentication') { helpText = `# Authentication Help\n\n`; helpText += `To use the Umbrella MCP server, you need to authenticate with your Umbrella Cost credentials.\n\n`; helpText += `## Credentials Setup\n`; helpText += `You need to provide your own Umbrella Cost credentials:\n\n`; helpText += `**Authentication Process:**\n`; helpText += `- Use your registered Umbrella Cost email address as username\n`; helpText += `- Use your account password\n`; helpText += `- Both MSP and Direct customer accounts are supported\n\n`; helpText += `## Authentication Process\n`; helpText += `1. Call the \`authenticate_user\` tool with your username and password\n`; helpText += `2. The server will obtain an authentication token from Umbrella Cost\n`; helpText += `3. The token will be used automatically for all subsequent API calls\n\n`; helpText += `## Security Notes\n`; helpText += `- Your credentials are only used to obtain a token and are not stored\n`; helpText += `- All API access is read-only for security\n`; helpText += `- Tokens expire after a certain time and you may need to re-authenticate\n\n`; } else if (topic === 'endpoints') { helpText = `# API Endpoints Help\n\n`; helpText += `The Umbrella MCP server provides access to various Umbrella Cost API endpoints organized by category:\n\n`; helpText += `## Categories\n`; helpText += `- **Cost Analysis**: Core cost and usage data\n`; helpText += `- **Usage Analysis**: Detailed resource usage information\n`; helpText += `- **Resource Explorer**: Resource-level cost breakdown\n`; helpText += `- **Dashboards**: Dashboard configurations\n`; helpText += `- **Budget Management**: Budget tracking and alerts\n`; helpText += `- **Recommendations**: Cost optimization suggestions\n`; helpText += `- **Anomaly Detection**: Cost anomaly identification\n`; helpText += `- **Commitment Analysis**: Reserved instances and savings plans\n`; helpText += `- **Kubernetes**: Container cost analysis\n`; helpText += `- **User Management**: User and customer information\n\n`; helpText += `Use \`list_endpoints()\` to see all available endpoints with descriptions.\n\n`; } else if (topic === 'parameters') { helpText = `# Query Parameters Help\n\n`; helpText += `Many API endpoints accept query parameters to filter and customize the results:\n\n`; helpText += `## Common Parameters\n`; helpText += `- \`startDate\`: Start date for queries (YYYY-MM-DD format)\n`; helpText += `- \`endDate\`: End date for queries (YYYY-MM-DD format)\n`; helpText += `- \`accountId\`: Filter by specific AWS account ID\n`; helpText += `- \`region\`: Filter by AWS region\n`; helpText += `- \`serviceNames\`: Filter by service names (array)\n`; helpText += `- \`limit\`: Limit the number of results returned\n`; helpText += `- \`offset\`: Skip a number of results (for pagination)\n\n`; helpText += `## Example Usage\n`; helpText += '```\n'; helpText += 'api__invoices_caui(\n'; helpText += ' startDate="2024-01-01",\n'; helpText += ' endDate="2024-01-31",\n'; helpText += ' accountId="123456789012"\n'; helpText += ')\n'; helpText += '```\n\n'; helpText += `Each endpoint may have specific parameters. Check the endpoint description for details.\n\n`; } else { helpText = `Unknown help topic: "${topic}". Available topics: general, authentication, endpoints, parameters`; } return { content: [ { type: 'text', text: helpText, }, ], }; } private async handleApiCall(path: string, args: any) { this.logger.info('API call started', { endpoint: path, argumentsKeys: args ? Object.keys(args) : [], hasUsername: !!args?.username }); try { // Determine which user to use - check if username is provided in args let username = args?.username; let session = null; if (username) { // User explicitly provided username this.logger.debug('Looking for session by username', { username }); session = this.sessionManager.getUserSessionByUsername(username); if (!session) { this.logger.warn('No session found for specified username', { username }); return { content: [ { type: 'text', text: `❌ No active session found for user: ${username}\n\nPlease authenticate first using \`authenticate_user\`.`, }, ], }; } } else { // No username provided - check if there's exactly one active session const activeSessions = this.sessionManager.getActiveSessions(); this.logger.debug('No username provided, checking active sessions', { count: activeSessions.length }); if (activeSessions.length === 0) { this.logger.warn('No authenticated users found'); return { content: [ { type: 'text', text: `❌ No authenticated users found.\n\nPlease authenticate first using \`authenticate_user(username="your-email@domain.com", password="your-password")\`.`, }, ], }; } else if (activeSessions.length === 1) { // Use the single active session session = this.sessionManager.getUserSessionByUsername(activeSessions[0].username); username = activeSessions[0].username; this.logger.debug('Using single active session', { username }); } else { // Multiple active sessions - user must specify which one this.logger.warn('Multiple active sessions found', { count: activeSessions.length }); let output = `❌ Multiple users authenticated. Please specify which user:\n\n`; output += `**Active Users:**\n`; activeSessions.forEach(s => { output += `- ${s.username}\n`; }); output += `\n**Usage:** Add \`username="user@email.com"\` to your API call.`; return { content: [ { type: 'text', text: output, }, ], }; } } if (!session) { this.logger.error('Unable to find authenticated session'); return { content: [ { type: 'text', text: `❌ Unable to find authenticated session. Please authenticate first.`, }, ], }; } // Remove username from args to avoid sending it to API const { username: _, ...apiArgs } = args || {}; // Validate and parse query parameters const queryParams: QueryParams = {}; if (apiArgs) { // Convert args to query params, validating common parameters Object.entries(apiArgs).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { (queryParams as any)[key] = value; } }); } this.logger.info('Making API request', { endpoint: path, username, sessionId: session.id, queryParams: Object.keys(queryParams) }); const validatedParams = QueryParamsSchema.partial().parse(queryParams); const response = await session.apiClient.makeRequest(path, validatedParams); this.logger.info('API request completed', { endpoint: path, username, success: response.success, hasData: !!response.data, dataType: response.data ? (Array.isArray(response.data) ? 'array' : typeof response.data) : 'none', dataLength: Array.isArray(response.data) ? response.data.length : undefined, error: response.error || 'none' }); if (response.success) { let output = `# API Response: ${path}\n\n`; output += `**Authenticated as:** ${username}\n`; output += `**Session ID:** ${session.id}\n\n`; output += `**Status:** ✅ Success\n\n`; if (response.data) { // Format the response data nicely if (Array.isArray(response.data)) { output += `**Results:** ${response.data.length} items\n\n`; if (response.data.length > 0) { output += '```json\n'; output += JSON.stringify(response.data.slice(0, 5), null, 2); // Show first 5 items if (response.data.length > 5) { output += `\n... and ${response.data.length - 5} more items\n`; } output += '\n```\n'; } } else if (typeof response.data === 'object') { output += '**Data:**\n```json\n'; output += JSON.stringify(response.data, null, 2); output += '\n```\n'; } else { output += `**Data:** ${response.data}\n`; } } return { content: [ { type: 'text', text: output, }, ], }; } else { this.logger.warn('API request failed', { endpoint: path, username, error: response.error, message: response.message }); return { content: [ { type: 'text', text: `❌ API Error for ${username}: ${response.error}\n\n${response.message || ''}`, }, ], }; } } catch (error: any) { this.logger.error('API call exception', { endpoint: path, error: error.message, stack: error.stack, args }); return { content: [ { type: 'text', text: `❌ Request Error: ${error.message}`, }, ], }; } } async run(): Promise<void> { this.logger.info('Starting MCP server transport'); try { const transport = new StdioServerTransport(); await this.server.connect(transport); this.logger.info('MCP Server started successfully', { serverName: 'Umbrella MCP Enhanced', baseURL: this.baseURL, multiTenant: true, logFile: path.join(process.cwd(), 'mcp-server.log') }); console.error(`🚀 Umbrella MCP Server started successfully (Enhanced Logging)`); console.error(`📡 Base URL: ${this.baseURL}`); console.error(`🔒 Security: Read-only access enabled`); console.error(`👥 Multi-tenant: Concurrent user sessions supported`); console.error(`📝 Logging: Enhanced logging enabled`); console.error(`📄 Log File: ${path.join(process.cwd(), 'mcp-server.log')}`); console.error(`💡 Use "authenticate_user" to get started`); // Set up graceful shutdown process.on('SIGINT', () => { this.logger.info('Received SIGINT, shutting down gracefully'); console.error('🛑 Received SIGINT, shutting down gracefully...'); this.shutdown(); process.exit(0); }); process.on('SIGTERM', () => { this.logger.info('Received SIGTERM, shutting down gracefully'); console.error('🛑 Received SIGTERM, shutting down gracefully...'); this.shutdown(); process.exit(0); }); } catch (error: any) { this.logger.error('Failed to start MCP server', { error: error.message, stack: error.stack }); throw error; } } shutdown(): void { this.logger.info('Shutting down MCP server', { activeSessions: this.sessionManager.getStats().activeSessions }); console.error('🔒 Shutting down Enhanced Umbrella MCP Server...'); this.sessionManager.shutdown(); console.error('✅ Shutdown complete'); this.logger.info('Shutdown complete'); } }

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/daviddraiumbrella/invoice-monitoring'

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