gcp-monitoring-query-natural-language
Query Google Cloud Monitoring metrics using natural language descriptions instead of complex query syntax. Specify time ranges and alignment periods to retrieve monitoring data.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Natural language description of the query you want to execute | |
| startTime | No | Start time in ISO format or relative time (e.g., "1h", "2d") | |
| endTime | No | End time in ISO format (defaults to now) | |
| alignmentPeriod | No | Alignment period (e.g., "60s", "300s") |
Implementation Reference
- src/services/monitoring/tools.ts:339-446 (handler)The core handler function for the tool. It processes the natural language query by using metricsLookup.suggestFilter to generate a GCP Monitoring filter, parses time ranges, calls the GCP Monitoring API via listTimeSeries, formats the results with formatTimeSeriesData, and returns markdown content.async ({ query, startTime, endTime, alignmentPeriod }) => { try { const projectId = await getProjectId(); // Use the metrics lookup to suggest a filter based on the natural language query const suggestedFilter = metricsLookup.suggestFilter(query); if (!suggestedFilter) { throw new GcpMcpError( "Could not determine an appropriate metric filter from your query. Please try a more specific query that mentions a metric type.", "INVALID_ARGUMENT", 400, ); } // Use default time range if not specified const start = startTime ? parseRelativeTime(startTime) : parseRelativeTime("1h"); const end = endTime ? parseRelativeTime(endTime) : new Date(); const client = getMonitoringClient(); // Build request const request: any = { name: `projects/${projectId}`, filter: suggestedFilter, interval: { startTime: { seconds: Math.floor(start.getTime() / 1000), nanos: 0, }, endTime: { seconds: Math.floor(end.getTime() / 1000), nanos: 0, }, }, }; // Add alignment if specified if (alignmentPeriod) { // Parse alignment period (e.g., "60s" -> 60 seconds) const match = alignmentPeriod.match(/^(\d+)([smhd])$/); if (!match) { throw new GcpMcpError( 'Invalid alignment period format. Use format like "60s", "5m", "1h".', "INVALID_ARGUMENT", 400, ); } const value = parseInt(match[1]); const unit = match[2]; let seconds = value; switch (unit) { case "m": // minutes seconds = value * 60; break; case "h": // hours seconds = value * 60 * 60; break; case "d": // days seconds = value * 60 * 60 * 24; break; } request.aggregation = { alignmentPeriod: { seconds: seconds, }, perSeriesAligner: "ALIGN_MEAN", }; } const [timeSeries] = await client.listTimeSeries(request); if (!timeSeries || timeSeries.length === 0) { return { content: [ { type: "text", text: `# Natural Language Query Results\n\nProject: ${projectId}\nQuery: ${query}\nGenerated Filter: ${suggestedFilter}\nTime Range: ${start.toISOString()} to ${end.toISOString()}\n\nNo metrics found matching the filter.\n\nTry refining your query to be more specific about the metric type, resource type, or labels.`, }, ], }; } const formattedData = formatTimeSeriesData(timeSeries); return { content: [ { type: "text", text: `# Natural Language Query Results\n\nProject: ${projectId}\nQuery: ${query}\nGenerated Filter: ${suggestedFilter}\nTime Range: ${start.toISOString()} to ${end.toISOString()}${alignmentPeriod ? `\nAlignment: ${alignmentPeriod}` : ""}\n\n${formattedData}`, }, ], }; } catch (error: any) { // Error handling for natural-language-metrics-query tool throw new GcpMcpError( `Failed to execute natural language query: ${error.message}`, error.code || "UNKNOWN", error.statusCode || 500, ); } }, );
- Input schema defined using Zod, validating the natural language query and optional time/alignment parameters.query: z .string() .describe( "Natural language description of the query you want to execute", ), startTime: z .string() .optional() .describe( 'Start time in ISO format or relative time (e.g., "1h", "2d")', ), endTime: z .string() .optional() .describe("End time in ISO format (defaults to now)"), alignmentPeriod: z .string() .optional() .describe('Alignment period (e.g., "60s", "300s")'), },
- src/services/monitoring/tools.ts:317-318 (registration)The MCP server.tool registration call for this specific tool."gcp-monitoring-query-natural-language", {
- Key helper method that converts natural language query to a GCP Monitoring filter string by finding matching metrics and extracting conditions for resources and labels.suggestFilter(query: string): string { const metrics = this.findMetrics(query); if (metrics.length === 0) { return ""; } // Use the top matching metric to create a filter const topMetric = metrics[0]; // Basic filter with just the metric type let filter = `metric.type="${topMetric.type}"`; // Try to extract additional filter conditions from the query const resourceMatch = /resource\s+(?:type|is|equals?)\s+["']?([a-zA-Z0-9_]+)["']?/i.exec(query); if (resourceMatch && resourceMatch[1]) { filter += ` AND resource.type="${resourceMatch[1]}"`; } // Look for label conditions for (const label of topMetric.labels) { const labelRegex = new RegExp( `${label.name}\\s+(?:is|equals?|=)\\s+["']?([\\w-]+)["']?`, "i", ); const match = labelRegex.exec(query); if (match && match[1]) { filter += ` AND metric.labels.${label.name}="${match[1]}"`; } } return filter; }
- Helper function to format the raw GCP time series data into readable Markdown tables.export function formatTimeSeriesData( timeSeries: google.monitoring.v3.ITimeSeries[], ): string { if (!timeSeries || timeSeries.length === 0) { return "No time series data found."; } let result = ""; for (const series of timeSeries) { // Format metric information const metricType = series.metric?.type; const metricLabels = series.metric?.labels ? Object.entries(series.metric?.labels) .map(([k, v]) => `${k}=${v}`) .join(", ") : ""; const resourceType = series.resource?.type; const resourceLabels = Object.entries(series.resource?.labels ?? {}) .map(([k, v]) => `${k}=${v}`) .join(", "); result += `## Metric: ${metricType}\n`; result += `- Resource: ${resourceType}(${resourceLabels})\n`; if (metricLabels) { result += `- Labels: ${metricLabels}\n`; } result += `- Kind: ${series.metricKind}, Type: ${series.valueType}\n\n`; // Format data points result += "| Timestamp | Value |\n"; result += "|-----------|-------|\n"; for (const point of series.points ?? []) { const timestamp = new Date( Number(point.interval?.endTime?.seconds) * 1000, ).toISOString(); // Extract the value based on valueType let value: string; if (point.value?.boolValue !== undefined) { value = String(point.value?.boolValue) ?? "N/A"; } else if (point.value?.int64Value !== undefined) { value = point.value?.int64Value?.toString() ?? "N/A"; } else if (point.value?.doubleValue !== undefined) { value = point.value?.doubleValue?.toFixed(6) ?? "N/A"; } else if (point.value?.stringValue !== undefined) { value = point.value?.stringValue ?? "N/A"; } else if (point.value?.distributionValue) { value = "Distribution"; } else { value = "N/A"; } result += `| ${timestamp} | ${value} |\n`; } result += "\n---\n\n"; } return result; }