google-cloud-mcp
Version:
Model Context Protocol server for Google Cloud services
422 lines (418 loc) • 18.1 kB
JavaScript
/**
* Google Cloud Logging integration for MCP
*/
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { Logging } from '@google-cloud/logging';
import { z } from 'zod';
import { getProjectId } from '../utils/auth.js';
import { GcpMcpError } from '../utils/error.js';
/**
* Initialises the Google Cloud Logging client
*
* @returns A configured Logging client
*/
function getLoggingClient() {
return new Logging({
projectId: process.env.GOOGLE_CLOUD_PROJECT
});
}
/**
* Formats a log entry for display
*
* @param entry The log entry to format
* @returns A formatted string representation of the log entry
*/
function formatLogEntry(entry) {
// Safely format the timestamp
let timestamp;
try {
// Check if timestamp exists and is valid
if (!entry.timestamp) {
timestamp = 'No timestamp';
}
else {
const date = new Date(entry.timestamp);
timestamp = !isNaN(date.getTime()) ? date.toISOString() : String(entry.timestamp);
}
}
catch (error) {
console.error('Error formatting timestamp:', error, 'Timestamp value:', entry.timestamp);
timestamp = String(entry.timestamp || 'Invalid timestamp');
}
const severity = entry.severity || 'DEFAULT';
const resourceType = entry.resource?.type || 'unknown';
const resourceLabels = entry.resource?.labels
? Object.entries(entry.resource.labels)
.map(([k, v]) => `${k}=${v}`)
.join(', ')
: '';
const resource = resourceLabels
? `${resourceType}(${resourceLabels})`
: resourceType;
// Format the payload with better error handling
let payload;
try {
if (entry.textPayload !== undefined && entry.textPayload !== null) {
payload = String(entry.textPayload);
}
else if (entry.jsonPayload) {
payload = JSON.stringify(entry.jsonPayload, null, 2);
}
else if (entry.protoPayload) {
payload = JSON.stringify(entry.protoPayload, null, 2);
}
else if (entry.data) {
payload = JSON.stringify(entry.data, null, 2);
}
else {
payload = '[No payload]';
}
}
catch (error) {
console.error('Error formatting log payload:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
payload = `[Error formatting payload: ${errorMessage}]`;
}
// Format labels if they exist
let labelsStr = '';
if (entry.labels && Object.keys(entry.labels).length > 0) {
try {
labelsStr = Object.entries(entry.labels)
.map(([k, v]) => `${k}=${v}`)
.join(', ');
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
labelsStr = `[Error formatting labels: ${errorMessage}]`;
}
}
// Format operation if it exists
let operationStr = '';
if (entry.operation) {
try {
const op = entry.operation;
operationStr = [
op.id ? `id=${op.id}` : '',
op.producer ? `producer=${op.producer}` : '',
op.first ? 'first=true' : '',
op.last ? 'last=true' : ''
].filter(Boolean).join(', ');
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
operationStr = `[Error formatting operation: ${errorMessage}]`;
}
}
// Create a more detailed and markdown-friendly format
return `## ${timestamp} | ${severity} | ${resource}
` +
(entry.logName ? `**Log:** ${entry.logName}\n` : '') +
(entry.insertId ? `**ID:** ${entry.insertId}\n` : '') +
(labelsStr ? `**Labels:** ${labelsStr}\n` : '') +
(operationStr ? `**Operation:** ${operationStr}\n` : '') +
`\n\`\`\`\n${payload}\n\`\`\``;
}
/**
* Registers Google Cloud Logging resources with the MCP server
*
* @param server The MCP server instance
*/
export function registerLoggingResources(server) {
// Register a resource for listing recent logs
server.resource('recent-logs', new ResourceTemplate('gcp-logs://{projectId}/recent', { list: undefined }), async (uri, { projectId }, _extra) => {
try {
const actualProjectId = projectId || await getProjectId();
const logging = getLoggingClient();
const defaultFilter = process.env.LOG_FILTER || '';
const [entries] = await logging.getEntries({
pageSize: 50,
filter: defaultFilter
});
if (!entries || entries.length === 0) {
return {
contents: [{
uri: uri.href,
text: 'No log entries found.'
}]
};
}
// Format logs with error handling for each entry
const formattedLogs = entries
.map((entry) => {
try {
return formatLogEntry(entry);
}
catch (err) {
console.error('Error formatting log entry:', err);
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
return `## Error Formatting Log Entry\n\nAn error occurred while formatting a log entry: ${errorMessage}`;
}
})
.join('\n\n');
return {
contents: [{
uri: uri.href,
text: `# Recent Logs for Project: ${actualProjectId}\n\n${formattedLogs}`
}]
};
}
catch (error) {
console.error('Error fetching recent logs:', error);
// Get project ID safely
let projectIdForError;
try {
projectIdForError = Array.isArray(projectId) ? projectId[0] : (projectId || process.env.GOOGLE_CLOUD_PROJECT || 'unknown');
}
catch {
projectIdForError = 'unknown';
}
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Return a user-friendly error message instead of throwing
return {
contents: [{
uri: uri.href,
text: `# Error Fetching Recent Logs\n\nAn error occurred while fetching recent logs for project ${projectIdForError}: ${errorMessage}\n\nPlease check your Google Cloud credentials and project configuration.`
}]
};
}
});
// Register a resource for querying logs with a filter
server.resource('filtered-logs', new ResourceTemplate('gcp-logs://{projectId}/filter/{filter}', { list: undefined }), async (uri, { projectId, filter }, _extra) => {
try {
const actualProjectId = projectId || await getProjectId();
const logging = getLoggingClient();
if (!filter) {
throw new GcpMcpError('Log filter is required', 'INVALID_ARGUMENT', 400);
}
const decodedFilter = Array.isArray(filter) ? decodeURIComponent(filter[0]) : decodeURIComponent(filter);
const [entries] = await logging.getEntries({
pageSize: 50,
filter: decodedFilter
});
if (!entries || entries.length === 0) {
return {
contents: [{
uri: uri.href,
text: `No log entries found matching filter: ${decodedFilter}`
}]
};
}
// Format logs with error handling for each entry
const formattedLogs = entries
.map((entry) => {
try {
return formatLogEntry(entry);
}
catch (err) {
console.error('Error formatting log entry:', err);
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
return `## Error Formatting Log Entry\n\nAn error occurred while formatting a log entry: ${errorMessage}`;
}
})
.join('\n\n');
return {
contents: [{
uri: uri.href,
text: `# Filtered Logs for Project: ${actualProjectId}\n\nFilter: ${decodedFilter}\n\n${formattedLogs}`
}]
};
}
catch (error) {
console.error('Error fetching filtered logs:', error);
// Get project ID and filter safely
let projectIdForError;
let filterForError;
try {
projectIdForError = Array.isArray(projectId) ? projectId[0] : (projectId || process.env.GOOGLE_CLOUD_PROJECT || 'unknown');
filterForError = Array.isArray(filter) ? filter[0] : String(filter || 'unknown');
}
catch {
projectIdForError = 'unknown';
filterForError = 'unknown';
}
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Return a user-friendly error message instead of throwing
return {
contents: [{
uri: uri.href,
text: `# Error Fetching Filtered Logs\n\nAn error occurred while fetching logs with filter "${filterForError}" for project ${projectIdForError}: ${errorMessage}\n\nPlease check your filter syntax and Google Cloud credentials.`
}]
};
}
});
}
/**
* Registers Google Cloud Logging tools with the MCP server
*
* @param server The MCP server instance
*/
export function registerLoggingTools(server) {
// Tool to query logs with a custom filter
server.tool('query-logs', {
filter: z.string().describe('The filter to apply to logs'),
limit: z.number().min(1).max(1000).default(50).describe('Maximum number of log entries to return')
}, async ({ filter, limit }, _extra) => {
try {
const projectId = await getProjectId();
const logging = getLoggingClient();
const [entries] = await logging.getEntries({
pageSize: limit,
filter
});
if (!entries || entries.length === 0) {
return {
content: [{
type: 'text',
text: `No log entries found matching filter: ${filter}`
}]
};
}
const formattedLogs = entries
.map((entry) => {
try {
return formatLogEntry(entry);
}
catch (err) {
console.error('Error formatting log entry:', err);
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
return `## Error Formatting Log Entry\n\nAn error occurred while formatting a log entry: ${errorMessage}`;
}
})
.join('\n\n');
return {
content: [{
type: 'text',
text: `# Log Query Results\n\nProject: ${projectId}\nFilter: ${filter}\nEntries: ${entries.length}\n\n${formattedLogs}`
}]
};
}
catch (error) {
console.error('Error querying logs:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Return a user-friendly error message instead of throwing
return {
content: [{
type: 'text',
text: `# Error Querying Logs
An error occurred while querying logs: ${errorMessage}
Please check your filter syntax and try again. For filter syntax help, see: https://cloud.google.com/logging/docs/view/logging-query-language`
}],
isError: true
};
}
});
// Tool to get logs for a specific time range
server.tool('logs-time-range', {
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)'),
filter: z.string().optional().describe('Additional filter criteria'),
limit: z.number().min(1).max(1000).default(50).describe('Maximum number of log entries to return')
}, async ({ startTime, endTime, filter, limit }, _extra) => {
try {
const projectId = await getProjectId();
const logging = getLoggingClient();
// Parse relative time expressions like "1h", "2d"
const parseRelativeTime = (timeStr) => {
try {
// Handle relative time expressions
if (typeof timeStr === 'string' && timeStr.match(/^\d+[hdwmy]$/i)) {
const now = new Date();
const value = parseInt(timeStr.slice(0, -1));
const unit = timeStr.slice(-1).toLowerCase();
if (isNaN(value)) {
throw new GcpMcpError(`Invalid time value in: ${timeStr}`, 'INVALID_ARGUMENT', 400);
}
const result = new Date(now);
switch (unit) {
case 'h': // hours
result.setHours(result.getHours() - value);
break;
case 'd': // days
result.setDate(result.getDate() - value);
break;
case 'w': // weeks
result.setDate(result.getDate() - (value * 7));
break;
case 'm': // months
result.setMonth(result.getMonth() - value);
break;
case 'y': // years
result.setFullYear(result.getFullYear() - value);
break;
default:
throw new GcpMcpError(`Invalid time unit: ${unit}`, 'INVALID_ARGUMENT', 400);
}
return result;
}
// Try to parse as ISO date
const date = new Date(timeStr);
if (isNaN(date.getTime())) {
throw new GcpMcpError(`Invalid time format: ${timeStr}`, 'INVALID_ARGUMENT', 400);
}
return date;
}
catch (error) {
if (error instanceof GcpMcpError) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new GcpMcpError(`Error parsing time: ${timeStr} - ${errorMessage}`, 'INVALID_ARGUMENT', 400);
}
};
const start = parseRelativeTime(startTime);
const end = endTime ? parseRelativeTime(endTime) : new Date();
// Build filter string
let filterStr = `timestamp >= "${start.toISOString()}" AND timestamp <= "${end.toISOString()}"`;
if (filter) {
filterStr = `${filterStr} AND ${filter}`;
}
const [entries] = await logging.getEntries({
pageSize: limit,
filter: filterStr
});
if (!entries || entries.length === 0) {
return {
content: [{
type: 'text',
text: `No log entries found in the specified time range with filter: ${filterStr}`
}]
};
}
const formattedLogs = entries
.map((entry) => {
try {
return formatLogEntry(entry);
}
catch (err) {
console.error('Error formatting log entry:', err);
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
return `## Error Formatting Log Entry\n\nAn error occurred while formatting a log entry: ${errorMessage}`;
}
})
.join('\n\n');
return {
content: [{
type: 'text',
text: `# Log Time Range Results\n\nProject: ${projectId}\nTime Range: ${start.toISOString()} to ${end.toISOString()}\nFilter: ${filter || 'None'}\nEntries: ${entries.length}\n\n${formattedLogs}`
}]
};
}
catch (error) {
console.error('Error querying logs with time range:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Return a user-friendly error message instead of throwing
return {
content: [{
type: 'text',
text: `# Error Querying Logs
An error occurred while querying logs: ${errorMessage}
Please check your time range format and try again. Valid formats include:
- ISO date strings (e.g., "2025-03-01T00:00:00Z")
- Relative time expressions: "1h" (1 hour ago), "2d" (2 days ago), "1w" (1 week ago), etc.`
}],
isError: true
};
}
});
}
//# sourceMappingURL=logging.js.map