Skip to main content
Glama
krzko

Google Cloud MCP Server

by krzko

gcp-trace-get-trace

Retrieve specific trace data from Google Cloud Trace by providing a trace ID to analyze request performance and debug distributed systems.

Input Schema

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

Implementation Reference

  • Main tool handler: fetches trace by ID from GCP Cloud Trace API v1 using REST API, processes spans, builds hierarchical structure, formats as Markdown, handles auth and errors.
    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,
        );
      }
    },
  • Input schema using Zod: traceId (required string), projectId (optional string).
    {
      traceId: z.string().describe("The trace ID to retrieve"),
      projectId: z
        .string()
        .optional()
        .describe("Optional Google Cloud project ID"),
    },
  • Tool registration within registerTraceTools function using McpServer.tool().
      "gcp-trace-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,
          );
        }
      },
    );
  • Helper to build hierarchical trace structure from raw API spans: creates TraceSpan objects, links children via parentSpanId, sorts by time.
    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,
      };
    }
  • Helper to format TraceData into human-readable Markdown with hierarchy, timings, attributes, status indicators.
    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;
    }
Behavior1/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Tool has no description.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness1/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Tool has no description.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness1/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Tool has no description.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters1/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Tool has no description.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose1/5

Does the description clearly state what the tool does and how it differs from similar tools?

Tool has no description.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines1/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Tool has no description.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/krzko/google-cloud-mcp'

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