UNPKG

google-cloud-mcp

Version:

Model Context Protocol server for Google Cloud services

179 lines 9.29 kB
import { MetricServiceClient } from '@google-cloud/monitoring'; import { z } from 'zod'; import { getProjectId } from '../utils/auth.js'; import { GcpMcpError } from '../utils/error.js'; import { parseRelativeTime } from '../utils/time.js'; /** * Registers the Spanner query count tool with the MCP server * * @param server The MCP server instance */ export function registerSpannerQueryCountTool(server) { server.tool('spanner-query-count', { instanceId: z.string().optional().describe('Spanner instance ID (optional, if not provided will show all instances)'), databaseId: z.string().optional().describe('Spanner database ID (optional, if not provided will show all databases)'), queryType: z.enum(['ALL', 'READ', 'QUERY']).default('ALL').describe('Type of queries to count (ALL, READ, QUERY)'), status: z.enum(['ALL', 'OK', 'ERROR']).default('ALL').describe('Status of queries to count (ALL, OK, ERROR)'), startTime: z.string().default('1h').describe('Start time for the query (e.g., "1h", "2d", "30m")'), endTime: z.string().optional().describe('End time for the query (defaults to now)'), alignmentPeriod: z.string().default('60s').describe('Alignment period for aggregating data points (e.g., "60s", "5m", "1h")') }, async ({ instanceId, databaseId, queryType, status, startTime, endTime, alignmentPeriod }, context) => { try { const projectId = await getProjectId(); const client = new MetricServiceClient({ projectId: process.env.GOOGLE_CLOUD_PROJECT }); console.error(`Retrieving Spanner query count for project ${projectId}`); // Parse time range const start = parseRelativeTime(startTime); const end = endTime ? parseRelativeTime(endTime) : new Date(); // Build filter for the metric let filter = 'metric.type = "spanner.googleapis.com/query_count"'; // Add resource filters if specified if (instanceId) { filter += ` AND resource.labels.instance_id = "${instanceId}"`; } // Add metric label filters if (databaseId) { filter += ` AND metric.labels.database = "${databaseId}"`; } if (queryType !== 'ALL') { filter += ` AND metric.labels.query_type = "${queryType.toLowerCase()}"`; } if (status !== 'ALL') { filter += ` AND metric.labels.status = "${status.toLowerCase()}"`; } console.error(`Using filter: ${filter}`); // 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; } // Build the request const request = { name: `projects/${projectId}`, filter, interval: { startTime: { seconds: Math.floor(start.getTime() / 1000), nanos: 0 }, endTime: { seconds: Math.floor(end.getTime() / 1000), nanos: 0 } }, aggregation: { alignmentPeriod: { seconds }, perSeriesAligner: 'ALIGN_SUM', crossSeriesReducer: 'REDUCE_SUM' } }; // Execute the request console.error('Sending request to Google Cloud Monitoring API...'); const timeSeriesData = await client.listTimeSeries(request); const timeSeries = timeSeriesData[0]; console.error(`Received ${timeSeries?.length || 0} time series`); if (!timeSeries || timeSeries.length === 0) { return { content: [{ type: 'text', text: `# Spanner Query Count\n\nProject: ${projectId}\n${instanceId ? `\nInstance: ${instanceId}` : ''}\n${databaseId ? `\nDatabase: ${databaseId}` : ''}\n\nQuery Type: ${queryType}\nStatus: ${status}\nTime Range: ${start.toISOString()} to ${end.toISOString()}\nAlignment Period: ${alignmentPeriod}\n\nNo query count data found for the specified parameters.` }] }; } // Format the results let markdown = `# Spanner Query Count\n\nProject: ${projectId}\n${instanceId ? `\nInstance: ${instanceId}` : ''}\n${databaseId ? `\nDatabase: ${databaseId}` : ''}\n\nQuery Type: ${queryType}\nStatus: ${status}\nTime Range: ${start.toISOString()} to ${end.toISOString()}\nAlignment Period: ${alignmentPeriod}\n\n`; // Create a table for each time series for (const series of timeSeries) { const seriesData = series; // Extract labels for the table header const instanceName = seriesData.resource.labels.instance_id || 'unknown'; const databaseName = seriesData.metric.labels?.database || 'all'; const queryTypeValue = seriesData.metric.labels?.query_type || 'all'; const statusValue = seriesData.metric.labels?.status || 'all'; const optimizerVersion = seriesData.metric.labels?.optimizer_version || 'unknown'; markdown += `## Instance: ${instanceName}, Database: ${databaseName}\n`; markdown += `Query Type: ${queryTypeValue}, Status: ${statusValue}, Optimizer Version: ${optimizerVersion}\n\n`; // Table header markdown += '| Timestamp | Query Count |\n'; markdown += '|-----------|------------|\n'; // Table rows if (seriesData.points && seriesData.points.length > 0) { // Sort points by time (oldest first) const sortedPoints = [...seriesData.points].sort((a, b) => { const aTime = Number(a.interval.startTime.seconds); const bTime = Number(b.interval.startTime.seconds); return aTime - bTime; }); for (const point of sortedPoints) { const timestamp = new Date(Number(point.interval.endTime.seconds) * 1000).toISOString(); const count = point.value.int64Value || '0'; markdown += `| ${timestamp} | ${count} |\n`; } } else { markdown += '| No data | No data |\n'; } markdown += '\n---\n\n'; } // Add summary if there are multiple series if (timeSeries.length > 1) { markdown += `\n## Summary\n\nTotal number of time series: ${timeSeries.length}\n`; // Calculate total query count across all series let totalCount = 0; for (const series of timeSeries) { const seriesData = series; if (seriesData.points && seriesData.points.length > 0) { for (const point of seriesData.points) { if (point.value.int64Value) { totalCount += parseInt(point.value.int64Value); } } } } markdown += `Total query count: ${totalCount}\n`; } return { content: [{ type: 'text', text: markdown }] }; } catch (error) { console.error('Error in spanner-query-count tool:', error); // Extract error message safely let errorMessage = 'Unknown error'; if (error instanceof Error) { errorMessage = error.message; } else if (typeof error === 'object' && error !== null) { errorMessage = String(error.message || JSON.stringify(error)); } else if (typeof error === 'string') { errorMessage = error; } const errorCode = error?.code || 'UNKNOWN'; const statusCode = error?.statusCode || 500; throw new GcpMcpError(`Failed to retrieve Spanner query count: ${errorMessage}`, errorCode, statusCode); } }); } //# sourceMappingURL=spanner-query-count.js.map