Skip to main content
Glama
andyl25

Google Cloud MCP Server

by andyl25

get-trace

Retrieve Google Cloud Trace data by trace ID to analyze request paths and diagnose performance issues in distributed systems.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
traceIdYesThe trace ID to retrieve
projectIdNoOptional Google Cloud project ID

Implementation Reference

  • Core handler function for 'get-trace' tool. Fetches trace data from Google Cloud Trace API v1 using the provided traceId, validates input, authenticates with Google Auth, builds trace hierarchy using helper, formats as markdown, and returns structured content response.
    async ({ traceId, projectId }, context) => {
      try {
        // Use provided project ID or get the default one from state manager first
        const actualProjectId = projectId || stateManager.getCurrentProjectId() || 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 v1
        // API Reference: https://cloud.google.com/trace/docs/reference/v1/rest/v1/projects.traces/get
        const apiUrl = `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces/${traceId}`;
        logger.debug(`Fetching trace from: ${apiUrl}`);
        const response = await fetch(
          apiUrl,
          {
            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();
        
        // Log the raw trace data for debugging
        logger.debug(`Raw trace data: ${JSON.stringify(traceData, null, 2)}`);
        
        // Debug: Log the exact structure of the trace data
        logger.debug('Trace data structure:');
        logger.debug(`- Type: ${typeof traceData}`);
        logger.debug(`- Keys: ${Object.keys(traceData).join(', ')}`);
        logger.debug(`- Has spans array: ${Array.isArray(traceData.spans)}`);
        logger.debug(`- Spans array length: ${traceData.spans?.length || 0}`);
        
        // Check if we have valid trace data
        // In v1 API, the response is a Trace object with spans array
        if (!traceData || !traceData.spans || traceData.spans.length === 0) {
          return {
            content: [{
              type: 'text',
              text: `No trace found with ID: ${traceId} in project: ${actualProjectId}`
            }]
          };
        }
        
        // Log the trace structure for debugging
        logger.debug(`Trace structure: projectId=${traceData.projectId}, traceId=${traceData.traceId}, spans count=${traceData.spans.length}`);
        
        // Log the first span to help with debugging
        if (traceData.spans && traceData.spans.length > 0) {
          const firstSpan = traceData.spans[0];
          logger.debug(`First span example: ${JSON.stringify(firstSpan, null, 2)}`);
          logger.debug(`First span fields: ${Object.keys(firstSpan).join(', ')}`);
          
          // Debug: Log specific fields that we're looking for in the span
          logger.debug('Span field details:');
          logger.debug(`- spanId: ${firstSpan.spanId}`);
          logger.debug(`- name: ${firstSpan.name}`);
          logger.debug(`- displayName: ${firstSpan.displayName}`);
          logger.debug(`- startTime: ${firstSpan.startTime}`);
          logger.debug(`- endTime: ${firstSpan.endTime}`);
          logger.debug(`- parentSpanId: ${firstSpan.parentSpanId}`);
          logger.debug(`- kind: ${firstSpan.kind}`);
          logger.debug(`- Has labels: ${!!firstSpan.labels}`);
          
          if (firstSpan.labels) {
            logger.debug(`- Label keys: ${Object.keys(firstSpan.labels).join(', ')}`);
            logger.debug(`- HTTP path label: ${firstSpan.labels['/http/path']}`);
            logger.debug(`- HTTP method label: ${firstSpan.labels['/http/method']}`);
            logger.debug(`- Component label: ${firstSpan.labels['/component']}`);
          }
        }
        
        // Add additional metadata to the response for better context
        let responseText = `\`\`\`json\n${JSON.stringify({
          traceId: traceId,
          projectId: actualProjectId,
          spanCount: traceData.spans.length
        }, null, 2)}\n\`\`\`\n`;
        
        // Log the number of spans found
        logger.debug(`Found ${traceData.spans.length} spans in trace ${traceId}`);
        
        try {
          logger.debug('Starting to build trace hierarchy...');
          
          // Debug: Log each span before processing
          traceData.spans.forEach((span: any, index: number) => {
            logger.debug(`Span ${index} (ID: ${span.spanId}):`);
            logger.debug(`- Name: ${span.name || 'undefined'}`);
            logger.debug(`- Parent: ${span.parentSpanId || 'None'}`);
            logger.debug(`- Has labels: ${!!span.labels}`);
            if (span.labels) {
              logger.debug(`- Label count: ${Object.keys(span.labels).length}`);
            }
          });
          
          // Build the trace hierarchy
          const hierarchicalTrace = buildTraceHierarchy(
            actualProjectId.toString(),
            traceId.toString(),
            traceData.spans
          );
          
          logger.debug('Trace hierarchy built successfully');
          logger.debug(`Root spans count: ${hierarchicalTrace.rootSpans.length}`);
          
          // Format the trace data for display
          logger.debug('Formatting trace data...');
          const formattedTrace = formatTraceData(hierarchicalTrace);
          
          // Combine the response text with the formatted trace
          responseText += formattedTrace;
          
          logger.debug('Trace formatting complete');
        } catch (hierarchyError: any) {
          // If we encounter an error building the hierarchy, log it and provide raw span info
          logger.error(`Error building trace hierarchy: ${hierarchyError.message}`);
          
          // Provide a simplified trace summary
          responseText += '## Error Building Trace Hierarchy\n\n';
          responseText += `Error: ${hierarchyError.message}\n\n`;
          responseText += '## Raw Span Summary\n\n';
          
          // List spans with basic information
          for (const span of traceData.spans) {
            const spanId = span.spanId || 'Unknown';
            const name = span.name || 'Unknown';
            const parentId = span.parentSpanId || 'None';
            
            responseText += `- **Span ID**: ${spanId}\n`;
            responseText += `  - Name: ${name}\n`;
            responseText += `  - Parent: ${parentId}\n`;
            
            // Add timing if available
            if (span.startTime && span.endTime) {
              const startDate = new Date(span.startTime);
              const endDate = new Date(span.endTime);
              const durationMs = endDate.getTime() - startDate.getTime();
              responseText += `  - Duration: ${durationMs}ms\n`;
            }
            
            // Add a few important labels if available
            if (span.labels) {
              responseText += `  - Labels: ${Object.keys(span.labels).length} total\n`;
              const importantLabels = ['/http/method', '/http/path', '/http/status_code', '/component', 'g.co/agent'];
              for (const key of importantLabels) {
                if (span.labels[key]) {
                  responseText += `    - ${key}: ${span.labels[key]}\n`;
                }
              }
            }
            
            responseText += '\n';
          }
        }
        
        return {
          content: [{
            type: 'text',
            text: responseText
          }]
        };
      } catch (error: any) {
        // Error handling for get-trace tool
        throw new GcpMcpError(
          `Failed to fetch trace: ${error.message}`,
          error.code || 'UNKNOWN',
          error.statusCode || 500
        );
      }
    }
  • Zod input schema for the get-trace tool defining required traceId (hex string) and optional projectId.
      traceId: z.string().describe('The trace ID to retrieve'),
      projectId: z.string().optional().describe('Optional Google Cloud project ID')
    },
  • MCP server.tool registration for the 'get-trace' tool, including input schema and handler function reference within registerTraceTools.
    server.tool(
      'get-trace',
      {
        traceId: z.string().describe('The trace ID to retrieve'),
        projectId: z.string().optional().describe('Optional Google Cloud project ID')
      },
      async ({ traceId, projectId }, context) => {
        try {
          // Use provided project ID or get the default one from state manager first
          const actualProjectId = projectId || stateManager.getCurrentProjectId() || 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 v1
          // API Reference: https://cloud.google.com/trace/docs/reference/v1/rest/v1/projects.traces/get
          const apiUrl = `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces/${traceId}`;
          logger.debug(`Fetching trace from: ${apiUrl}`);
          const response = await fetch(
            apiUrl,
            {
              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();
          
          // Log the raw trace data for debugging
          logger.debug(`Raw trace data: ${JSON.stringify(traceData, null, 2)}`);
          
          // Debug: Log the exact structure of the trace data
          logger.debug('Trace data structure:');
          logger.debug(`- Type: ${typeof traceData}`);
          logger.debug(`- Keys: ${Object.keys(traceData).join(', ')}`);
          logger.debug(`- Has spans array: ${Array.isArray(traceData.spans)}`);
          logger.debug(`- Spans array length: ${traceData.spans?.length || 0}`);
          
          // Check if we have valid trace data
          // In v1 API, the response is a Trace object with spans array
          if (!traceData || !traceData.spans || traceData.spans.length === 0) {
            return {
              content: [{
                type: 'text',
                text: `No trace found with ID: ${traceId} in project: ${actualProjectId}`
              }]
            };
          }
          
          // Log the trace structure for debugging
          logger.debug(`Trace structure: projectId=${traceData.projectId}, traceId=${traceData.traceId}, spans count=${traceData.spans.length}`);
          
          // Log the first span to help with debugging
          if (traceData.spans && traceData.spans.length > 0) {
            const firstSpan = traceData.spans[0];
            logger.debug(`First span example: ${JSON.stringify(firstSpan, null, 2)}`);
            logger.debug(`First span fields: ${Object.keys(firstSpan).join(', ')}`);
            
            // Debug: Log specific fields that we're looking for in the span
            logger.debug('Span field details:');
            logger.debug(`- spanId: ${firstSpan.spanId}`);
            logger.debug(`- name: ${firstSpan.name}`);
            logger.debug(`- displayName: ${firstSpan.displayName}`);
            logger.debug(`- startTime: ${firstSpan.startTime}`);
            logger.debug(`- endTime: ${firstSpan.endTime}`);
            logger.debug(`- parentSpanId: ${firstSpan.parentSpanId}`);
            logger.debug(`- kind: ${firstSpan.kind}`);
            logger.debug(`- Has labels: ${!!firstSpan.labels}`);
            
            if (firstSpan.labels) {
              logger.debug(`- Label keys: ${Object.keys(firstSpan.labels).join(', ')}`);
              logger.debug(`- HTTP path label: ${firstSpan.labels['/http/path']}`);
              logger.debug(`- HTTP method label: ${firstSpan.labels['/http/method']}`);
              logger.debug(`- Component label: ${firstSpan.labels['/component']}`);
            }
          }
          
          // Add additional metadata to the response for better context
          let responseText = `\`\`\`json\n${JSON.stringify({
            traceId: traceId,
            projectId: actualProjectId,
            spanCount: traceData.spans.length
          }, null, 2)}\n\`\`\`\n`;
          
          // Log the number of spans found
          logger.debug(`Found ${traceData.spans.length} spans in trace ${traceId}`);
          
          try {
            logger.debug('Starting to build trace hierarchy...');
            
            // Debug: Log each span before processing
            traceData.spans.forEach((span: any, index: number) => {
              logger.debug(`Span ${index} (ID: ${span.spanId}):`);
              logger.debug(`- Name: ${span.name || 'undefined'}`);
              logger.debug(`- Parent: ${span.parentSpanId || 'None'}`);
              logger.debug(`- Has labels: ${!!span.labels}`);
              if (span.labels) {
                logger.debug(`- Label count: ${Object.keys(span.labels).length}`);
              }
            });
            
            // Build the trace hierarchy
            const hierarchicalTrace = buildTraceHierarchy(
              actualProjectId.toString(),
              traceId.toString(),
              traceData.spans
            );
            
            logger.debug('Trace hierarchy built successfully');
            logger.debug(`Root spans count: ${hierarchicalTrace.rootSpans.length}`);
            
            // Format the trace data for display
            logger.debug('Formatting trace data...');
            const formattedTrace = formatTraceData(hierarchicalTrace);
            
            // Combine the response text with the formatted trace
            responseText += formattedTrace;
            
            logger.debug('Trace formatting complete');
          } catch (hierarchyError: any) {
            // If we encounter an error building the hierarchy, log it and provide raw span info
            logger.error(`Error building trace hierarchy: ${hierarchyError.message}`);
            
            // Provide a simplified trace summary
            responseText += '## Error Building Trace Hierarchy\n\n';
            responseText += `Error: ${hierarchyError.message}\n\n`;
            responseText += '## Raw Span Summary\n\n';
            
            // List spans with basic information
            for (const span of traceData.spans) {
              const spanId = span.spanId || 'Unknown';
              const name = span.name || 'Unknown';
              const parentId = span.parentSpanId || 'None';
              
              responseText += `- **Span ID**: ${spanId}\n`;
              responseText += `  - Name: ${name}\n`;
              responseText += `  - Parent: ${parentId}\n`;
              
              // Add timing if available
              if (span.startTime && span.endTime) {
                const startDate = new Date(span.startTime);
                const endDate = new Date(span.endTime);
                const durationMs = endDate.getTime() - startDate.getTime();
                responseText += `  - Duration: ${durationMs}ms\n`;
              }
              
              // Add a few important labels if available
              if (span.labels) {
                responseText += `  - Labels: ${Object.keys(span.labels).length} total\n`;
                const importantLabels = ['/http/method', '/http/path', '/http/status_code', '/component', 'g.co/agent'];
                for (const key of importantLabels) {
                  if (span.labels[key]) {
                    responseText += `    - ${key}: ${span.labels[key]}\n`;
                  }
                }
              }
              
              responseText += '\n';
            }
          }
          
          return {
            content: [{
              type: 'text',
              text: responseText
            }]
          };
        } catch (error: any) {
          // Error handling for get-trace tool
          throw new GcpMcpError(
            `Failed to fetch trace: ${error.message}`,
            error.code || 'UNKNOWN',
            error.statusCode || 500
          );
        }
      }
    );
  • buildTraceHierarchy helper: Processes raw spans from Trace API into hierarchical TraceData with rootSpans and nested childSpans, handling v1 API formats, labels to attributes, status detection.
    export function buildTraceHierarchy(
      projectId: string,
      traceId: string,
      spans: any[]
    ): TraceData {
      // Log the raw spans for debugging
      logger.debug(`Building trace hierarchy for trace ${traceId} with ${spans.length} spans`);
      
      // Debug: Log the structure of the first span to understand the format
      if (spans.length > 0) {
        logger.debug(`First span structure: ${JSON.stringify(spans[0], null, 2)}`);
        logger.debug(`First span keys: ${Object.keys(spans[0]).join(', ')}`);
      }
      
      // We're using the v1 API which returns spans in a consistent format
      
      // Map to store spans by ID for quick lookup
      const spanMap = new Map<string, TraceSpan>();
      
      // Convert raw spans to our TraceSpan format
      logger.debug('Converting spans to TraceSpan format...');
      const traceSpans: TraceSpan[] = spans.map((span, index) => {
        // Extract span data - ensure we have a valid spanId
        const spanId = span.spanId || '';
        logger.debug(`Processing span ${index} with ID: ${spanId}`);
        
        // Handle different display name formats in v1 API
        // According to https://cloud.google.com/trace/docs/reference/v1/rest/v1/projects.traces#TraceSpan
        let displayName = 'Unknown Span';
        
        // Debug: Log the name-related fields
        logger.debug(`Span ${spanId} name fields:`);
        logger.debug(`- name: ${span.name || 'undefined'}`);
        logger.debug(`- displayName: ${typeof span.displayName === 'object' ? JSON.stringify(span.displayName) : span.displayName || 'undefined'}`);
        
        // In v1 API, the name field is the actual span name
        if (span.name) {
          displayName = span.name;
          logger.debug(`Using span.name: ${displayName}`);
        } else if (span.displayName?.value) {
          displayName = span.displayName.value;
          logger.debug(`Using span.displayName.value: ${displayName}`);
        } else if (typeof span.displayName === 'string') {
          displayName = span.displayName;
          logger.debug(`Using span.displayName string: ${displayName}`);
        }
        
        // For v1 API, check if the name is a full URL path (common in Google Cloud Trace)
        if (displayName.startsWith('/')) {
          // This is likely an HTTP path
          displayName = `HTTP ${displayName}`;
        }
        
        // If we still have an unknown span, try to extract a meaningful name from labels
        if (displayName === 'Unknown Span') {
          // Try to extract from common label patterns in Google Cloud Trace v1 API
          if (span.labels) {
            // Common Google Cloud Trace labels
            if (span.labels['/http/path']) {
              const method = span.labels['/http/method'] || '';
              displayName = `${method} ${span.labels['/http/path']}`;
            } else if (span.labels['/http/url']) {
              displayName = `HTTP ${span.labels['/http/url']}`;
            } else if (span.labels['/http/status_code']) {
              displayName = `HTTP Status: ${span.labels['/http/status_code']}`;
            } else if (span.labels['/component']) {
              displayName = `Component: ${span.labels['/component']}`;
            } else if (span.labels['/db/statement']) {
              const dbSystem = span.labels['/db/system'] || 'DB';
              displayName = `${dbSystem}: ${span.labels['/db/statement'].substring(0, 30)}...`;
            } else if (span.labels['g.co/agent']) {
              displayName = `Agent: ${span.labels['g.co/agent']}`;
            } else if (span.labels['g.co/gae/app/module']) {
              displayName = `GAE Module: ${span.labels['g.co/gae/app/module']}`;
            } else if (span.labels['g.co/gae/app/version']) {
              displayName = `GAE Version: ${span.labels['g.co/gae/app/version']}`;
            } else if (span.labels['g.co/gce/instance_id']) {
              displayName = `GCE Instance: ${span.labels['g.co/gce/instance_id']}`;
            }
            
            // If still unknown, check for any label that might be descriptive
            if (displayName === 'Unknown Span') {
              // Look for descriptive labels
              const descriptiveLabels = Object.entries(span.labels)
                .filter(([key, value]) => 
                  typeof value === 'string' && 
                  !key.startsWith('/') && 
                  !key.startsWith('g.co/') &&
                  value.length < 50
                );
              
              if (descriptiveLabels.length > 0) {
                // Use the first descriptive label
                const [key, value] = descriptiveLabels[0];
                displayName = `${key}: ${value}`;
              }
            }
          }
          
          // Try alternative fields from the span object
          if (displayName === 'Unknown Span') {
            // Check for operation name (common in Cloud Trace)
            if (span.operation && span.operation.name) {
              displayName = `Operation: ${span.operation.name}`;
            }
            
            // Check for any other descriptive fields
            const possibleNameFields = ['operationName', 'description', 'type', 'method', 'rpcName', 'kind'];
            for (const field of possibleNameFields) {
              if (span[field] && typeof span[field] === 'string') {
                displayName = `${field}: ${span[field]}`;
                break;
              }
            }
          }
        }
        
        // If we still have an unknown span, include the span ID in the display name
        if (displayName === 'Unknown Span' && spanId) {
          displayName = `Unknown Span (ID: ${spanId})`;
        }
        
        // Extract timestamps - in v1 API these are RFC3339 strings
        const startTime = span.startTime || '';
        const endTime = span.endTime || '';
        const parentSpanId = span.parentSpanId || '';
        
        // Extract span kind - in v1 API, this might be in different formats
        let kind = 'UNSPECIFIED';
        if (span.kind) {
          kind = span.kind;
        } else if (span.spanKind) {
          kind = span.spanKind;
        }
        
        // In v1 API, the span kind might be encoded in labels
        if (kind === 'UNSPECIFIED' && span.labels) {
          if (span.labels['/span/kind']) {
            kind = span.labels['/span/kind'];
          } else if (span.labels['span.kind']) {
            kind = span.labels['span.kind'];
          }
        }
        
        // Extract status - in v1 API this might be in different formats
        let status = TraceStatus.UNSPECIFIED;
        if (span.status) {
          if (span.status.code === 0) {
            status = TraceStatus.OK;
          } else if (span.status.code > 0) {
            status = TraceStatus.ERROR;
          }
        }
        
        // In v1 API, error status might be in labels
        if (status === TraceStatus.UNSPECIFIED && span.labels) {
          if (span.labels['/error/message'] || span.labels['error']) {
            status = TraceStatus.ERROR;
          } else if (span.labels['/http/status_code']) {
            const statusCode = parseInt(span.labels['/http/status_code'], 10);
            if (statusCode >= 400) {
              status = TraceStatus.ERROR;
            } else if (statusCode >= 200 && statusCode < 400) {
              status = TraceStatus.OK;
            }
          }
        }
        
        // Extract attributes/labels - handle both v1 and v2 formats
        const attributes: Record<string, string> = {};
        
        // Debug: Log label information
        logger.debug(`Span ${spanId} labels:`);
        logger.debug(`- Has labels: ${!!span.labels}`);
        if (span.labels) {
          logger.debug(`- Label keys: ${Object.keys(span.labels).join(', ')}`);
        }
        
        // Handle v1 API format (labels)
        if (span.labels) {
          logger.debug(`Processing ${Object.keys(span.labels).length} labels for span ${spanId}`);
          for (const [key, value] of Object.entries(span.labels)) {
            if (value !== undefined && value !== null) {
              attributes[key] = String(value);
            }
          }
        }
        
        // Also handle v2 API format (attributes.attributeMap)
        if (span.attributes && span.attributes.attributeMap) {
          for (const [key, value] of Object.entries(span.attributes.attributeMap)) {
            if (value !== undefined && value !== null) {
              attributes[key] = (value as any)?.stringValue || 
                              String((value as any)?.intValue || 
                              (value as any)?.boolValue || '');
            }
          }
        }
        
        // Handle any other fields that might contain useful information
        for (const [key, value] of Object.entries(span)) {
          // Skip keys we've already processed or that are part of the standard span structure
          if (['spanId', 'name', 'displayName', 'startTime', 'endTime', 'parentSpanId', 
               'kind', 'status', 'labels', 'attributes', 'childSpans'].includes(key)) {
            continue;
          }
          
          // Add any other fields as attributes
          if (value !== undefined && value !== null) {
            if (typeof value !== 'object') {
              attributes[`raw.${key}`] = String(value);
            } else if (!Array.isArray(value)) {
              // For simple objects, flatten one level
              try {
                attributes[`raw.${key}`] = JSON.stringify(value).substring(0, 100);
                if (JSON.stringify(value).length > 100) {
                  attributes[`raw.${key}`] += '...';
                }
              } catch (e) {
                // If we can't stringify, just note that it exists
                attributes[`raw.${key}`] = '[Complex Object]';
              }
            }
          }
        }
        
        // Create the trace span
        const traceSpan: TraceSpan = {
          spanId,
          displayName,
          startTime,
          endTime,
          kind,
          status,
          attributes,
          childSpans: []
        };
        
        if (parentSpanId) {
          traceSpan.parentSpanId = parentSpanId;
          logger.debug(`Span ${spanId} has parent: ${parentSpanId}`);
        } else {
          logger.debug(`Span ${spanId} has no parent (will be a root span)`);
        }
        
        // Debug: Log the final display name
        logger.debug(`Final display name for span ${spanId}: "${displayName}"`);
        
        // Store in map for quick lookup
        spanMap.set(spanId, traceSpan);
        
        return traceSpan;
      });
      
      // Build the hierarchy
      const rootSpans: TraceSpan[] = [];
      
      logger.debug(`Building trace hierarchy for ${traceSpans.length} spans...`);
      logger.debug(`Span map contains ${spanMap.size} spans`);
      
      for (const span of traceSpans) {
        logger.debug(`Processing hierarchy for span ${span.spanId} (${span.displayName})`);
        
        if (span.parentSpanId) {
          logger.debug(`Span ${span.spanId} has parent ${span.parentSpanId}`);
          // This is a child span, add it to its parent
          const parentSpan = spanMap.get(span.parentSpanId);
          if (parentSpan) {
            logger.debug(`Found parent span ${span.parentSpanId} for child ${span.spanId}`);
            if (!parentSpan.childSpans) {
              parentSpan.childSpans = [];
              logger.debug(`Initialized childSpans array for parent ${span.parentSpanId}`);
            }
            parentSpan.childSpans.push(span);
            logger.debug(`Added span ${span.spanId} as child of ${span.parentSpanId}`);
          } else {
            // Parent not found, treat as root
            logger.debug(`Parent ${span.parentSpanId} not found for span ${span.spanId}, treating as root`);
            rootSpans.push(span);
          }
        } else {
          // This is a root span
          logger.debug(`Span ${span.spanId} has no parent, adding as root span`);
          rootSpans.push(span);
        }
      }
      
      // Sort child spans by start time
      for (const span of traceSpans) {
        if (span.childSpans && span.childSpans.length > 0) {
          logger.debug(`Sorting ${span.childSpans.length} child spans for parent ${span.spanId}`);
          span.childSpans.sort((a, b) => {
            return new Date(a.startTime).getTime() - new Date(b.startTime).getTime();
          });
        }
      }
      
      // Debug: Log the root spans and their children
      logger.debug(`Final hierarchy: ${rootSpans.length} root spans`);
      for (const rootSpan of rootSpans) {
        logger.debug(`Root span: ${rootSpan.spanId} (${rootSpan.displayName})`);
        logger.debug(`- Has ${rootSpan.childSpans?.length || 0} direct children`);
        
        // Count total descendants
        let totalDescendants = 0;
        const countDescendants = (span: TraceSpan) => {
          if (span.childSpans) {
            totalDescendants += span.childSpans.length;
            for (const child of span.childSpans) {
              countDescendants(child);
            }
          }
        };
        countDescendants(rootSpan);
        logger.debug(`- Total descendants: ${totalDescendants}`);
      }
      
      return {
        traceId,
        projectId,
        rootSpans,
        allSpans: traceSpans
      };
    }
  • formatTraceData helper: Converts TraceData hierarchy to human-readable markdown with hierarchy tree view, timings, important attributes prioritized, status emojis, and error details.
    export function formatTraceData(traceData: TraceData): string {
      let markdown = `## Trace Details\n\n`;
      markdown += `- **Trace ID**: ${traceData.traceId}\n`;
      markdown += `- **Project ID**: ${traceData.projectId}\n`;
      markdown += `- **Total Spans**: ${traceData.allSpans.length}\n`;
      markdown += `- **Associated Logs**: [View logs for this trace](gcp-trace://${traceData.projectId}/traces/${traceData.traceId}/logs)\n\n`;
      
      // Add a summary of span types if we have them
      const spanTypes = new Map<string, number>();
      traceData.allSpans.forEach(span => {
        if (span.kind && span.kind !== 'UNSPECIFIED') {
          spanTypes.set(span.kind, (spanTypes.get(span.kind) || 0) + 1);
        }
      });
      
      if (spanTypes.size > 0) {
        markdown += `- **Span Types**:\n`;
        for (const [type, count] of spanTypes.entries()) {
          markdown += `  - ${type}: ${count}\n`;
        }
        markdown += `\n`;
      }
      
      // Format root spans and their children
      markdown += `## Trace Hierarchy\n\n`;
      
      for (const rootSpan of traceData.rootSpans) {
        markdown += formatSpanHierarchy(rootSpan, 0);
      }
      
      // Add section for failed spans if any
      const failedSpans = traceData.allSpans.filter(span => span.status === TraceStatus.ERROR);
      if (failedSpans.length > 0) {
        markdown += `\n## Failed Spans (${failedSpans.length})\n\n`;
        
        for (const span of failedSpans) {
          markdown += `- **${span.displayName}** (${span.spanId})\n`;
          markdown += `  - Start: ${new Date(span.startTime).toISOString()}\n`;
          markdown += `  - End: ${new Date(span.endTime).toISOString()}\n`;
          markdown += `  - Duration: ${calculateDuration(span.startTime, span.endTime)}\n`;
          
          // Add error details if available
          if (span.attributes['error.message']) {
            markdown += `  - Error: ${span.attributes['error.message']}\n`;
          }
          
          markdown += '\n';
        }
      }
      
      return markdown;
    }

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