google-cloud-mcp
Version:
Model Context Protocol server for Google Cloud services
179 lines • 9.29 kB
JavaScript
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