Skip to main content
Glama
krzko

Google Cloud MCP Server

by krzko

gcp-trace-get-trace

Retrieve detailed trace data using a trace ID from Google Cloud Trace. Includes optional project ID support for targeted querying. Simplifies trace analysis in Google Cloud.

Input Schema

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

Implementation Reference

  • Registration of the 'gcp-trace-get-trace' tool using server.tool(), including inline schema and handler function that fetches trace data from GCP Cloud Trace API v1, validates input, handles auth, builds trace hierarchy using buildTraceHierarchy, formats output with formatTraceData, and returns formatted markdown response.
    server.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, ); } }, );
  • Zod schema for tool inputs: required traceId (string), optional projectId (string).
    { traceId: z.string().describe("The trace ID to retrieve"), projectId: z .string() .optional() .describe("Optional Google Cloud project ID"), },
  • Executes the tool: authenticates with GCP, fetches specific trace by ID using REST API, processes spans into hierarchy, handles errors, formats hierarchical view as markdown with timings, attributes, status.
    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 TraceData from flat spans array returned by GCP API, populates displayName intelligently from various fields/labels, infers kind/status, links children to parents via spanMap.
    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 detailed markdown response, recursive hierarchy rendering with indents/status emojis/timings/attributes (prioritized HTTP/error/DB etc.), failed spans summary.
    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/krzko/google-cloud-mcp'

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