natural-language-metrics-query
Query Google Cloud Monitoring metrics using natural language. Specify time ranges and alignment periods to retrieve monitoring data without writing complex queries.
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:266-365 (handler)The main handler function that processes natural language queries by generating a metric filter using metricsLookup.suggestFilter, constructs a Monitoring API request with time range and optional alignment, fetches time series data, and formats the results for display.async ({ query, startTime, endTime, alignmentPeriod }, context) => { 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 as unknown as TimeSeriesData[]); 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 ); }
- Zod schema defining the input parameters for the natural-language-metrics-query tool: query (required string), optional startTime, endTime, and alignmentPeriod.{ 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:258-367 (registration)Registers the 'natural-language-metrics-query' tool on the MCP server with input schema and handler function inside the registerMonitoringTools function.server.tool( 'natural-language-metrics-query', { 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")') }, async ({ query, startTime, endTime, alignmentPeriod }, context) => { 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 as unknown as TimeSeriesData[]); 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 ); } } );
- Core helper method that translates natural language queries into Google Cloud Monitoring filter strings by finding the best matching metric and extracting resource types and labels from the query.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; }