Skip to main content
Glama
krzko

Google Cloud MCP Server

by krzko

gcp-trace-query-natural-language

Query Google Cloud Trace data using natural language to analyze performance issues and debug distributed systems.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesNatural language query about traces (e.g., "Show me failed traces from the last hour")
projectIdNoOptional Google Cloud project ID

Implementation Reference

  • Registration of the 'gcp-trace-query-natural-language' MCP tool, including inline input schema (query string and optional projectId) and the complete handler function that parses natural language queries to query GCP Cloud Trace API for traces, handle specific trace IDs, logs, time ranges, filters, and formats results in markdown tables.
      "gcp-trace-query-natural-language",
      {
        query: z
          .string()
          .describe(
            'Natural language query about traces (e.g., "Show me failed traces from the last hour")',
          ),
        projectId: z
          .string()
          .optional()
          .describe("Optional Google Cloud project ID"),
      },
      async ({ query, projectId }, context) => {
        try {
          // Use provided project ID or get the default one from state manager first
          const actualProjectId =
            projectId ||
            stateManager.getCurrentProjectId() ||
            (await getProjectId());
    
          // Process the natural language query
          const normalizedQuery = query.toLowerCase();
    
          // Default parameters
          let filter = "";
          let limit = 10;
          let startTime = "1h"; // Default to 1 hour
          let traceId = "";
    
          // Extract trace ID if present
          const traceIdMatch = normalizedQuery.match(
            /trace(?:\s+id)?[:\s]+([a-f0-9]+)/i,
          );
          if (traceIdMatch && traceIdMatch[1]) {
            traceId = traceIdMatch[1];
    
            // If we have a trace ID, implement the get-trace functionality directly
            // Validate trace ID format (hex string)
            if (typeof traceId === "string" && !traceId.match(/^[a-f0-9]+$/i)) {
              throw new GcpMcpError(
                "Invalid trace ID format. Trace ID should be a hexadecimal string.",
                "INVALID_ARGUMENT",
                400,
              );
            }
    
            // Initialize Google Auth client
            const auth = await initGoogleAuth(true);
            if (!auth) {
              throw new GcpMcpError(
                "Google Cloud authentication not available. Please configure authentication to access trace data.",
                "UNAUTHENTICATED",
                401,
              );
            }
            const client = await auth.getClient();
            const token = await client.getAccessToken();
    
            // Fetch the trace from the Cloud Trace API
            const response = await fetch(
              `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces/${traceId}`,
              {
                headers: {
                  Authorization: `Bearer ${token.token}`,
                  "Content-Type": "application/json",
                },
              },
            );
    
            if (!response.ok) {
              const errorText = await response.text();
              throw new GcpMcpError(
                `Failed to fetch trace: ${errorText}`,
                "FAILED_PRECONDITION",
                response.status,
              );
            }
    
            const traceData = await response.json();
    
            if (!traceData || !traceData.spans || traceData.spans.length === 0) {
              return {
                content: [
                  {
                    type: "text",
                    text: `No trace found with ID: ${traceId} in project: ${actualProjectId}`,
                  },
                ],
              };
            }
    
            // Build the trace hierarchy
            const hierarchicalTrace = buildTraceHierarchy(
              actualProjectId.toString(),
              traceId.toString(),
              traceData.spans,
            );
    
            // Format the trace data for display
            const formattedTrace = formatTraceData(hierarchicalTrace);
    
            return {
              content: [
                {
                  type: "text",
                  text: formattedTrace,
                },
              ],
            };
          }
    
          // Extract time range
          if (
            normalizedQuery.includes("last hour") ||
            normalizedQuery.includes("past hour")
          ) {
            startTime = "1h";
          } else if (
            normalizedQuery.includes("last day") ||
            normalizedQuery.includes("past day") ||
            normalizedQuery.includes("24 hours") ||
            normalizedQuery.includes("today")
          ) {
            startTime = "24h";
          } else if (
            normalizedQuery.includes("last week") ||
            normalizedQuery.includes("past week")
          ) {
            startTime = "7d";
          } else if (
            normalizedQuery.includes("last month") ||
            normalizedQuery.includes("past month")
          ) {
            startTime = "30d";
          }
    
          // Extract status filter
          if (
            normalizedQuery.includes("fail") ||
            normalizedQuery.includes("error") ||
            normalizedQuery.includes("exception") ||
            normalizedQuery.includes("problem")
          ) {
            filter = "status.code != 0"; // Non-zero status codes indicate errors
          }
    
          // Extract limit
          const limitMatch = normalizedQuery.match(
            /\b(show|display|list|get|find)?\s+(\d+)\s+(trace|span|result)/i,
          );
          if (limitMatch && limitMatch[2]) {
            limit = parseInt(limitMatch[2]);
            limit = Math.min(Math.max(limit, 1), 100); // Clamp between 1 and 100
          }
    
          // If query mentions logs, use the find-traces-from-logs tool
          if (
            normalizedQuery.includes("log") ||
            normalizedQuery.includes("logging")
          ) {
            let logFilter = "severity>=ERROR"; // Default to error logs
    
            if (
              normalizedQuery.includes("info") ||
              normalizedQuery.includes("information")
            ) {
              logFilter = "severity>=INFO";
            } else if (
              normalizedQuery.includes("warn") ||
              normalizedQuery.includes("warning")
            ) {
              logFilter = "severity>=WARNING";
            } else if (normalizedQuery.includes("debug")) {
              logFilter = "severity>=DEBUG";
            }
    
            // Implement find-traces-from-logs functionality directly
            // Initialize the logging client
            const logging = new Logging({
              projectId: actualProjectId,
            });
    
            // Fetch logs with the given filter
            const [entries] = await logging.getEntries({
              filter: logFilter,
              pageSize: limit,
            });
    
            if (!entries || entries.length === 0) {
              return {
                content: [
                  {
                    type: "text",
                    text: `No logs found matching the filter: "${logFilter}" in project: ${actualProjectId}`,
                  },
                ],
              };
            }
    
            // Extract trace IDs from logs
            const traceMap = new Map<
              string,
              {
                traceId: string;
                timestamp: string;
                severity: string;
                logName: string;
                message: string;
              }
            >();
    
            for (const entry of entries) {
              const metadata = entry.metadata;
              const traceId = extractTraceIdFromLog(metadata);
    
              if (traceId) {
                // Convert timestamp to string
                let timestampStr = "Unknown";
                if (metadata.timestamp) {
                  if (typeof metadata.timestamp === "string") {
                    timestampStr = metadata.timestamp;
                  } else {
                    try {
                      // Handle different timestamp formats
                      if (
                        typeof metadata.timestamp === "object" &&
                        metadata.timestamp !== null
                      ) {
                        if (
                          "seconds" in metadata.timestamp &&
                          "nanos" in metadata.timestamp
                        ) {
                          // Handle Timestamp proto format
                          const seconds = Number(metadata.timestamp.seconds);
                          const nanos = Number(metadata.timestamp.nanos || 0);
                          const milliseconds = seconds * 1000 + nanos / 1000000;
                          timestampStr = new Date(milliseconds).toISOString();
                        } else {
                          // Try to convert using JSON
                          timestampStr = JSON.stringify(metadata.timestamp);
                        }
                      } else {
                        timestampStr = String(metadata.timestamp);
                      }
                    } catch (e) {
                      timestampStr = "Invalid timestamp";
                    }
                  }
                }
    
                // Convert severity to string
                let severityStr = "DEFAULT";
                if (metadata.severity) {
                  severityStr = String(metadata.severity);
                }
    
                // Convert logName to string
                let logNameStr = "Unknown";
                if (metadata.logName) {
                  logNameStr = String(metadata.logName);
                }
    
                // Extract message
                let messageStr = "No message";
                if (metadata.textPayload) {
                  messageStr = String(metadata.textPayload);
                } else if (metadata.jsonPayload) {
                  try {
                    messageStr = JSON.stringify(metadata.jsonPayload);
                  } catch (e) {
                    messageStr = "Invalid JSON payload";
                  }
                }
    
                traceMap.set(traceId, {
                  traceId,
                  timestamp: timestampStr,
                  severity: severityStr,
                  logName: logNameStr,
                  message: messageStr,
                });
              }
            }
    
            if (traceMap.size === 0) {
              return {
                content: [
                  {
                    type: "text",
                    text: `No traces found in the logs matching the filter: "${logFilter}" in project: ${actualProjectId}`,
                  },
                ],
              };
            }
    
            // Format the traces for display
            let markdown = `# Traces Found in Logs\n\n`;
            markdown += `Project: ${actualProjectId}\n`;
            markdown += `Log Filter: ${logFilter}\n`;
            markdown += `Found ${traceMap.size} unique traces in ${entries.length} log entries:\n\n`;
    
            // Table header
            markdown +=
              "| Trace ID | Timestamp | Severity | Log Name | Message |\n";
            markdown +=
              "|----------|-----------|----------|----------|--------|\n";
    
            // Table rows
            for (const trace of traceMap.values()) {
              const traceId = trace.traceId;
              // Handle timestamp formatting safely
              let timestamp = trace.timestamp;
              try {
                if (
                  timestamp !== "Unknown" &&
                  timestamp !== "Invalid timestamp"
                ) {
                  timestamp = new Date(trace.timestamp).toISOString();
                }
              } catch (e) {
                // Keep the original timestamp if conversion fails
              }
              const severity = trace.severity;
              const logName = trace.logName.split("/").pop() || trace.logName;
              const message =
                trace.message.length > 100
                  ? `${trace.message.substring(0, 100)}...`
                  : trace.message;
    
              markdown += `| ${traceId} | ${timestamp} | ${severity} | ${logName} | ${message} |\n`;
            }
    
            return {
              content: [
                {
                  type: "text",
                  text: markdown,
                },
              ],
            };
          }
    
          // Otherwise, use the list-traces tool
          // Implement list-traces functionality directly
          // Calculate time range
          const endTime = new Date();
          let startTimeDate: Date;
    
          if (startTime) {
            logger.debug(`Raw startTime parameter: ${JSON.stringify(startTime)}`);
    
            // Handle the case where startTime might be passed as an object from JSON
            const startTimeStr =
              typeof startTime === "string" ? startTime : String(startTime);
            logger.debug(`Processing startTime: ${startTimeStr}`);
    
            // Check if the input is a relative time format (e.g., "1h", "2d", "30m")
            if (startTimeStr.match(/^\d+[hmd]$/)) {
              // Parse relative time (e.g., "1h", "2d")
              const value = parseInt(startTimeStr.slice(0, -1));
              const unit = startTimeStr.slice(-1);
    
              startTimeDate = new Date(endTime);
              if (unit === "h") {
                startTimeDate.setHours(startTimeDate.getHours() - value);
              } else if (unit === "d") {
                startTimeDate.setDate(startTimeDate.getDate() - value);
              } else if (unit === "m") {
                startTimeDate.setMinutes(startTimeDate.getMinutes() - value);
              }
              logger.debug(
                `Parsed relative time: ${startTimeStr} to ${startTimeDate.toISOString()}`,
              );
            } else {
              // Parse ISO format
              try {
                startTimeDate = new Date(startTimeStr);
                if (isNaN(startTimeDate.getTime())) {
                  throw new Error("Invalid date format");
                }
                logger.debug(
                  `Parsed ISO time: ${startTimeStr} to ${startTimeDate.toISOString()}`,
                );
              } catch (error: unknown) {
                const errorMessage =
                  error instanceof Error ? error.message : "Unknown error";
                logger.error(`Error parsing time: ${errorMessage}`);
                throw new GcpMcpError(
                  `Invalid start time format: "${startTimeStr}". Use ISO format or relative time (e.g., "1h", "2d").`,
                  "INVALID_ARGUMENT",
                  400,
                );
              }
            }
          } else {
            // Default to 1 hour ago
            startTimeDate = new Date(endTime);
            startTimeDate.setHours(startTimeDate.getHours() - 1);
          }
    
          // Build the request body
          const requestBody: any = {
            projectId: actualProjectId,
            startTime: startTimeDate.toISOString(),
            endTime: endTime.toISOString(),
            pageSize: limit,
          };
    
          if (filter) {
            requestBody.filter = filter;
          }
    
          // Initialize Google Auth client
          const auth = await initGoogleAuth(true);
          if (!auth) {
            throw new GcpMcpError(
              "Google Cloud authentication not available. Please configure authentication to access trace data.",
              "UNAUTHENTICATED",
              401,
            );
          }
          const client = await auth.getClient();
          const token = await client.getAccessToken();
    
          // Build the query parameters for the request
          const queryParams = new URLSearchParams();
    
          // Format timestamps in RFC3339 UTC "Zulu" format
          const startTimeUTC = new Date(
            startTimeDate.toISOString(),
          ).toISOString();
          const endTimeUTC = new Date(endTime.toISOString()).toISOString();
    
          // Add required query parameters according to the API documentation
          queryParams.append("startTime", startTimeUTC);
          queryParams.append("endTime", endTimeUTC);
          queryParams.append("pageSize", limit.toString());
          // Add view parameter to specify the type of data returned
          queryParams.append("view", "MINIMAL");
          if (filter) {
            queryParams.append("filter", filter);
          }
    
          // Construct the URL for the Cloud Trace API v1 endpoint
          const apiUrl = `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces`;
          const requestUrl = `${apiUrl}?${queryParams.toString()}`;
    
          logger.debug(`Fetching traces from: ${requestUrl}`);
    
          // Fetch traces from the Cloud Trace API
          const response = await fetch(requestUrl, {
            method: "GET",
            headers: {
              Authorization: `Bearer ${token.token}`,
              Accept: "application/json",
            },
          });
    
          if (!response.ok) {
            const errorText = await response.text();
            throw new GcpMcpError(
              `Failed to list traces: ${errorText}`,
              "FAILED_PRECONDITION",
              response.status,
            );
          }
    
          const tracesData = await response.json();
    
          // Check if we have valid traces data
          // In v1 API, the response contains a traces array
          if (!tracesData.traces || tracesData.traces.length === 0) {
            return {
              content: [
                {
                  type: "text",
                  text: `No traces found matching the criteria in project: ${actualProjectId}`,
                },
              ],
            };
          }
    
          // Format the traces for display
          let markdown = `# Traces for ${actualProjectId}\n\n`;
          markdown += `Time Range: ${startTimeDate.toISOString()} to ${endTime.toISOString()}\n`;
          markdown += `Filter: ${filter || "None"}\n\n`;
          markdown += `Found ${tracesData.traces.length} traces:\n\n`;
    
          // Table header
          markdown +=
            "| Trace ID | Display Name | Start Time | Duration | Status |\n";
          markdown +=
            "|----------|--------------|------------|----------|--------|\n";
    
          // Table rows
          for (const trace of tracesData.traces) {
            const traceId = trace.traceId;
            const displayName = trace.displayName || "Unknown";
            const startTimeStr = new Date(trace.startTime).toISOString();
            const duration = calculateDuration(trace.startTime, trace.endTime);
            const status =
              trace.status?.code === 0
                ? "OK"
                : `Error: ${trace.status?.message || "Unknown error"}`;
    
            markdown += `| ${traceId} | ${displayName} | ${startTimeStr} | ${duration} | ${status} |\n`;
          }
    
          return {
            content: [
              {
                type: "text",
                text: markdown,
              },
            ],
          };
        } catch (error: any) {
          // Error handling for natural-language-trace-query tool
          throw new GcpMcpError(
            `Failed to process natural language query: ${error.message}`,
            error.code || "UNKNOWN",
            error.statusCode || 500,
          );
        }
      },
    );
  • The handler function executes natural language queries by parsing intent (e.g., time ranges like 'last hour', errors, logs), fetching traces via Cloud Trace API (/v1/projects/{project}/traces), handling auth, formatting hierarchical traces or lists in markdown tables, and error handling.
    async ({ query, projectId }, context) => {
      try {
        // Use provided project ID or get the default one from state manager first
        const actualProjectId =
          projectId ||
          stateManager.getCurrentProjectId() ||
          (await getProjectId());
    
        // Process the natural language query
        const normalizedQuery = query.toLowerCase();
    
        // Default parameters
        let filter = "";
        let limit = 10;
        let startTime = "1h"; // Default to 1 hour
        let traceId = "";
    
        // Extract trace ID if present
        const traceIdMatch = normalizedQuery.match(
          /trace(?:\s+id)?[:\s]+([a-f0-9]+)/i,
        );
        if (traceIdMatch && traceIdMatch[1]) {
          traceId = traceIdMatch[1];
    
          // If we have a trace ID, implement the get-trace functionality directly
          // Validate trace ID format (hex string)
          if (typeof traceId === "string" && !traceId.match(/^[a-f0-9]+$/i)) {
            throw new GcpMcpError(
              "Invalid trace ID format. Trace ID should be a hexadecimal string.",
              "INVALID_ARGUMENT",
              400,
            );
          }
    
          // Initialize Google Auth client
          const auth = await initGoogleAuth(true);
          if (!auth) {
            throw new GcpMcpError(
              "Google Cloud authentication not available. Please configure authentication to access trace data.",
              "UNAUTHENTICATED",
              401,
            );
          }
          const client = await auth.getClient();
          const token = await client.getAccessToken();
    
          // Fetch the trace from the Cloud Trace API
          const response = await fetch(
            `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces/${traceId}`,
            {
              headers: {
                Authorization: `Bearer ${token.token}`,
                "Content-Type": "application/json",
              },
            },
          );
    
          if (!response.ok) {
            const errorText = await response.text();
            throw new GcpMcpError(
              `Failed to fetch trace: ${errorText}`,
              "FAILED_PRECONDITION",
              response.status,
            );
          }
    
          const traceData = await response.json();
    
          if (!traceData || !traceData.spans || traceData.spans.length === 0) {
            return {
              content: [
                {
                  type: "text",
                  text: `No trace found with ID: ${traceId} in project: ${actualProjectId}`,
                },
              ],
            };
          }
    
          // Build the trace hierarchy
          const hierarchicalTrace = buildTraceHierarchy(
            actualProjectId.toString(),
            traceId.toString(),
            traceData.spans,
          );
    
          // Format the trace data for display
          const formattedTrace = formatTraceData(hierarchicalTrace);
    
          return {
            content: [
              {
                type: "text",
                text: formattedTrace,
              },
            ],
          };
        }
    
        // Extract time range
        if (
          normalizedQuery.includes("last hour") ||
          normalizedQuery.includes("past hour")
        ) {
          startTime = "1h";
        } else if (
          normalizedQuery.includes("last day") ||
          normalizedQuery.includes("past day") ||
          normalizedQuery.includes("24 hours") ||
          normalizedQuery.includes("today")
        ) {
          startTime = "24h";
        } else if (
          normalizedQuery.includes("last week") ||
          normalizedQuery.includes("past week")
        ) {
          startTime = "7d";
        } else if (
          normalizedQuery.includes("last month") ||
          normalizedQuery.includes("past month")
        ) {
          startTime = "30d";
        }
    
        // Extract status filter
        if (
          normalizedQuery.includes("fail") ||
          normalizedQuery.includes("error") ||
          normalizedQuery.includes("exception") ||
          normalizedQuery.includes("problem")
        ) {
          filter = "status.code != 0"; // Non-zero status codes indicate errors
        }
    
        // Extract limit
        const limitMatch = normalizedQuery.match(
          /\b(show|display|list|get|find)?\s+(\d+)\s+(trace|span|result)/i,
        );
        if (limitMatch && limitMatch[2]) {
          limit = parseInt(limitMatch[2]);
          limit = Math.min(Math.max(limit, 1), 100); // Clamp between 1 and 100
        }
    
        // If query mentions logs, use the find-traces-from-logs tool
        if (
          normalizedQuery.includes("log") ||
          normalizedQuery.includes("logging")
        ) {
          let logFilter = "severity>=ERROR"; // Default to error logs
    
          if (
            normalizedQuery.includes("info") ||
            normalizedQuery.includes("information")
          ) {
            logFilter = "severity>=INFO";
          } else if (
            normalizedQuery.includes("warn") ||
            normalizedQuery.includes("warning")
          ) {
            logFilter = "severity>=WARNING";
          } else if (normalizedQuery.includes("debug")) {
            logFilter = "severity>=DEBUG";
          }
    
          // Implement find-traces-from-logs functionality directly
          // Initialize the logging client
          const logging = new Logging({
            projectId: actualProjectId,
          });
    
          // Fetch logs with the given filter
          const [entries] = await logging.getEntries({
            filter: logFilter,
            pageSize: limit,
          });
    
          if (!entries || entries.length === 0) {
            return {
              content: [
                {
                  type: "text",
                  text: `No logs found matching the filter: "${logFilter}" in project: ${actualProjectId}`,
                },
              ],
            };
          }
    
          // Extract trace IDs from logs
          const traceMap = new Map<
            string,
            {
              traceId: string;
              timestamp: string;
              severity: string;
              logName: string;
              message: string;
            }
          >();
    
          for (const entry of entries) {
            const metadata = entry.metadata;
            const traceId = extractTraceIdFromLog(metadata);
    
            if (traceId) {
              // Convert timestamp to string
              let timestampStr = "Unknown";
              if (metadata.timestamp) {
                if (typeof metadata.timestamp === "string") {
                  timestampStr = metadata.timestamp;
                } else {
                  try {
                    // Handle different timestamp formats
                    if (
                      typeof metadata.timestamp === "object" &&
                      metadata.timestamp !== null
                    ) {
                      if (
                        "seconds" in metadata.timestamp &&
                        "nanos" in metadata.timestamp
                      ) {
                        // Handle Timestamp proto format
                        const seconds = Number(metadata.timestamp.seconds);
                        const nanos = Number(metadata.timestamp.nanos || 0);
                        const milliseconds = seconds * 1000 + nanos / 1000000;
                        timestampStr = new Date(milliseconds).toISOString();
                      } else {
                        // Try to convert using JSON
                        timestampStr = JSON.stringify(metadata.timestamp);
                      }
                    } else {
                      timestampStr = String(metadata.timestamp);
                    }
                  } catch (e) {
                    timestampStr = "Invalid timestamp";
                  }
                }
              }
    
              // Convert severity to string
              let severityStr = "DEFAULT";
              if (metadata.severity) {
                severityStr = String(metadata.severity);
              }
    
              // Convert logName to string
              let logNameStr = "Unknown";
              if (metadata.logName) {
                logNameStr = String(metadata.logName);
              }
    
              // Extract message
              let messageStr = "No message";
              if (metadata.textPayload) {
                messageStr = String(metadata.textPayload);
              } else if (metadata.jsonPayload) {
                try {
                  messageStr = JSON.stringify(metadata.jsonPayload);
                } catch (e) {
                  messageStr = "Invalid JSON payload";
                }
              }
    
              traceMap.set(traceId, {
                traceId,
                timestamp: timestampStr,
                severity: severityStr,
                logName: logNameStr,
                message: messageStr,
              });
            }
          }
    
          if (traceMap.size === 0) {
            return {
              content: [
                {
                  type: "text",
                  text: `No traces found in the logs matching the filter: "${logFilter}" in project: ${actualProjectId}`,
                },
              ],
            };
          }
    
          // Format the traces for display
          let markdown = `# Traces Found in Logs\n\n`;
          markdown += `Project: ${actualProjectId}\n`;
          markdown += `Log Filter: ${logFilter}\n`;
          markdown += `Found ${traceMap.size} unique traces in ${entries.length} log entries:\n\n`;
    
          // Table header
          markdown +=
            "| Trace ID | Timestamp | Severity | Log Name | Message |\n";
          markdown +=
            "|----------|-----------|----------|----------|--------|\n";
    
          // Table rows
          for (const trace of traceMap.values()) {
            const traceId = trace.traceId;
            // Handle timestamp formatting safely
            let timestamp = trace.timestamp;
            try {
              if (
                timestamp !== "Unknown" &&
                timestamp !== "Invalid timestamp"
              ) {
                timestamp = new Date(trace.timestamp).toISOString();
              }
            } catch (e) {
              // Keep the original timestamp if conversion fails
            }
            const severity = trace.severity;
            const logName = trace.logName.split("/").pop() || trace.logName;
            const message =
              trace.message.length > 100
                ? `${trace.message.substring(0, 100)}...`
                : trace.message;
    
            markdown += `| ${traceId} | ${timestamp} | ${severity} | ${logName} | ${message} |\n`;
          }
    
          return {
            content: [
              {
                type: "text",
                text: markdown,
              },
            ],
          };
        }
    
        // Otherwise, use the list-traces tool
        // Implement list-traces functionality directly
        // Calculate time range
        const endTime = new Date();
        let startTimeDate: Date;
    
        if (startTime) {
          logger.debug(`Raw startTime parameter: ${JSON.stringify(startTime)}`);
    
          // Handle the case where startTime might be passed as an object from JSON
          const startTimeStr =
            typeof startTime === "string" ? startTime : String(startTime);
          logger.debug(`Processing startTime: ${startTimeStr}`);
    
          // Check if the input is a relative time format (e.g., "1h", "2d", "30m")
          if (startTimeStr.match(/^\d+[hmd]$/)) {
            // Parse relative time (e.g., "1h", "2d")
            const value = parseInt(startTimeStr.slice(0, -1));
            const unit = startTimeStr.slice(-1);
    
            startTimeDate = new Date(endTime);
            if (unit === "h") {
              startTimeDate.setHours(startTimeDate.getHours() - value);
            } else if (unit === "d") {
              startTimeDate.setDate(startTimeDate.getDate() - value);
            } else if (unit === "m") {
              startTimeDate.setMinutes(startTimeDate.getMinutes() - value);
            }
            logger.debug(
              `Parsed relative time: ${startTimeStr} to ${startTimeDate.toISOString()}`,
            );
          } else {
            // Parse ISO format
            try {
              startTimeDate = new Date(startTimeStr);
              if (isNaN(startTimeDate.getTime())) {
                throw new Error("Invalid date format");
              }
              logger.debug(
                `Parsed ISO time: ${startTimeStr} to ${startTimeDate.toISOString()}`,
              );
            } catch (error: unknown) {
              const errorMessage =
                error instanceof Error ? error.message : "Unknown error";
              logger.error(`Error parsing time: ${errorMessage}`);
              throw new GcpMcpError(
                `Invalid start time format: "${startTimeStr}". Use ISO format or relative time (e.g., "1h", "2d").`,
                "INVALID_ARGUMENT",
                400,
              );
            }
          }
        } else {
          // Default to 1 hour ago
          startTimeDate = new Date(endTime);
          startTimeDate.setHours(startTimeDate.getHours() - 1);
        }
    
        // Build the request body
        const requestBody: any = {
          projectId: actualProjectId,
          startTime: startTimeDate.toISOString(),
          endTime: endTime.toISOString(),
          pageSize: limit,
        };
    
        if (filter) {
          requestBody.filter = filter;
        }
    
        // Initialize Google Auth client
        const auth = await initGoogleAuth(true);
        if (!auth) {
          throw new GcpMcpError(
            "Google Cloud authentication not available. Please configure authentication to access trace data.",
            "UNAUTHENTICATED",
            401,
          );
        }
        const client = await auth.getClient();
        const token = await client.getAccessToken();
    
        // Build the query parameters for the request
        const queryParams = new URLSearchParams();
    
        // Format timestamps in RFC3339 UTC "Zulu" format
        const startTimeUTC = new Date(
          startTimeDate.toISOString(),
        ).toISOString();
        const endTimeUTC = new Date(endTime.toISOString()).toISOString();
    
        // Add required query parameters according to the API documentation
        queryParams.append("startTime", startTimeUTC);
        queryParams.append("endTime", endTimeUTC);
        queryParams.append("pageSize", limit.toString());
        // Add view parameter to specify the type of data returned
        queryParams.append("view", "MINIMAL");
        if (filter) {
          queryParams.append("filter", filter);
        }
    
        // Construct the URL for the Cloud Trace API v1 endpoint
        const apiUrl = `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces`;
        const requestUrl = `${apiUrl}?${queryParams.toString()}`;
    
        logger.debug(`Fetching traces from: ${requestUrl}`);
    
        // Fetch traces from the Cloud Trace API
        const response = await fetch(requestUrl, {
          method: "GET",
          headers: {
            Authorization: `Bearer ${token.token}`,
            Accept: "application/json",
          },
        });
    
        if (!response.ok) {
          const errorText = await response.text();
          throw new GcpMcpError(
            `Failed to list traces: ${errorText}`,
            "FAILED_PRECONDITION",
            response.status,
          );
        }
    
        const tracesData = await response.json();
    
        // Check if we have valid traces data
        // In v1 API, the response contains a traces array
        if (!tracesData.traces || tracesData.traces.length === 0) {
          return {
            content: [
              {
                type: "text",
                text: `No traces found matching the criteria in project: ${actualProjectId}`,
              },
            ],
          };
        }
    
        // Format the traces for display
        let markdown = `# Traces for ${actualProjectId}\n\n`;
        markdown += `Time Range: ${startTimeDate.toISOString()} to ${endTime.toISOString()}\n`;
        markdown += `Filter: ${filter || "None"}\n\n`;
        markdown += `Found ${tracesData.traces.length} traces:\n\n`;
    
        // Table header
        markdown +=
          "| Trace ID | Display Name | Start Time | Duration | Status |\n";
        markdown +=
          "|----------|--------------|------------|----------|--------|\n";
    
        // Table rows
        for (const trace of tracesData.traces) {
          const traceId = trace.traceId;
          const displayName = trace.displayName || "Unknown";
          const startTimeStr = new Date(trace.startTime).toISOString();
          const duration = calculateDuration(trace.startTime, trace.endTime);
          const status =
            trace.status?.code === 0
              ? "OK"
              : `Error: ${trace.status?.message || "Unknown error"}`;
    
          markdown += `| ${traceId} | ${displayName} | ${startTimeStr} | ${duration} | ${status} |\n`;
        }
    
        return {
          content: [
            {
              type: "text",
              text: markdown,
            },
          ],
        };
      } catch (error: any) {
        // Error handling for natural-language-trace-query tool
        throw new GcpMcpError(
          `Failed to process natural language query: ${error.message}`,
          error.code || "UNKNOWN",
          error.statusCode || 500,
        );
      }
    },
  • Zod schema for tool inputs: required 'query' string for natural language description, optional 'projectId' string.
    {
      query: z
        .string()
        .describe(
          'Natural language query about traces (e.g., "Show me failed traces from the last hour")',
        ),
      projectId: z
        .string()
        .optional()
        .describe("Optional Google Cloud project ID"),
    },

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