Skip to main content
Glama

Google Cloud MCP Server

by andyl25
tools.js14.9 kB
import { z } from 'zod'; import { getProjectId } from '../../utils/auth.js'; import { GcpMcpError } from '../../utils/error.js'; import { formatTimeSeriesData, getMonitoringClient } from './types.js'; import { parseRelativeTime } from '../../utils/time.js'; import { metricsLookup } from './metrics_lookup.js'; /** * Registers Google Cloud Monitoring tools with the MCP server * * @param server The MCP server instance */ export async function registerMonitoringTools(server) { // Initialize the metrics lookup service try { await metricsLookup.initialize(); } catch (error) { // Failed to initialize metrics lookup service - continuing anyway // Continue even if metrics lookup initialization fails } // Tool to query metrics with a custom filter and time range server.tool('query-metrics', { filter: z.string().describe('The filter to apply to metrics'), startTime: z.string().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 ({ filter, startTime, endTime, alignmentPeriod }, context) => { try { const projectId = await getProjectId(); const client = getMonitoringClient(); const start = parseRelativeTime(startTime); const end = endTime ? parseRelativeTime(endTime) : new Date(); // Build 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 } } }; // 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: `# Metric Query Results\n\nProject: ${projectId}\nFilter: ${filter}\nTime Range: ${start.toISOString()} to ${end.toISOString()}\n\nNo metrics found matching the filter.` }] }; } const formattedData = formatTimeSeriesData(timeSeries); return { content: [{ type: 'text', text: `# Metric Query Results\n\nProject: ${projectId}\nFilter: ${filter}\nTime Range: ${start.toISOString()} to ${end.toISOString()}\n${alignmentPeriod ? `\nAlignment: ${alignmentPeriod}` : ''}\n\n${formattedData}` }] }; } catch (error) { throw new GcpMcpError(`Failed to query metrics: ${error.message}`, error.code || 'UNKNOWN', error.statusCode || 500); } }); // Tool to list available metric types server.tool('list-metric-types', { filter: z.string().optional().describe('Simple search term (e.g., "spanner") or full filter expression (e.g., "metric.type = starts_with(\\"spanner\\")")'), pageSize: z.number().min(1).max(100).default(20).describe('Maximum number of metric types to return'), timeout: z.number().min(5).max(60).default(30).describe('Timeout in seconds for the request') }, async ({ filter, pageSize, timeout }, context) => { try { const projectId = await getProjectId(); const client = getMonitoringClient(); // Format the filter if it's a simple string without operators let formattedFilter = filter; let useClientSideFiltering = false; if (filter && !filter.includes('=') && !filter.includes('>') && !filter.includes('<')) { // If it's just a simple term, we'll use client-side filtering // We don't set a filter for the API call to avoid syntax errors formattedFilter = undefined; useClientSideFiltering = true; } // Create a promise that rejects after the timeout const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Request timed out after ${timeout} seconds`)); }, timeout * 1000); }); // Create the actual request promise const requestPromise = (async () => { const request = { name: `projects/${projectId}`, pageSize }; if (formattedFilter) { request.filter = formattedFilter; } return await client.listMetricDescriptors(request); })(); // Race the timeout against the actual request const [metricDescriptors] = await Promise.race([requestPromise, timeoutPromise]); // Apply client-side filtering if needed let filteredDescriptors = metricDescriptors; if (useClientSideFiltering && filter) { const searchTerm = filter.toLowerCase(); filteredDescriptors = metricDescriptors.filter((descriptor) => { // Search in the type name if (descriptor.type && descriptor.type.toLowerCase().includes(searchTerm)) { return true; } // Search in the display name if (descriptor.displayName && descriptor.displayName.toLowerCase().includes(searchTerm)) { return true; } // Search in the description if (descriptor.description && descriptor.description.toLowerCase().includes(searchTerm)) { return true; } return false; }); } if (!filteredDescriptors || filteredDescriptors.length === 0) { return { content: [{ type: 'text', text: `# Available Metric Types\n\nProject: ${projectId}\n${filter ? `\nSearch term: "${filter}"` : ''}\n\nNo metric types found matching your search term. Try a different search term or increase the timeout.` }] }; } let markdown = `# Available Metric Types\n\nProject: ${projectId}\n${filter ? `\nSearch term: "${filter}"` : ''}\n${useClientSideFiltering ? `\n*Note: Filtering was performed client-side by searching for "${filter}" in metric type, display name, and description.*` : ''}\n\nFound ${filteredDescriptors.length} metric types${metricDescriptors.length !== filteredDescriptors.length ? ` (filtered from ${metricDescriptors.length} total)` : ''}.\n\n`; // Table header markdown += '| Metric Type | Display Name | Kind | Value Type | Description |\n'; markdown += '|-------------|--------------|------|------------|-------------|\n'; // Table rows - limit to first 50 to avoid excessive output const limitedDescriptors = filteredDescriptors.slice(0, 50); for (const descriptor of limitedDescriptors) { const description = (descriptor.description || '').replace(/\n/g, ' ').substring(0, 100); markdown += `| ${descriptor.type || ''} | ${descriptor.displayName || ''} | ${descriptor.metricKind || ''} | ${descriptor.valueType || ''} | ${description} |\n`; } if (filteredDescriptors.length > 50) { markdown += `\n*Note: Showing first 50 of ${filteredDescriptors.length} metric types. Use a more specific search term to narrow down results.*`; } return { content: [{ type: 'text', text: markdown }] }; } catch (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; } // Check for timeout errors if (errorMessage.includes('timed out')) { throw new GcpMcpError(`Request timed out. Try using a filter to narrow down results or increase the timeout parameter.`, 'DEADLINE_EXCEEDED', 504); } // Handle other errors const errorCode = error?.code || 'UNKNOWN'; const statusCode = error?.statusCode || 500; throw new GcpMcpError(`Failed to list metric types: ${errorMessage}`, errorCode, statusCode); } }); // Tool to query metrics using natural language 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 = { 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) { // 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); } }); } //# sourceMappingURL=tools.js.map

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/andyl25/googlecloud-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server