Skip to main content
Glama

Google Cloud MCP Server

by andyl25
resources.ts16.5 kB
/** * Google Cloud Trace resources for MCP */ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; import { getProjectId, initGoogleAuth } from '../../utils/auth.js'; import { GcpMcpError } from '../../utils/error.js'; import { buildTraceHierarchy, formatTraceData } from './types.js'; import { LogEntry } from '../logging/types.js'; import { logger } from '../../utils/logger.js'; /** * Registers Google Cloud Trace resources with the MCP server * * @param server The MCP server instance */ export function registerTraceResources(server: McpServer): void { // Resource to get a specific trace by ID server.resource( 'trace-by-id', new ResourceTemplate('gcp-trace://{projectId}/traces/{traceId}', { list: undefined }), async (uri, { projectId, traceId }, context) => { try { // Use provided project ID or get the default one const actualProjectId = projectId || await getProjectId(); // Validate trace ID format (hex string) if (typeof traceId === 'string' && !traceId.match(/^[a-f0-9]+$/i)) { throw new GcpMcpError( 'Invalid trace ID format. Trace ID should be a hexadecimal string.', 'INVALID_ARGUMENT', 400 ); } // Initialize Google Auth client const auth = await initGoogleAuth(true); if (!auth) { throw new GcpMcpError( 'Google Cloud authentication not available. Please configure authentication to access trace data.', 'UNAUTHENTICATED', 401 ); } const client = await auth.getClient(); const token = await client.getAccessToken(); // Fetch the trace from the Cloud Trace API const response = await fetch( `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces/${traceId}`, { method: 'GET', headers: { 'Authorization': `Bearer ${token.token}`, 'Accept': 'application/json' } } ); if (!response.ok) { const errorText = await response.text(); throw new GcpMcpError( `Failed to fetch trace: ${errorText}`, 'FAILED_PRECONDITION', response.status ); } const traceData = await response.json(); if (!traceData || !traceData.spans || traceData.spans.length === 0) { return { contents: [{ uri: uri.href, text: `# Trace Not Found\n\nNo trace found with ID: ${traceId} in project: ${actualProjectId}` }] }; } // Build the trace hierarchy const hierarchicalTrace = buildTraceHierarchy( actualProjectId.toString(), traceId.toString(), traceData.spans ); // Format the trace data for display const formattedTrace = formatTraceData(hierarchicalTrace); return { contents: [{ uri: uri.href, text: `# Trace Details for ${traceId}\n\n${formattedTrace}` }] }; } catch (error: any) { // Error handling for trace-by-id resource throw new GcpMcpError( `Failed to fetch trace: ${error.message}`, error.code || 'UNKNOWN', error.statusCode || 500 ); } } ); // Resource to get logs associated with a trace server.resource( 'trace-logs', new ResourceTemplate('gcp-trace://{projectId}/traces/{traceId}/logs', { list: undefined }), async (uri, { projectId, traceId }, context) => { try { // Use provided project ID or get the default one const actualProjectId = projectId || await getProjectId(); // Validate trace ID format (hex string) if (typeof traceId === 'string' && !traceId.match(/^[a-f0-9]+$/i)) { throw new GcpMcpError( 'Invalid trace ID format. Trace ID should be a hexadecimal string.', 'INVALID_ARGUMENT', 400 ); } // Import the Logging client const { Logging } = await import('@google-cloud/logging'); // Initialize the logging client const logging = new Logging({ projectId: typeof actualProjectId === 'string' ? actualProjectId : undefined }); // Create a more flexible filter that searches for the trace ID in multiple fields const traceFilter = ` (trace="projects/${actualProjectId}/traces/${traceId}" OR jsonPayload.logging.googleapis.com/trace="projects/${actualProjectId}/traces/${traceId}" OR labels."logging.googleapis.com/trace_id"="${traceId}" OR labels."trace_id"="${traceId}" OR jsonPayload.trace_id="${traceId}") `.replace(/\s+/g, ' ').trim(); // Fetch logs with the flexible filter const [entries] = await logging.getEntries({ pageSize: 50, filter: traceFilter }); if (!entries || entries.length === 0) { return { contents: [{ uri: uri.href, text: `# No Logs Found for Trace No log entries found associated with trace ID: ${traceId} in project: ${actualProjectId} Filter used: ${traceFilter}` }] }; } // Format logs let formattedLogs = ''; for (const entry of entries) { // Cast the entry to our LogEntry type to access properties safely const logEntry = entry as unknown as LogEntry; // Safely extract timestamp let timestamp = 'Unknown time'; if (logEntry.timestamp) { try { const date = new Date(logEntry.timestamp); if (!isNaN(date.getTime())) { timestamp = date.toISOString(); } } catch (e) { // Keep default timestamp if parsing fails } } const severity = logEntry.severity || 'DEFAULT'; const message = logEntry.jsonPayload && typeof logEntry.jsonPayload === 'object' && 'message' in logEntry.jsonPayload ? String(logEntry.jsonPayload.message) : logEntry.textPayload || JSON.stringify(logEntry.jsonPayload || {}); formattedLogs += `## Log Entry (${severity}) - ${timestamp}\n\n`; formattedLogs += `${message}\n\n`; // Add resource information if available if (logEntry.resource && typeof logEntry.resource === 'object') { const resource = logEntry.resource as { type?: string; labels?: Record<string, string> }; if (resource.type) { formattedLogs += `**Resource**: ${resource.type}\n`; } if (resource.labels && typeof resource.labels === 'object') { formattedLogs += `**Labels**:\n`; for (const [key, value] of Object.entries(resource.labels)) { formattedLogs += `- ${key}: ${value}\n`; } } formattedLogs += '\n'; } // Add log-specific labels if available if (logEntry.labels && typeof logEntry.labels === 'object' && Object.keys(logEntry.labels).length > 0) { formattedLogs += `**Log Labels**:\n`; for (const [key, value] of Object.entries(logEntry.labels)) { formattedLogs += `- ${key}: ${value}\n`; } formattedLogs += '\n'; } formattedLogs += '---\n\n'; } return { contents: [{ uri: uri.href, text: `# Logs for Trace: ${traceId}\n\nProject: ${actualProjectId}\nTotal Log Entries: ${entries.length}\n\n${formattedLogs}` }] }; } catch (error: any) { // Error handling throw new GcpMcpError( `Failed to fetch logs for trace: ${error.message}`, error.code || 'UNKNOWN', error.statusCode || 500 ); } } ); // Resource to list recent failed traces server.resource( 'recent-failed-traces', new ResourceTemplate('gcp-trace://{projectId}/recent-failed', { list: undefined }), async (uri, { projectId }, context) => { try { // Use provided project ID or get the default one const actualProjectId = projectId || await getProjectId(); // Calculate time range (last 1 hour) const endTime = new Date(); let startTime = new Date(endTime.getTime() - 60 * 60 * 1000); // 1 hour ago // Check if we have a startTime parameter in the URI query const uriParams = new URL(uri.href).searchParams; const startTimeParam = uriParams.get('startTime'); if (startTimeParam) { logger.debug(`Found startTime parameter in URI: ${startTimeParam}`); // Check if it's a relative time format (e.g., "1h", "2d", "30m") const relativeTimeRegex = /^(\d+)([hmd])$/; const match = startTimeParam.match(relativeTimeRegex); if (match) { // Parse relative time (e.g., "1h", "2d") const value = parseInt(match[1]); const unit = match[2]; startTime = new Date(endTime); if (unit === 'h') { startTime.setHours(startTime.getHours() - value); } else if (unit === 'd') { startTime.setDate(startTime.getDate() - value); } else if (unit === 'm') { startTime.setMinutes(startTime.getMinutes() - value); } logger.debug(`Parsed relative time: ${startTimeParam} to ${startTime.toISOString()}`); } else { // Try to parse as ISO format try { const parsedDate = new Date(startTimeParam); if (!isNaN(parsedDate.getTime())) { startTime = parsedDate; logger.debug(`Parsed ISO time: ${startTimeParam} to ${startTime.toISOString()}`); } else { logger.error(`Invalid date format: ${startTimeParam}, using default`); } } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error(`Error parsing time: ${errorMessage}, using default`); } } } // Build the filter for failed traces const filter = { projectId: actualProjectId, startTime: startTime.toISOString(), endTime: endTime.toISOString(), filter: 'status.code != 0', // Non-zero status codes indicate errors pageSize: 10 // Limit to 10 traces }; // Initialize Google Auth client const auth = await initGoogleAuth(true); if (!auth) { throw new GcpMcpError( 'Google Cloud authentication not available. Please configure authentication to access trace data.', 'UNAUTHENTICATED', 401 ); } const client = await auth.getClient(); const token = await client.getAccessToken(); // Build the query parameters for the request const queryParams = new URLSearchParams(); // Format timestamps in RFC3339 UTC "Zulu" format as required by the API // Example format: "2014-10-02T15:01:23Z" const startTimeUTC = new Date(filter.startTime).toISOString(); const endTimeUTC = new Date(filter.endTime).toISOString(); // Build the query parameters for the request according to the API documentation // Required parameters queryParams.append('startTime', startTimeUTC); // Start of the time interval (inclusive) queryParams.append('endTime', endTimeUTC); // End of the time interval (inclusive) // Optional parameters queryParams.append('pageSize', filter.pageSize.toString()); // Maximum number of traces to return queryParams.append('filter', filter.filter); // Filter for failed traces (status.code != 0) queryParams.append('view', 'MINIMAL'); // Type of data returned (MINIMAL, ROOTSPAN, COMPLETE) // Construct the URL for the Cloud Trace API v1 endpoint // The correct endpoint format according to the documentation is: // GET https://cloudtrace.googleapis.com/v1/projects/{projectId}/traces const apiUrl = `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces`; const requestUrl = `${apiUrl}?${queryParams.toString()}`; logger.debug(`Recent Failed Traces URL: ${requestUrl}`); logger.debug(`Recent Failed Traces Query Params: ${JSON.stringify(Object.fromEntries(queryParams.entries()))}`); // Fetch failed traces from the Cloud Trace API using the correct v1 endpoint const response = await fetch( requestUrl, { method: 'GET', headers: { 'Authorization': `Bearer ${token.token}`, 'Accept': 'application/json' } } ); logger.debug(`Recent Failed Traces Response Status: ${response.status}`); if (!response.ok) { const errorText = await response.text(); logger.error(`Recent Failed Traces Error: ${errorText}`); throw new GcpMcpError( `Failed to fetch failed traces: ${errorText}`, 'FAILED_PRECONDITION', response.status ); } const tracesData = await response.json(); if (!tracesData.traces || tracesData.traces.length === 0) { return { contents: [{ uri: uri.href, text: `# Recent Failed Traces\n\nNo failed traces found in the last hour for project: ${actualProjectId}` }] }; } // Format the traces for display let markdown = `# Recent Failed Traces\n\n`; markdown += `Project: ${actualProjectId}\n`; markdown += `Time Range: ${startTime.toISOString()} to ${endTime.toISOString()}\n\n`; markdown += `Found ${tracesData.traces.length} failed traces:\n\n`; // Table header markdown += '| Trace ID | Display Name | Start Time | Duration | Error |\n'; markdown += '|----------|--------------|------------|----------|-------|\n'; // Table rows for (const trace of tracesData.traces) { const traceId = trace.traceId; const displayName = trace.displayName || 'Unknown'; const startTimeStr = new Date(trace.startTime).toISOString(); const duration = calculateDuration(trace.startTime, trace.endTime); const error = trace.status?.message || 'Unknown error'; markdown += `| [${traceId}](gcp-trace://${actualProjectId}/traces/${traceId}) | ${displayName} | ${startTimeStr} | ${duration} | ${error} |\n`; } return { contents: [{ uri: uri.href, text: markdown }] }; } catch (error: any) { // Error handling for recent-failed-traces resource throw new GcpMcpError( `Failed to fetch failed traces: ${error.message}`, error.code || 'UNKNOWN', error.statusCode || 500 ); } } ); } /** * Calculates the duration between two timestamps * * @param startTime The start time * @param endTime The end time * @returns Formatted duration string */ function calculateDuration(startTime: string, endTime: string): string { const start = new Date(startTime).getTime(); const end = new Date(endTime).getTime(); const durationMs = end - start; if (durationMs < 1000) { return `${durationMs}ms`; } else if (durationMs < 60000) { return `${(durationMs / 1000).toFixed(2)}s`; } else { const minutes = Math.floor(durationMs / 60000); const seconds = ((durationMs % 60000) / 1000).toFixed(2); return `${minutes}m ${seconds}s`; } }

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/andyl25/googlecloud-mcp'

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