gcp-trace-list-traces
Retrieve and filter Google Cloud Trace data to monitor application performance, identify errors, and analyze request patterns across services.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| filter | No | Optional filter for traces (e.g., "status.code != 0" for errors) | |
| limit | No | Maximum number of traces to return | |
| projectId | No | Optional Google Cloud project ID | |
| startTime | No | Start time in RFC3339 format (e.g., "2023-01-01T00:00:00Z") or relative time (e.g., "1h", "2d") |
Implementation Reference
- src/services/trace/tools.ts:296-498 (handler)The handler function for the 'gcp-trace-list-traces' tool. It authenticates with GCP, parses time range (supports relative like '1h'), builds and sends GET request to Cloud Trace API v1 /traces endpoint with parameters for time range, limit, filter, view=COMPLETE, orderBy=start desc, processes response, and returns formatted markdown table using helper formatTracesResponse.async ({ projectId, filter, limit, startTime }, context) => { try { // Use provided project ID or get the default one from state manager first const actualProjectId = projectId || stateManager.getCurrentProjectId() || (await getProjectId()); // 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); } // 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(); // Format timestamps in RFC3339 UTC "Zulu" format as required by the API // Example format: "2014-10-02T15:01:23Z" // The Cloud Trace API requires RFC3339 format timestamps const startTimeUTC = startTimeDate.toISOString(); const endTimeUTC = endTime.toISOString(); logger.debug( `Using formatted timestamps: startTime=${startTimeUTC}, endTime=${endTimeUTC}`, ); // Build the query parameters for the request according to the API documentation const queryParams = new URLSearchParams(); // Required parameters - format must be RFC3339 UTC "Zulu" format // The Cloud Trace API requires timestamps in RFC3339 format // Example: "2014-10-02T15:01:23Z" queryParams.append("startTime", startTimeUTC); // Start of the time interval (inclusive) queryParams.append("endTime", endTimeUTC); // End of the time interval (inclusive) // Optional parameters queryParams.append("pageSize", limit.toString()); // Maximum number of traces to return // The view parameter is optional and defaults to MINIMAL // ROOTSPAN includes the root span with the trace // COMPLETE includes all spans with the trace queryParams.append("view", "COMPLETE"); // Type of data returned (MINIMAL, ROOTSPAN, COMPLETE) // Add orderBy parameter to sort by most recent traces first queryParams.append("orderBy", "start desc"); // Sort by start time descending // Optional filter parameter if (filter) { queryParams.append("filter", filter); // Filter against labels for the request } // Construct the URL for the Cloud Trace API v1 endpoint // The correct endpoint format according to the documentation is: // GET https://cloudtrace.googleapis.com/v1/projects/{projectId}/traces const apiUrl = `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces`; const requestUrl = `${apiUrl}?${queryParams.toString()}`; logger.debug(`List Traces URL: ${requestUrl}`); logger.debug( `List Traces Query Params: ${JSON.stringify(Object.fromEntries(queryParams.entries()))}`, ); logger.debug( `List Traces Time Range: ${startTimeDate.toISOString()} to ${endTime.toISOString()}`, ); logger.debug(`List Traces Raw Query String: ${queryParams.toString()}`); // Fetch traces from the Cloud Trace API logger.debug(`Sending request to Cloud Trace API: ${requestUrl}`); let tracesData; try { const response = await fetch(requestUrl, { method: "GET", headers: { Authorization: `Bearer ${token.token}`, Accept: "application/json", }, }); logger.debug(`List Traces Response Status: ${response.status}`); if (!response.ok) { const errorText = await response.text(); logger.error(`List Traces Error: ${errorText}`); throw new GcpMcpError( `Failed to list traces: ${errorText}`, "FAILED_PRECONDITION", response.status, ); } // Log the full response headers to help debug const responseHeaders: Record<string, string> = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); logger.debug( `List Traces Response Headers: ${JSON.stringify(responseHeaders)}`, ); tracesData = await response.json(); logger.debug( `List Traces Response Data: ${JSON.stringify(tracesData, null, 2)}`, ); } catch (fetchError: any) { logger.error(`Fetch error: ${fetchError.message}`); throw new GcpMcpError( `Failed to fetch traces: ${fetchError.message}`, "INTERNAL", 500, ); } // 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}`, }, ], }; } // Use the helper function to format the response return formatTracesResponse( tracesData, actualProjectId, startTimeDate, endTime, filter, ); } catch (error: any) { // Error handling for list-traces tool throw new GcpMcpError( `Failed to list traces: ${error.message}`, error.code || "UNKNOWN", error.statusCode || 500, ); } },
- src/services/trace/tools.ts:272-295 (schema)Zod input schema for the gcp-trace-list-traces tool defining parameters: projectId (optional), filter (optional), limit (1-100, default 10), startTime (optional, supports relative/ISO).{ projectId: z .string() .optional() .describe("Optional Google Cloud project ID"), filter: z .string() .optional() .describe( 'Optional filter for traces (e.g., "status.code != 0" for errors)', ), limit: z .number() .min(1) .max(100) .default(10) .describe("Maximum number of traces to return"), startTime: z .string() .optional() .describe( 'Start time in RFC3339 format (e.g., "2023-01-01T00:00:00Z") or relative time (e.g., "1h", "2d")', ), },
- src/services/trace/tools.ts:270-499 (registration)Registration of the gcp-trace-list-traces tool in registerTraceTools function using McpServer.tool() with name, schema, and inline handler.server.tool( "gcp-trace-list-traces", { projectId: z .string() .optional() .describe("Optional Google Cloud project ID"), filter: z .string() .optional() .describe( 'Optional filter for traces (e.g., "status.code != 0" for errors)', ), limit: z .number() .min(1) .max(100) .default(10) .describe("Maximum number of traces to return"), startTime: z .string() .optional() .describe( 'Start time in RFC3339 format (e.g., "2023-01-01T00:00:00Z") or relative time (e.g., "1h", "2d")', ), }, async ({ projectId, filter, limit, startTime }, context) => { try { // Use provided project ID or get the default one from state manager first const actualProjectId = projectId || stateManager.getCurrentProjectId() || (await getProjectId()); // 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); } // 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(); // Format timestamps in RFC3339 UTC "Zulu" format as required by the API // Example format: "2014-10-02T15:01:23Z" // The Cloud Trace API requires RFC3339 format timestamps const startTimeUTC = startTimeDate.toISOString(); const endTimeUTC = endTime.toISOString(); logger.debug( `Using formatted timestamps: startTime=${startTimeUTC}, endTime=${endTimeUTC}`, ); // Build the query parameters for the request according to the API documentation const queryParams = new URLSearchParams(); // Required parameters - format must be RFC3339 UTC "Zulu" format // The Cloud Trace API requires timestamps in RFC3339 format // Example: "2014-10-02T15:01:23Z" queryParams.append("startTime", startTimeUTC); // Start of the time interval (inclusive) queryParams.append("endTime", endTimeUTC); // End of the time interval (inclusive) // Optional parameters queryParams.append("pageSize", limit.toString()); // Maximum number of traces to return // The view parameter is optional and defaults to MINIMAL // ROOTSPAN includes the root span with the trace // COMPLETE includes all spans with the trace queryParams.append("view", "COMPLETE"); // Type of data returned (MINIMAL, ROOTSPAN, COMPLETE) // Add orderBy parameter to sort by most recent traces first queryParams.append("orderBy", "start desc"); // Sort by start time descending // Optional filter parameter if (filter) { queryParams.append("filter", filter); // Filter against labels for the request } // Construct the URL for the Cloud Trace API v1 endpoint // The correct endpoint format according to the documentation is: // GET https://cloudtrace.googleapis.com/v1/projects/{projectId}/traces const apiUrl = `https://cloudtrace.googleapis.com/v1/projects/${actualProjectId}/traces`; const requestUrl = `${apiUrl}?${queryParams.toString()}`; logger.debug(`List Traces URL: ${requestUrl}`); logger.debug( `List Traces Query Params: ${JSON.stringify(Object.fromEntries(queryParams.entries()))}`, ); logger.debug( `List Traces Time Range: ${startTimeDate.toISOString()} to ${endTime.toISOString()}`, ); logger.debug(`List Traces Raw Query String: ${queryParams.toString()}`); // Fetch traces from the Cloud Trace API logger.debug(`Sending request to Cloud Trace API: ${requestUrl}`); let tracesData; try { const response = await fetch(requestUrl, { method: "GET", headers: { Authorization: `Bearer ${token.token}`, Accept: "application/json", }, }); logger.debug(`List Traces Response Status: ${response.status}`); if (!response.ok) { const errorText = await response.text(); logger.error(`List Traces Error: ${errorText}`); throw new GcpMcpError( `Failed to list traces: ${errorText}`, "FAILED_PRECONDITION", response.status, ); } // Log the full response headers to help debug const responseHeaders: Record<string, string> = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); logger.debug( `List Traces Response Headers: ${JSON.stringify(responseHeaders)}`, ); tracesData = await response.json(); logger.debug( `List Traces Response Data: ${JSON.stringify(tracesData, null, 2)}`, ); } catch (fetchError: any) { logger.error(`Fetch error: ${fetchError.message}`); throw new GcpMcpError( `Failed to fetch traces: ${fetchError.message}`, "INTERNAL", 500, ); } // 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}`, }, ], }; } // Use the helper function to format the response return formatTracesResponse( tracesData, actualProjectId, startTimeDate, endTime, filter, ); } catch (error: any) { // Error handling for list-traces tool throw new GcpMcpError( `Failed to list traces: ${error.message}`, error.code || "UNKNOWN", error.statusCode || 500, ); } }, );
- Helper function formatTracesResponse called by the handler to format the traces API response into a markdown table listing trace details.function formatTracesResponse( tracesData: any, projectId: string, startTime: Date, endTime: Date, filter?: string, ): any { // Format the traces for display let markdown = `# Traces for ${projectId}\n\n`; markdown += `Time Range: ${startTime.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" : trace.status?.code > 0 ? "❌ ERROR" : "⚪ UNKNOWN"; markdown += `| \`${traceId}\` | ${displayName} | ${startTimeStr} | ${duration} | ${status} |\n`; } markdown += "\n\nTo view a specific trace, use the `get-trace` tool with the trace ID."; return { content: [ { type: "text", text: markdown, }, ], }; }
- Supporting helper calculateDuration used in formatTracesResponse to compute trace duration strings.function calculateDuration(startTime: string, endTime: string): string { const start = new Date(startTime).getTime(); const end = new Date(endTime).getTime(); const durationMs = end - start; if (durationMs < 1000) { return `${durationMs}ms`; } else if (durationMs < 60000) { return `${(durationMs / 1000).toFixed(2)}s`; } else { const minutes = Math.floor(durationMs / 60000); const seconds = ((durationMs % 60000) / 1000).toFixed(2); return `${minutes}m ${seconds}s`; } }