UNPKG

google-cloud-mcp

Version:

Model Context Protocol server for Google Cloud services

422 lines (418 loc) 18.1 kB
/** * Google Cloud Logging integration for MCP */ import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { Logging } from '@google-cloud/logging'; import { z } from 'zod'; import { getProjectId } from '../utils/auth.js'; import { GcpMcpError } from '../utils/error.js'; /** * Initialises the Google Cloud Logging client * * @returns A configured Logging client */ function getLoggingClient() { return new Logging({ projectId: process.env.GOOGLE_CLOUD_PROJECT }); } /** * Formats a log entry for display * * @param entry The log entry to format * @returns A formatted string representation of the log entry */ function formatLogEntry(entry) { // Safely format the timestamp let timestamp; try { // Check if timestamp exists and is valid if (!entry.timestamp) { timestamp = 'No timestamp'; } else { const date = new Date(entry.timestamp); timestamp = !isNaN(date.getTime()) ? date.toISOString() : String(entry.timestamp); } } catch (error) { console.error('Error formatting timestamp:', error, 'Timestamp value:', entry.timestamp); timestamp = String(entry.timestamp || 'Invalid timestamp'); } const severity = entry.severity || 'DEFAULT'; const resourceType = entry.resource?.type || 'unknown'; const resourceLabels = entry.resource?.labels ? Object.entries(entry.resource.labels) .map(([k, v]) => `${k}=${v}`) .join(', ') : ''; const resource = resourceLabels ? `${resourceType}(${resourceLabels})` : resourceType; // Format the payload with better error handling let payload; try { if (entry.textPayload !== undefined && entry.textPayload !== null) { payload = String(entry.textPayload); } else if (entry.jsonPayload) { payload = JSON.stringify(entry.jsonPayload, null, 2); } else if (entry.protoPayload) { payload = JSON.stringify(entry.protoPayload, null, 2); } else if (entry.data) { payload = JSON.stringify(entry.data, null, 2); } else { payload = '[No payload]'; } } catch (error) { console.error('Error formatting log payload:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; payload = `[Error formatting payload: ${errorMessage}]`; } // Format labels if they exist let labelsStr = ''; if (entry.labels && Object.keys(entry.labels).length > 0) { try { labelsStr = Object.entries(entry.labels) .map(([k, v]) => `${k}=${v}`) .join(', '); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; labelsStr = `[Error formatting labels: ${errorMessage}]`; } } // Format operation if it exists let operationStr = ''; if (entry.operation) { try { const op = entry.operation; operationStr = [ op.id ? `id=${op.id}` : '', op.producer ? `producer=${op.producer}` : '', op.first ? 'first=true' : '', op.last ? 'last=true' : '' ].filter(Boolean).join(', '); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; operationStr = `[Error formatting operation: ${errorMessage}]`; } } // Create a more detailed and markdown-friendly format return `## ${timestamp} | ${severity} | ${resource} ` + (entry.logName ? `**Log:** ${entry.logName}\n` : '') + (entry.insertId ? `**ID:** ${entry.insertId}\n` : '') + (labelsStr ? `**Labels:** ${labelsStr}\n` : '') + (operationStr ? `**Operation:** ${operationStr}\n` : '') + `\n\`\`\`\n${payload}\n\`\`\``; } /** * Registers Google Cloud Logging resources with the MCP server * * @param server The MCP server instance */ export function registerLoggingResources(server) { // Register a resource for listing recent logs server.resource('recent-logs', new ResourceTemplate('gcp-logs://{projectId}/recent', { list: undefined }), async (uri, { projectId }, _extra) => { try { const actualProjectId = projectId || await getProjectId(); const logging = getLoggingClient(); const defaultFilter = process.env.LOG_FILTER || ''; const [entries] = await logging.getEntries({ pageSize: 50, filter: defaultFilter }); if (!entries || entries.length === 0) { return { contents: [{ uri: uri.href, text: 'No log entries found.' }] }; } // Format logs with error handling for each entry const formattedLogs = entries .map((entry) => { try { return formatLogEntry(entry); } catch (err) { console.error('Error formatting log entry:', err); const errorMessage = err instanceof Error ? err.message : 'Unknown error'; return `## Error Formatting Log Entry\n\nAn error occurred while formatting a log entry: ${errorMessage}`; } }) .join('\n\n'); return { contents: [{ uri: uri.href, text: `# Recent Logs for Project: ${actualProjectId}\n\n${formattedLogs}` }] }; } catch (error) { console.error('Error fetching recent logs:', error); // Get project ID safely let projectIdForError; try { projectIdForError = Array.isArray(projectId) ? projectId[0] : (projectId || process.env.GOOGLE_CLOUD_PROJECT || 'unknown'); } catch { projectIdForError = 'unknown'; } const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Return a user-friendly error message instead of throwing return { contents: [{ uri: uri.href, text: `# Error Fetching Recent Logs\n\nAn error occurred while fetching recent logs for project ${projectIdForError}: ${errorMessage}\n\nPlease check your Google Cloud credentials and project configuration.` }] }; } }); // Register a resource for querying logs with a filter server.resource('filtered-logs', new ResourceTemplate('gcp-logs://{projectId}/filter/{filter}', { list: undefined }), async (uri, { projectId, filter }, _extra) => { try { const actualProjectId = projectId || await getProjectId(); const logging = getLoggingClient(); if (!filter) { throw new GcpMcpError('Log filter is required', 'INVALID_ARGUMENT', 400); } const decodedFilter = Array.isArray(filter) ? decodeURIComponent(filter[0]) : decodeURIComponent(filter); const [entries] = await logging.getEntries({ pageSize: 50, filter: decodedFilter }); if (!entries || entries.length === 0) { return { contents: [{ uri: uri.href, text: `No log entries found matching filter: ${decodedFilter}` }] }; } // Format logs with error handling for each entry const formattedLogs = entries .map((entry) => { try { return formatLogEntry(entry); } catch (err) { console.error('Error formatting log entry:', err); const errorMessage = err instanceof Error ? err.message : 'Unknown error'; return `## Error Formatting Log Entry\n\nAn error occurred while formatting a log entry: ${errorMessage}`; } }) .join('\n\n'); return { contents: [{ uri: uri.href, text: `# Filtered Logs for Project: ${actualProjectId}\n\nFilter: ${decodedFilter}\n\n${formattedLogs}` }] }; } catch (error) { console.error('Error fetching filtered logs:', error); // Get project ID and filter safely let projectIdForError; let filterForError; try { projectIdForError = Array.isArray(projectId) ? projectId[0] : (projectId || process.env.GOOGLE_CLOUD_PROJECT || 'unknown'); filterForError = Array.isArray(filter) ? filter[0] : String(filter || 'unknown'); } catch { projectIdForError = 'unknown'; filterForError = 'unknown'; } const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Return a user-friendly error message instead of throwing return { contents: [{ uri: uri.href, text: `# Error Fetching Filtered Logs\n\nAn error occurred while fetching logs with filter "${filterForError}" for project ${projectIdForError}: ${errorMessage}\n\nPlease check your filter syntax and Google Cloud credentials.` }] }; } }); } /** * Registers Google Cloud Logging tools with the MCP server * * @param server The MCP server instance */ export function registerLoggingTools(server) { // Tool to query logs with a custom filter server.tool('query-logs', { filter: z.string().describe('The filter to apply to logs'), limit: z.number().min(1).max(1000).default(50).describe('Maximum number of log entries to return') }, async ({ filter, limit }, _extra) => { try { const projectId = await getProjectId(); const logging = getLoggingClient(); const [entries] = await logging.getEntries({ pageSize: limit, filter }); if (!entries || entries.length === 0) { return { content: [{ type: 'text', text: `No log entries found matching filter: ${filter}` }] }; } const formattedLogs = entries .map((entry) => { try { return formatLogEntry(entry); } catch (err) { console.error('Error formatting log entry:', err); const errorMessage = err instanceof Error ? err.message : 'Unknown error'; return `## Error Formatting Log Entry\n\nAn error occurred while formatting a log entry: ${errorMessage}`; } }) .join('\n\n'); return { content: [{ type: 'text', text: `# Log Query Results\n\nProject: ${projectId}\nFilter: ${filter}\nEntries: ${entries.length}\n\n${formattedLogs}` }] }; } catch (error) { console.error('Error querying logs:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Return a user-friendly error message instead of throwing return { content: [{ type: 'text', text: `# Error Querying Logs An error occurred while querying logs: ${errorMessage} Please check your filter syntax and try again. For filter syntax help, see: https://cloud.google.com/logging/docs/view/logging-query-language` }], isError: true }; } }); // Tool to get logs for a specific time range server.tool('logs-time-range', { startTime: z.string().describe('Start time in ISO format or relative time (e.g., "1h", "2d")'), endTime: z.string().optional().describe('End time in ISO format (defaults to now)'), filter: z.string().optional().describe('Additional filter criteria'), limit: z.number().min(1).max(1000).default(50).describe('Maximum number of log entries to return') }, async ({ startTime, endTime, filter, limit }, _extra) => { try { const projectId = await getProjectId(); const logging = getLoggingClient(); // Parse relative time expressions like "1h", "2d" const parseRelativeTime = (timeStr) => { try { // Handle relative time expressions if (typeof timeStr === 'string' && timeStr.match(/^\d+[hdwmy]$/i)) { const now = new Date(); const value = parseInt(timeStr.slice(0, -1)); const unit = timeStr.slice(-1).toLowerCase(); if (isNaN(value)) { throw new GcpMcpError(`Invalid time value in: ${timeStr}`, 'INVALID_ARGUMENT', 400); } const result = new Date(now); switch (unit) { case 'h': // hours result.setHours(result.getHours() - value); break; case 'd': // days result.setDate(result.getDate() - value); break; case 'w': // weeks result.setDate(result.getDate() - (value * 7)); break; case 'm': // months result.setMonth(result.getMonth() - value); break; case 'y': // years result.setFullYear(result.getFullYear() - value); break; default: throw new GcpMcpError(`Invalid time unit: ${unit}`, 'INVALID_ARGUMENT', 400); } return result; } // Try to parse as ISO date const date = new Date(timeStr); if (isNaN(date.getTime())) { throw new GcpMcpError(`Invalid time format: ${timeStr}`, 'INVALID_ARGUMENT', 400); } return date; } catch (error) { if (error instanceof GcpMcpError) { throw error; } const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new GcpMcpError(`Error parsing time: ${timeStr} - ${errorMessage}`, 'INVALID_ARGUMENT', 400); } }; const start = parseRelativeTime(startTime); const end = endTime ? parseRelativeTime(endTime) : new Date(); // Build filter string let filterStr = `timestamp >= "${start.toISOString()}" AND timestamp <= "${end.toISOString()}"`; if (filter) { filterStr = `${filterStr} AND ${filter}`; } const [entries] = await logging.getEntries({ pageSize: limit, filter: filterStr }); if (!entries || entries.length === 0) { return { content: [{ type: 'text', text: `No log entries found in the specified time range with filter: ${filterStr}` }] }; } const formattedLogs = entries .map((entry) => { try { return formatLogEntry(entry); } catch (err) { console.error('Error formatting log entry:', err); const errorMessage = err instanceof Error ? err.message : 'Unknown error'; return `## Error Formatting Log Entry\n\nAn error occurred while formatting a log entry: ${errorMessage}`; } }) .join('\n\n'); return { content: [{ type: 'text', text: `# Log Time Range Results\n\nProject: ${projectId}\nTime Range: ${start.toISOString()} to ${end.toISOString()}\nFilter: ${filter || 'None'}\nEntries: ${entries.length}\n\n${formattedLogs}` }] }; } catch (error) { console.error('Error querying logs with time range:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Return a user-friendly error message instead of throwing return { content: [{ type: 'text', text: `# Error Querying Logs An error occurred while querying logs: ${errorMessage} Please check your time range format and try again. Valid formats include: - ISO date strings (e.g., "2025-03-01T00:00:00Z") - Relative time expressions: "1h" (1 hour ago), "2d" (2 days ago), "1w" (1 week ago), etc.` }], isError: true }; } }); } //# sourceMappingURL=logging.js.map