diff --git a/.gitignore b/.gitignore index 8446722..dda2037 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ CLAUDE.md TODO.md -# Build output +# # Build output dist/ build/ diff --git a/package.json b/package.json index 15336c3..530778f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "google-cloud-mcp", - "version": "0.3.0", + "version": "0.4.0", "description": "Model Context Protocol server for Google Cloud services", "type": "module", "main": "dist/index.js", @@ -27,11 +27,12 @@ "iam", "logging", "monitoring", + "profiler", "spanner", "trace" ], "author": "Kristof Kowalski ", - "license": "MIT", + "license": "Apache", "dependencies": { "@google-cloud/iam": "^2.3.0", "@google-cloud/logging": "^11.2.0", diff --git a/src/index.ts b/src/index.ts index f181e67..405d6a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ * Google Cloud MCP Server * * This server provides Model Context Protocol resources and tools for interacting - * with Google Cloud services (Error Reporting, IAM, Logging, Monitoring, Spanner, and Trace). + * with Google Cloud services (Error Reporting, IAM, Logging, Monitoring, Profiler, Spanner, and Trace). */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import dotenv from "dotenv"; @@ -30,6 +30,10 @@ import { registerErrorReportingResources, registerErrorReportingTools, } from "./services/error-reporting/index.js"; +import { + registerProfilerResources, + registerProfilerTools, +} from "./services/profiler/index.js"; import { registerPrompts } from "./prompts/index.js"; import { initGoogleAuth, authClient } from "./utils/auth.js"; import { registerResourceDiscovery } from "./utils/resource-discovery.js"; @@ -212,6 +216,17 @@ async function main(): Promise { ); } + try { + // Register Google Cloud Profiler service + logger.info("Registering Google Cloud Profiler services"); + registerProfilerResources(server); + registerProfilerTools(server); + } catch (error) { + logger.warn( + `Error registering Profiler services: ${error instanceof Error ? error.message : String(error)}`, + ); + } + try { // Register additional tools logger.info("Registering additional tools"); diff --git a/src/services/error-reporting/resources.ts b/src/services/error-reporting/resources.ts index a31d6a3..574575c 100644 --- a/src/services/error-reporting/resources.ts +++ b/src/services/error-reporting/resources.ts @@ -20,7 +20,7 @@ import { export function registerErrorReportingResources(server: McpServer): void { // Register a resource for recent error analysis server.resource( - "recent-errors", + "gcp-error-reporting-recent-errors", new ResourceTemplate("gcp-error-reporting://{projectId}/recent", { list: undefined, }), @@ -110,7 +110,7 @@ export function registerErrorReportingResources(server: McpServer): void { // Register a resource for error analysis with custom time range server.resource( - "error-analysis", + "gcp-error-reporting-error-analysis", new ResourceTemplate( "gcp-error-reporting://{projectId}/analysis/{timeRange}", { list: undefined }, @@ -228,7 +228,7 @@ export function registerErrorReportingResources(server: McpServer): void { // Register a resource for service-specific error analysis server.resource( - "service-errors", + "gcp-error-reporting-service-errors", new ResourceTemplate( "gcp-error-reporting://{projectId}/service/{serviceName}", { list: undefined }, diff --git a/src/services/error-reporting/tools.ts b/src/services/error-reporting/tools.ts index 1a75336..ea6ecc7 100644 --- a/src/services/error-reporting/tools.ts +++ b/src/services/error-reporting/tools.ts @@ -19,7 +19,7 @@ import { export function registerErrorReportingTools(server: McpServer): void { // Tool to list error groups with filtering and time range support server.tool( - "list-error-groups", + "gcp-error-reporting-list-groups", { title: "List Error Groups", description: @@ -178,7 +178,7 @@ export function registerErrorReportingTools(server: McpServer): void { // Tool to get detailed information about a specific error group server.tool( - "get-error-group-details", + "gcp-error-reporting-get-group-details", { title: "Get Error Group Details", description: @@ -407,7 +407,7 @@ export function registerErrorReportingTools(server: McpServer): void { // Tool to analyse error trends over time server.tool( - "analyse-error-trends", + "gcp-error-reporting-analyse-trends", { title: "Analyse Error Trends", description: diff --git a/src/services/iam/resources.ts b/src/services/iam/resources.ts index 7a2f22d..09bdc37 100644 --- a/src/services/iam/resources.ts +++ b/src/services/iam/resources.ts @@ -22,7 +22,7 @@ import { logger } from "../../utils/logger.js"; export function registerIamResources(server: McpServer): void { // Register a resource for project IAM policy server.resource( - "project-iam-policy", + "gcp-iam-project-policy", new ResourceTemplate("gcp-iam://{projectId}/policy", { list: undefined }), async (uri, { projectId }) => { try { @@ -75,7 +75,7 @@ export function registerIamResources(server: McpServer): void { // Register a resource for IAM policy analysis summary server.resource( - "iam-policy-summary", + "gcp-iam-policy-summary", new ResourceTemplate("gcp-iam://{projectId}/summary", { list: undefined }), async (uri, { projectId }) => { try { diff --git a/src/services/iam/tools.ts b/src/services/iam/tools.ts index 437fb24..89385b0 100644 --- a/src/services/iam/tools.ts +++ b/src/services/iam/tools.ts @@ -22,7 +22,7 @@ import { logger } from "../../utils/logger.js"; export function registerIamTools(server: McpServer): void { // Tool to get project-level IAM policy server.registerTool( - "get-project-iam-policy", + "gcp-iam-get-project-policy", { title: "Get Project IAM Policy", description: "Retrieve the IAM policy for a Google Cloud project", @@ -92,7 +92,7 @@ export function registerIamTools(server: McpServer): void { // Tool to test IAM permissions on a project server.registerTool( - "test-project-permissions", + "gcp-iam-test-project-permissions", { title: "Test Project IAM Permissions", description: @@ -174,7 +174,7 @@ export function registerIamTools(server: McpServer): void { // Tool to test permissions on specific resources server.registerTool( - "test-resource-permissions", + "gcp-iam-test-resource-permissions", { title: "Test Resource-Specific IAM Permissions", description: @@ -254,7 +254,7 @@ export function registerIamTools(server: McpServer): void { // Tool to validate deployment permissions for common GCP services server.registerTool( - "validate-deployment-permissions", + "gcp-iam-validate-deployment-permissions", { title: "Validate Deployment Permissions", description: @@ -407,7 +407,7 @@ export function registerIamTools(server: McpServer): void { // Tool to list all available deployment permission sets server.registerTool( - "list-deployment-services", + "gcp-iam-list-deployment-services", { title: "List Available Deployment Services", description: @@ -467,7 +467,7 @@ export function registerIamTools(server: McpServer): void { // Tool to analyse permission gaps for a specific resource and operation server.registerTool( - "analyse-permission-gaps", + "gcp-iam-analyse-permission-gaps", { title: "Analyse Permission Gaps", description: diff --git a/src/services/logging/resources.ts b/src/services/logging/resources.ts index 6f2e0c3..42de0a4 100644 --- a/src/services/logging/resources.ts +++ b/src/services/logging/resources.ts @@ -17,7 +17,7 @@ import { formatLogEntry, getLoggingClient, LogEntry } from "./types.js"; export function registerLoggingResources(server: McpServer): void { // Register a resource for listing recent logs server.resource( - "recent-logs", + "gcp-logging-recent-logs", new ResourceTemplate("gcp-logs://{projectId}/recent", { list: undefined }), async (uri, { projectId }) => { try { @@ -93,7 +93,7 @@ export function registerLoggingResources(server: McpServer): void { // Register a resource for querying logs with a filter server.resource( - "filtered-logs", + "gcp-logging-filtered-logs", new ResourceTemplate("gcp-logs://{projectId}/filter/{filter}", { list: undefined, }), diff --git a/src/services/logging/tools.ts b/src/services/logging/tools.ts index 6d70678..2b2036f 100644 --- a/src/services/logging/tools.ts +++ b/src/services/logging/tools.ts @@ -15,7 +15,7 @@ import { parseRelativeTime } from "../../utils/time.js"; export function registerLoggingTools(server: McpServer): void { // Tool to query logs with a custom filter server.registerTool( - "query-logs", + "gcp-logging-query-logs", { title: "Query Logs", description: @@ -99,7 +99,7 @@ Please check your filter syntax and try again. For filter syntax help, see: http // Tool to get logs for a specific time range server.registerTool( - "logs-time-range", + "gcp-logging-query-time-range", { title: "Query Logs by Time Range", description: @@ -199,7 +199,7 @@ Please check your time range format and try again. Valid formats include: // Advanced tool for searching across all payload types and fields server.registerTool( - "search-logs-comprehensive", + "gcp-logging-search-comprehensive", { title: "Comprehensive Log Search", description: diff --git a/src/services/monitoring/resources.ts b/src/services/monitoring/resources.ts index 406ebdf..0704b2e 100644 --- a/src/services/monitoring/resources.ts +++ b/src/services/monitoring/resources.ts @@ -17,7 +17,7 @@ import { formatTimeSeriesData, getMonitoringClient } from "./types.js"; export function registerMonitoringResources(server: McpServer): void { // Register a resource for recent metrics server.resource( - "recent-metrics", + "gcp-monitoring-recent-metrics", new ResourceTemplate("gcp-monitoring://{projectId}/recent", { list: undefined, }), @@ -83,7 +83,7 @@ export function registerMonitoringResources(server: McpServer): void { // Register a resource for metrics with a custom filter server.resource( - "filtered-metrics", + "gcp-monitoring-filtered-metrics", new ResourceTemplate("gcp-monitoring://{projectId}/filter/{filter}", { list: undefined, }), diff --git a/src/services/monitoring/tools.ts b/src/services/monitoring/tools.ts index 6ca68cd..7a4062e 100644 --- a/src/services/monitoring/tools.ts +++ b/src/services/monitoring/tools.ts @@ -26,7 +26,7 @@ export async function registerMonitoringTools( } // Tool to query metrics with a custom filter and time range server.tool( - "query-metrics", + "gcp-monitoring-query-metrics", { filter: z.string().describe("The filter to apply to metrics"), startTime: z @@ -138,7 +138,7 @@ export async function registerMonitoringTools( // Tool to list available metric types server.tool( - "list-metric-types", + "gcp-monitoring-list-metric-types", { filter: z .string() @@ -314,7 +314,7 @@ export async function registerMonitoringTools( // Tool to query metrics using natural language server.tool( - "natural-language-metrics-query", + "gcp-monitoring-query-natural-language", { query: z .string() diff --git a/src/services/profiler/index.ts b/src/services/profiler/index.ts new file mode 100644 index 0000000..f32d9a6 --- /dev/null +++ b/src/services/profiler/index.ts @@ -0,0 +1,6 @@ +/** + * Google Cloud Profiler service exports + */ +export { registerProfilerTools } from "./tools.js"; +export { registerProfilerResources } from "./resources.js"; +export * from "./types.js"; diff --git a/src/services/profiler/resources.ts b/src/services/profiler/resources.ts new file mode 100644 index 0000000..79dea57 --- /dev/null +++ b/src/services/profiler/resources.ts @@ -0,0 +1,482 @@ +/** + * Google Cloud Profiler resources for MCP + */ +import { + McpServer, + ResourceTemplate, +} from "@modelcontextprotocol/sdk/server/mcp.js"; +import { getProjectId, initGoogleAuth } from "../../utils/auth.js"; +import { GcpMcpError } from "../../utils/error.js"; +import { + analyseProfilePatterns, + formatProfileSummary, + getProfileTypeDescription, + Profile, + ProfileType, + ListProfilesResponse, +} from "./types.js"; + +/** + * Registers Google Cloud Profiler resources with the MCP server + * + * @param server The MCP server instance + */ +export function registerProfilerResources(server: McpServer): void { + // Resource template for listing all profiles with analysis + server.resource( + "gcp-profiler-all-profiles", + new ResourceTemplate("gcp-profiler://{projectId}/profiles", { + list: undefined, + }), + async (uri, { projectId }) => { + try { + const actualProjectId = projectId || (await getProjectId()); + + // Initialize Google Auth client (same pattern as error reporting) + const auth = await initGoogleAuth(true); + if (!auth) { + throw new GcpMcpError( + "Google Cloud authentication not available. Please configure authentication to access profiler data.", + "UNAUTHENTICATED", + 401, + ); + } + const client = await auth.getClient(); + const token = await client.getAccessToken(); + + // Build query parameters for comprehensive data collection + const params = new URLSearchParams({ + pageSize: "100", + }); + + // Make REST API call to list profiles + const apiUrl = `https://cloudprofiler.googleapis.com/v2/projects/${actualProjectId}/profiles?${params}`; + + const response = await fetch(apiUrl, { + method: "GET", + headers: { + Authorization: `Bearer ${token.token}`, + Accept: "application/json", + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new GcpMcpError( + `Failed to fetch profiles: ${errorText}`, + "FAILED_PRECONDITION", + response.status, + ); + } + + const data: ListProfilesResponse = await response.json(); + const profiles = data.profiles || []; + + if (!profiles || profiles.length === 0) { + return { + contents: [ + { + uri: uri.href, + text: `# Google Cloud Profiler Profiles\n\nProject: ${actualProjectId}\n\nNo profiles found. Ensure Cloud Profiler is enabled and collecting data for your applications.`, + mimeType: "text/markdown", + }, + ], + }; + } + + // Generate comprehensive analysis + const analysis = analyseProfilePatterns(profiles); + + let content = `# Google Cloud Profiler Profiles\n\nProject: ${actualProjectId}\nTotal Profiles: ${profiles.length}\n`; + if (data.nextPageToken) + content += `More profiles available (truncated view)\n`; + if (data.skippedProfiles) + content += `Skipped Profiles: ${data.skippedProfiles}\n`; + content += `\n${analysis}\n\n`; + + // Add profile details + content += `## Profile Details\n\n`; + profiles.forEach((profile, index) => { + content += `### Profile ${index + 1}\n${formatProfileSummary(profile)}\n`; + }); + + return { + contents: [ + { + uri: uri.href, + text: content, + mimeType: "text/markdown", + }, + ], + }; + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + throw new GcpMcpError( + `Failed to fetch profiler profiles resource: ${errorMessage}`, + "INTERNAL_ERROR", + 500, + ); + } + }, + ); + + // Resource template for CPU profiles with specific analysis + server.resource( + "gcp-profiler-cpu-profiles", + new ResourceTemplate("gcp-profiler://{projectId}/cpu-profiles", { + list: undefined, + }), + async (uri, { projectId }) => { + try { + const actualProjectId = projectId || (await getProjectId()); + + // Initialize Google Auth client + const auth = await initGoogleAuth(true); + if (!auth) { + throw new GcpMcpError( + "Google Cloud authentication not available. Please configure authentication to access profiler data.", + "UNAUTHENTICATED", + 401, + ); + } + const client = await auth.getClient(); + const token = await client.getAccessToken(); + + // Build query parameters + const params = new URLSearchParams({ + pageSize: "100", + }); + + // Make REST API call + const apiUrl = `https://cloudprofiler.googleapis.com/v2/projects/${actualProjectId}/profiles?${params}`; + + const response = await fetch(apiUrl, { + method: "GET", + headers: { + Authorization: `Bearer ${token.token}`, + Accept: "application/json", + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new GcpMcpError( + `Failed to fetch CPU profiles: ${errorText}`, + "FAILED_PRECONDITION", + response.status, + ); + } + + const data: ListProfilesResponse = await response.json(); + const allProfiles = data.profiles || []; + + // Filter for CPU profiles only + const cpuProfiles = allProfiles.filter( + (p) => p.profileType === ProfileType.CPU, + ); + + if (!cpuProfiles || cpuProfiles.length === 0) { + return { + contents: [ + { + uri: uri.href, + text: `# CPU Profiles\n\nProject: ${projectId}\n\nNo CPU profiles found. Ensure CPU profiling is enabled for your applications.`, + mimeType: "text/markdown", + }, + ], + }; + } + + let content = `# CPU Performance Profiles\n\nProject: ${projectId}\nCPU Profiles: ${cpuProfiles.length} (of ${allProfiles.length} total)\n\n`; + + // CPU-specific analysis + content += `## CPU Performance Analysis\n\n`; + content += `${getProfileTypeDescription(ProfileType.CPU)}\n\n`; + + content += `**CPU Profiling Insights:**\n`; + content += `- **Profile Count:** ${cpuProfiles.length} CPU profiles available\n`; + content += `- **Analysis Focus:** Identify CPU hotspots and compute-intensive operations\n`; + content += `- **Optimisation Targets:** Functions with high CPU usage and frequent execution\n\n`; + + // Generate analysis for CPU profiles + const analysis = analyseProfilePatterns(cpuProfiles); + content += analysis; + + // CPU-specific recommendations + content += `\n## CPU Optimisation Recommendations\n\n`; + content += `**Performance Analysis:**\n`; + content += `- Review CPU-intensive functions for algorithmic improvements\n`; + content += `- Look for opportunities to optimise loops and recursive operations\n`; + content += `- Consider parallelisation for CPU-bound workloads\n`; + content += `- Profile before and after optimisations to measure impact\n\n`; + + content += `**Development Best Practices:**\n`; + content += `- Use CPU profiling during development to identify bottlenecks early\n`; + content += `- Set up continuous CPU profiling for production monitoring\n`; + content += `- Establish CPU usage baselines for performance regression detection\n`; + + return { + contents: [ + { + uri: uri.href, + text: content, + mimeType: "text/markdown", + }, + ], + }; + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + throw new GcpMcpError( + `Failed to fetch CPU profiles resource: ${errorMessage}`, + "INTERNAL_ERROR", + 500, + ); + } + }, + ); + + // Resource template for memory profiles with heap analysis + server.resource( + "gcp-profiler-memory-profiles", + new ResourceTemplate("gcp-profiler://{projectId}/memory-profiles", { + list: undefined, + }), + async (uri, { projectId }) => { + try { + const actualProjectId = projectId || (await getProjectId()); + + // Initialize Google Auth client + const auth = await initGoogleAuth(true); + if (!auth) { + throw new GcpMcpError( + "Google Cloud authentication not available. Please configure authentication to access profiler data.", + "UNAUTHENTICATED", + 401, + ); + } + const client = await auth.getClient(); + const token = await client.getAccessToken(); + + const params = new URLSearchParams({ + pageSize: "100", + }); + + const apiUrl = `https://cloudprofiler.googleapis.com/v2/projects/${projectId}/profiles?${params}`; + + const response = await fetch(apiUrl, { + method: "GET", + headers: { + Authorization: `Bearer ${token.token}`, + Accept: "application/json", + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new GcpMcpError( + `Failed to fetch memory profiles: ${errorText}`, + "FAILED_PRECONDITION", + response.status, + ); + } + + const data: ListProfilesResponse = await response.json(); + const allProfiles = data.profiles || []; + + // Filter for memory-related profiles + const memoryProfiles = allProfiles.filter( + (p) => + p.profileType === ProfileType.HEAP || + p.profileType === ProfileType.HEAP_ALLOC || + p.profileType === ProfileType.PEAK_HEAP, + ); + + if (!memoryProfiles || memoryProfiles.length === 0) { + return { + contents: [ + { + uri: uri.href, + text: `# Memory Profiles\n\nProject: ${projectId}\n\nNo memory profiles found. Ensure heap profiling is enabled for your applications.`, + mimeType: "text/markdown", + }, + ], + }; + } + + let content = `# Memory Performance Profiles\n\nProject: ${projectId}\nMemory Profiles: ${memoryProfiles.length} (of ${allProfiles.length} total)\n\n`; + + // Memory-specific analysis + content += `## Memory Profiling Analysis\n\n`; + + // Analyse by memory profile type + const heapProfiles = memoryProfiles.filter( + (p) => p.profileType === ProfileType.HEAP, + ); + const allocProfiles = memoryProfiles.filter( + (p) => p.profileType === ProfileType.HEAP_ALLOC, + ); + const peakProfiles = memoryProfiles.filter( + (p) => p.profileType === ProfileType.PEAK_HEAP, + ); + + content += `**Memory Profile Distribution:**\n`; + if (heapProfiles.length > 0) { + content += `- **Heap Profiles:** ${heapProfiles.length} - Current memory allocations\n`; + } + if (allocProfiles.length > 0) { + content += `- **Allocation Profiles:** ${allocProfiles.length} - Memory allocation patterns\n`; + } + if (peakProfiles.length > 0) { + content += `- **Peak Heap Profiles:** ${peakProfiles.length} - Maximum memory usage\n`; + } + content += `\n`; + + // Generate analysis for memory profiles + const analysis = analyseProfilePatterns(memoryProfiles); + content += analysis; + + // Memory-specific recommendations + content += `\n## Memory Optimisation Recommendations\n\n`; + content += `**Memory Management:**\n`; + content += `- Analyse allocation patterns to identify memory-intensive operations\n`; + content += `- Look for memory leaks and objects that aren't being garbage collected\n`; + content += `- Consider object pooling for frequently allocated objects\n`; + content += `- Review data structures for memory efficiency\n\n`; + + content += `**Performance Tuning:**\n`; + content += `- Monitor peak memory usage to right-size instance resources\n`; + content += `- Set up memory usage alerts based on profiling data\n`; + content += `- Use allocation profiling to optimise hot allocation paths\n`; + content += `- Compare memory usage before and after code changes\n`; + + return { + contents: [ + { + uri: uri.href, + text: content, + mimeType: "text/markdown", + }, + ], + }; + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + throw new GcpMcpError( + `Failed to fetch memory profiles resource: ${errorMessage}`, + "INTERNAL_ERROR", + 500, + ); + } + }, + ); + + // Resource template for performance recommendations based on all profiles + server.resource( + "gcp-profiler-performance-recommendations", + new ResourceTemplate( + "gcp-profiler://{projectId}/performance-recommendations", + { + list: undefined, + }, + ), + async (uri, { projectId }) => { + try { + const actualProjectId = projectId || (await getProjectId()); + + // Initialize Google Auth client + const auth = await initGoogleAuth(true); + if (!auth) { + throw new GcpMcpError( + "Google Cloud authentication not available. Please configure authentication to access profiler data.", + "UNAUTHENTICATED", + 401, + ); + } + const client = await auth.getClient(); + const token = await client.getAccessToken(); + + const params = new URLSearchParams({ + pageSize: "200", // Get more data for better recommendations + }); + + const apiUrl = `https://cloudprofiler.googleapis.com/v2/projects/${projectId}/profiles?${params}`; + + const response = await fetch(apiUrl, { + method: "GET", + headers: { + Authorization: `Bearer ${token.token}`, + Accept: "application/json", + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new GcpMcpError( + `Failed to fetch profiles for recommendations: ${errorText}`, + "FAILED_PRECONDITION", + response.status, + ); + } + + const data: ListProfilesResponse = await response.json(); + const profiles = data.profiles || []; + + if (!profiles || profiles.length === 0) { + return { + contents: [ + { + uri: uri.href, + text: `# Performance Recommendations\n\nProject: ${projectId}\n\nNo profiles available to generate recommendations. Enable Cloud Profiler for your applications to get performance insights.`, + mimeType: "text/markdown", + }, + ], + }; + } + + let content = `# Performance Recommendations\n\nProject: ${projectId}\nBased on ${profiles.length} profiles\n\n`; + + // Generate comprehensive analysis + const analysis = analyseProfilePatterns(profiles); + content += analysis; + + // Add comprehensive recommendations section + content += `\n## Comprehensive Performance Strategy\n\n`; + + content += `### Immediate Actions\n\n`; + content += `1. **Profile Review:** Analyse the ${profiles.length} collected profiles for immediate optimisation opportunities\n`; + content += `2. **Hotspot Identification:** Focus on the most CPU and memory-intensive operations\n`; + content += `3. **Baseline Establishment:** Use current profile data to establish performance baselines\n\n`; + + content += `### Medium-term Optimisations\n\n`; + content += `1. **Continuous Profiling:** Set up automated profiling and monitoring\n`; + content += `2. **Performance Testing:** Integrate profiling into your testing pipeline\n`; + content += `3. **Resource Optimisation:** Right-size resources based on profiling insights\n\n`; + + content += `### Long-term Performance Culture\n\n`; + content += `1. **Performance Budgets:** Establish performance budgets based on profiling data\n`; + content += `2. **Regression Detection:** Set up alerts for performance regressions\n`; + content += `3. **Team Education:** Train development teams on performance profiling techniques\n`; + + return { + contents: [ + { + uri: uri.href, + text: content, + mimeType: "text/markdown", + }, + ], + }; + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + throw new GcpMcpError( + `Failed to fetch performance recommendations resource: ${errorMessage}`, + "INTERNAL_ERROR", + 500, + ); + } + }, + ); +} diff --git a/src/services/profiler/tools.ts b/src/services/profiler/tools.ts new file mode 100644 index 0000000..cb24aeb --- /dev/null +++ b/src/services/profiler/tools.ts @@ -0,0 +1,678 @@ +/** + * Google Cloud Profiler tools for MCP + */ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { getProjectId, initGoogleAuth } from "../../utils/auth.js"; +import { GcpMcpError } from "../../utils/error.js"; +import { + formatProfileSummary, + analyseProfilePatterns, + getProfileTypeDescription, + Profile, + ProfileType, + ListProfilesResponse, +} from "./types.js"; + +/** + * Registers Google Cloud Profiler tools with the MCP server + * + * @param server The MCP server instance + */ +export function registerProfilerTools(server: McpServer): void { + // Tool to list profiles with filtering and pagination support + server.tool( + "gcp-profiler-list-profiles", + { + title: "List Profiles", + description: + "List profiles from Google Cloud Profiler with optional filtering and pagination", + inputSchema: { + pageSize: z + .number() + .min(1) + .max(1000) + .default(50) + .describe("Maximum number of profiles to return (1-1000)"), + pageToken: z + .string() + .optional() + .describe("Token for pagination to get next page of results"), + profileType: z + .enum([ + ProfileType.CPU, + ProfileType.WALL, + ProfileType.HEAP, + ProfileType.THREADS, + ProfileType.CONTENTION, + ProfileType.PEAK_HEAP, + ProfileType.HEAP_ALLOC, + ]) + .optional() + .describe("Filter by specific profile type"), + target: z + .string() + .optional() + .describe("Filter by deployment target (service name)"), + }, + }, + async ({ pageSize, pageToken, profileType, target }) => { + try { + const projectId = await getProjectId(); + + // Initialize Google Auth client (same pattern as error reporting) + const auth = await initGoogleAuth(true); + if (!auth) { + throw new GcpMcpError( + "Google Cloud authentication not available. Please configure authentication to access profiler data.", + "UNAUTHENTICATED", + 401, + ); + } + const client = await auth.getClient(); + const token = await client.getAccessToken(); + + // Parse parameters + const actualPageSize = pageSize || 50; + + // Build query parameters + const params = new URLSearchParams({ + pageSize: actualPageSize.toString(), + }); + + // Add page token if provided + if (pageToken) { + params.set("pageToken", pageToken); + } + + // Make REST API call to list profiles + const apiUrl = `https://cloudprofiler.googleapis.com/v2/projects/${projectId}/profiles?${params}`; + + const response = await fetch(apiUrl, { + method: "GET", + headers: { + Authorization: `Bearer ${token.token}`, + Accept: "application/json", + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new GcpMcpError( + `Failed to fetch profiles: ${errorText}`, + "FAILED_PRECONDITION", + response.status, + ); + } + + const data: ListProfilesResponse = await response.json(); + let profiles = data.profiles || []; + + // Apply client-side filtering if specified + if (profileType) { + profiles = profiles.filter((p) => p.profileType === profileType); + } + + if (target) { + profiles = profiles.filter((p) => + p.deployment?.target?.toLowerCase().includes(target.toLowerCase()), + ); + } + + if (!profiles || profiles.length === 0) { + let filterText = ""; + if (profileType) filterText += `Profile Type: ${profileType}\n`; + if (target) filterText += `Target: ${target}\n`; + + return { + content: [ + { + type: "text", + text: `# Profiles\n\nProject: ${projectId}\n${filterText}${data.nextPageToken ? `Page Token: ${pageToken || "first"}\n` : ""}No profiles found.`, + }, + ], + }; + } + + // Generate analysis and insights + const analysis = analyseProfilePatterns(profiles); + + let content = `# Profiler Analysis\n\nProject: ${projectId}\n`; + if (profileType) + content += `Profile Type Filter: ${getProfileTypeDescription(profileType)}\n`; + if (target) content += `Target Filter: ${target}\n`; + if (data.nextPageToken) + content += `Next Page Available: Use token "${data.nextPageToken}"\n`; + if (data.skippedProfiles) + content += `Skipped Profiles: ${data.skippedProfiles}\n`; + content += `\n${analysis}\n\n`; + + content += `## Detailed Profile List\n\n`; + + profiles.forEach((profile, index) => { + content += `### ${index + 1}. ${formatProfileSummary(profile)}\n`; + }); + + // Add pagination info if available + if (data.nextPageToken) { + content += `\n---\n\n**Pagination:** Use page token "${data.nextPageToken}" to get the next ${actualPageSize} results.\n`; + } + + return { + content: [ + { + type: "text", + text: content, + }, + ], + }; + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + throw new GcpMcpError( + `Failed to list profiles: ${errorMessage}`, + "INTERNAL_ERROR", + 500, + ); + } + }, + ); + + // Tool to get detailed analysis of specific profile types and patterns + server.tool( + "gcp-profiler-analyse-performance", + { + title: "Analyse Profile Performance", + description: + "Analyse profiles to identify performance patterns, bottlenecks, and optimisation opportunities", + inputSchema: { + profileType: z + .enum([ + ProfileType.CPU, + ProfileType.WALL, + ProfileType.HEAP, + ProfileType.THREADS, + ProfileType.CONTENTION, + ProfileType.PEAK_HEAP, + ProfileType.HEAP_ALLOC, + ]) + .optional() + .describe("Focus analysis on specific profile type"), + target: z + .string() + .optional() + .describe("Focus analysis on specific deployment target"), + pageSize: z + .number() + .min(1) + .max(1000) + .default(100) + .describe( + "Number of profiles to analyse (more profiles = better insights)", + ), + }, + }, + async ({ profileType, target, pageSize }) => { + try { + const projectId = await getProjectId(); + + // Initialize Google Auth client (same pattern as error reporting) + const auth = await initGoogleAuth(true); + if (!auth) { + throw new GcpMcpError( + "Google Cloud authentication not available. Please configure authentication to access profiler data.", + "UNAUTHENTICATED", + 401, + ); + } + const client = await auth.getClient(); + const token = await client.getAccessToken(); + + // Parse parameters + const actualPageSize = pageSize || 100; + + // Build query parameters for maximum data collection + const params = new URLSearchParams({ + pageSize: actualPageSize.toString(), + }); + + // Make REST API call to list profiles + const apiUrl = `https://cloudprofiler.googleapis.com/v2/projects/${projectId}/profiles?${params}`; + + const response = await fetch(apiUrl, { + method: "GET", + headers: { + Authorization: `Bearer ${token.token}`, + Accept: "application/json", + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new GcpMcpError( + `Failed to fetch profiles for analysis: ${errorText}`, + "FAILED_PRECONDITION", + response.status, + ); + } + + const data: ListProfilesResponse = await response.json(); + let profiles = data.profiles || []; + + // Apply filtering if specified + if (profileType) { + profiles = profiles.filter((p) => p.profileType === profileType); + } + + if (target) { + profiles = profiles.filter((p) => + p.deployment?.target?.toLowerCase().includes(target.toLowerCase()), + ); + } + + if (!profiles || profiles.length === 0) { + let filterText = "No profiles found for analysis"; + if (profileType) filterText += ` with profile type: ${profileType}`; + if (target) filterText += ` and target: ${target}`; + + return { + content: [ + { + type: "text", + text: `# Profile Performance Analysis\n\nProject: ${projectId}\n\n${filterText}.`, + }, + ], + }; + } + + // Generate comprehensive analysis + let content = `# Profile Performance Analysis\n\nProject: ${projectId}\n`; + if (profileType) + content += `Focus: ${getProfileTypeDescription(profileType)}\n`; + if (target) content += `Target: ${target}\n`; + content += `Analysed: ${profiles.length} profiles\n\n`; + + // Get detailed analysis + const analysis = analyseProfilePatterns(profiles); + content += analysis; + + // Add performance insights specific to the analysis + content += `\n## Performance Insights\n\n`; + + // Analyse profile collection patterns + const timeDistribution = analyseProfileTimeDistribution(profiles); + content += timeDistribution; + + // Analyse deployment patterns + const deploymentAnalysis = analyseDeploymentPatterns(profiles); + content += deploymentAnalysis; + + // Add actionable recommendations + content += `\n## Actionable Recommendations\n\n`; + content += getActionableRecommendations(profiles, profileType); + + return { + content: [ + { + type: "text", + text: content, + }, + ], + }; + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + throw new GcpMcpError( + `Failed to analyse profile performance: ${errorMessage}`, + "INTERNAL_ERROR", + 500, + ); + } + }, + ); + + // Tool to compare profiles and identify performance trends + server.tool( + "gcp-profiler-compare-trends", + { + title: "Compare Profile Trends", + description: + "Compare profiles over time to identify performance trends, regressions, and improvements", + inputSchema: { + target: z + .string() + .optional() + .describe("Focus comparison on specific deployment target"), + profileType: z + .enum([ + ProfileType.CPU, + ProfileType.WALL, + ProfileType.HEAP, + ProfileType.THREADS, + ProfileType.CONTENTION, + ProfileType.PEAK_HEAP, + ProfileType.HEAP_ALLOC, + ]) + .optional() + .describe("Focus comparison on specific profile type"), + pageSize: z + .number() + .min(1) + .max(1000) + .default(200) + .describe("Number of profiles to analyse for trends"), + }, + }, + async ({ target, profileType, pageSize }) => { + try { + const projectId = await getProjectId(); + + // Initialize Google Auth client + const auth = await initGoogleAuth(true); + if (!auth) { + throw new GcpMcpError( + "Google Cloud authentication not available. Please configure authentication to access profiler data.", + "UNAUTHENTICATED", + 401, + ); + } + const client = await auth.getClient(); + const token = await client.getAccessToken(); + + const actualPageSize = pageSize || 200; + + // Build query parameters + const params = new URLSearchParams({ + pageSize: actualPageSize.toString(), + }); + + // Make REST API call to list profiles + const apiUrl = `https://cloudprofiler.googleapis.com/v2/projects/${projectId}/profiles?${params}`; + + const response = await fetch(apiUrl, { + method: "GET", + headers: { + Authorization: `Bearer ${token.token}`, + Accept: "application/json", + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new GcpMcpError( + `Failed to fetch profiles for trend analysis: ${errorText}`, + "FAILED_PRECONDITION", + response.status, + ); + } + + const data: ListProfilesResponse = await response.json(); + let profiles = data.profiles || []; + + // Apply filtering + if (profileType) { + profiles = profiles.filter((p) => p.profileType === profileType); + } + + if (target) { + profiles = profiles.filter((p) => + p.deployment?.target?.toLowerCase().includes(target.toLowerCase()), + ); + } + + if (!profiles || profiles.length === 0) { + return { + content: [ + { + type: "text", + text: `# Profile Trend Analysis\n\nProject: ${projectId}\n\nNo profiles found for trend analysis.`, + }, + ], + }; + } + + // Generate trend analysis + let content = `# Profile Trend Analysis\n\nProject: ${projectId}\n`; + if (profileType) + content += `Profile Type: ${getProfileTypeDescription(profileType)}\n`; + if (target) content += `Target: ${target}\n`; + content += `Analysed: ${profiles.length} profiles\n\n`; + + // Analyse trends over time + const trendAnalysis = analyseProfileTrends(profiles); + content += trendAnalysis; + + return { + content: [ + { + type: "text", + text: content, + }, + ], + }; + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + throw new GcpMcpError( + `Failed to analyse profile trends: ${errorMessage}`, + "INTERNAL_ERROR", + 500, + ); + } + }, + ); +} + +/** + * Analyses profile collection time distribution + */ +function analyseProfileTimeDistribution(profiles: Profile[]): string { + const profilesByTime = profiles + .filter((p) => p.startTime) + .sort( + (a, b) => + new Date(a.startTime).getTime() - new Date(b.startTime).getTime(), + ); + + if (profilesByTime.length === 0) { + return "No time-stamped profiles available for temporal analysis.\n\n"; + } + + let analysis = "### Profile Collection Timeline\n\n"; + + // Group by time buckets (last 24 hours, last week, older) + const now = new Date().getTime(); + const oneDayAgo = now - 24 * 60 * 60 * 1000; + const oneWeekAgo = now - 7 * 24 * 60 * 60 * 1000; + + const recent = profilesByTime.filter( + (p) => new Date(p.startTime).getTime() > oneDayAgo, + ); + const thisWeek = profilesByTime.filter((p) => { + const time = new Date(p.startTime).getTime(); + return time <= oneDayAgo && time > oneWeekAgo; + }); + const older = profilesByTime.filter( + (p) => new Date(p.startTime).getTime() <= oneWeekAgo, + ); + + analysis += `- **Last 24 hours:** ${recent.length} profiles\n`; + analysis += `- **Last week:** ${thisWeek.length} profiles\n`; + analysis += `- **Older:** ${older.length} profiles\n\n`; + + if (recent.length > 0) { + const oldestRecent = new Date(recent[0].startTime).toLocaleString(); + const newestRecent = new Date( + recent[recent.length - 1].startTime, + ).toLocaleString(); + analysis += `Recent activity spans from ${oldestRecent} to ${newestRecent}\n\n`; + } + + return analysis; +} + +/** + * Analyses deployment patterns across profiles + */ +function analyseDeploymentPatterns(profiles: Profile[]): string { + let analysis = "### Deployment Analysis\n\n"; + + // Analyse targets + const targetCounts = profiles.reduce( + (acc, profile) => { + const target = profile.deployment?.target || "Unknown"; + acc[target] = (acc[target] || 0) + 1; + return acc; + }, + {} as Record, + ); + + analysis += `**Target Distribution:**\n`; + Object.entries(targetCounts) + .sort(([, a], [, b]) => b - a) + .forEach(([target, count]) => { + const percentage = Math.round((count / profiles.length) * 100); + analysis += `- ${target}: ${count} profiles (${percentage}%)\n`; + }); + + analysis += `\n`; + + // Analyse labels if available + const allLabels = new Set(); + profiles.forEach((profile) => { + Object.keys(profile.labels || {}).forEach((label) => allLabels.add(label)); + Object.keys(profile.deployment?.labels || {}).forEach((label) => + allLabels.add(label), + ); + }); + + if (allLabels.size > 0) { + analysis += `**Common Labels Found:** ${Array.from(allLabels).join(", ")}\n\n`; + } + + return analysis; +} + +/** + * Generates actionable recommendations based on profile analysis + */ +function getActionableRecommendations( + profiles: Profile[], + profileType?: ProfileType, +): string { + let recommendations = ""; + + const targets = [ + ...new Set(profiles.map((p) => p.deployment?.target).filter(Boolean)), + ]; + const hasMultipleTargets = targets.length > 1; + + recommendations += `**Immediate Actions:**\n`; + recommendations += `- Review the ${profiles.length} profiles for performance patterns\n`; + + if (hasMultipleTargets) { + recommendations += `- Compare performance across ${targets.length} different targets\n`; + } + + if (profileType) { + switch (profileType) { + case ProfileType.CPU: + recommendations += `- Identify CPU hotspots and optimise high-usage functions\n`; + recommendations += `- Look for algorithms that can be optimised or parallelised\n`; + break; + case ProfileType.HEAP: + recommendations += `- Review memory allocation patterns for optimisation\n`; + recommendations += `- Check for memory leaks or excessive garbage collection\n`; + break; + case ProfileType.WALL: + recommendations += `- Identify blocking I/O operations and network calls\n`; + recommendations += `- Consider asynchronous processing for slow operations\n`; + break; + } + } + + recommendations += `\n**Long-term Optimisation:**\n`; + recommendations += `- Set up continuous profiling alerts for performance regressions\n`; + recommendations += `- Establish performance baselines for each service\n`; + recommendations += `- Integrate profiling data with monitoring and alerting systems\n`; + recommendations += `- Use profile data to guide load testing and capacity planning\n`; + + return recommendations; +} + +/** + * Analyses performance trends over time + */ +function analyseProfileTrends(profiles: Profile[]): string { + const profilesByTime = profiles + .filter((p) => p.startTime) + .sort( + (a, b) => + new Date(a.startTime).getTime() - new Date(b.startTime).getTime(), + ); + + if (profilesByTime.length < 2) { + return "Insufficient time-series data for trend analysis. Need at least 2 time-stamped profiles.\n"; + } + + let analysis = "## Trend Analysis\n\n"; + + // Analyse profile frequency over time + analysis += "### Profile Collection Frequency\n\n"; + + const earliest = new Date(profilesByTime[0].startTime); + const latest = new Date(profilesByTime[profilesByTime.length - 1].startTime); + const timeSpan = latest.getTime() - earliest.getTime(); + const timeSpanDays = timeSpan / (1000 * 60 * 60 * 24); + + analysis += `- **Time Span:** ${Math.round(timeSpanDays)} days (from ${earliest.toLocaleDateString()} to ${latest.toLocaleDateString()})\n`; + analysis += `- **Collection Frequency:** ${Math.round(profiles.length / timeSpanDays)} profiles per day\n\n`; + + // Analyse profile type trends + analysis += "### Profile Type Trends\n\n"; + + const typesByTime = profilesByTime.reduce( + (acc, profile) => { + const day = new Date(profile.startTime).toDateString(); + if (!acc[day]) acc[day] = {}; + acc[day][profile.profileType] = (acc[day][profile.profileType] || 0) + 1; + return acc; + }, + {} as Record>, + ); + + const days = Object.keys(typesByTime).sort(); + if (days.length > 1) { + analysis += `Collected profiles across ${days.length} different days:\n\n`; + days.slice(-5).forEach((day) => { + // Show last 5 days + const typeCounts = typesByTime[day]; + const totalForDay = Object.values(typeCounts).reduce( + (sum, count) => sum + count, + 0, + ); + analysis += `**${day}:** ${totalForDay} profiles (`; + analysis += Object.entries(typeCounts) + .map(([type, count]) => `${type}: ${count}`) + .join(", "); + analysis += `)\n`; + }); + analysis += `\n`; + } + + // Recommendations based on trends + analysis += "### Trend-Based Recommendations\n\n"; + + if (timeSpanDays < 1) { + analysis += `- **Short timeframe:** Consider collecting profiles over a longer period for better trend analysis\n`; + } else if (profiles.length / timeSpanDays < 1) { + analysis += `- **Low frequency:** Consider increasing profile collection frequency for better insights\n`; + } else { + analysis += `- **Good coverage:** Profile collection frequency appears adequate for trend analysis\n`; + } + + analysis += `- **Pattern monitoring:** Set up alerts for unusual changes in profile patterns\n`; + analysis += `- **Performance baseline:** Use this trend data to establish performance baselines\n`; + + return analysis; +} diff --git a/src/services/profiler/types.ts b/src/services/profiler/types.ts new file mode 100644 index 0000000..3467abd --- /dev/null +++ b/src/services/profiler/types.ts @@ -0,0 +1,393 @@ +/** + * Type definitions for Google Cloud Profiler service + */ +import { initGoogleAuth } from "../../utils/auth.js"; + +/** + * Interface for Google Cloud Profile (matches REST API schema) + */ +export interface Profile { + /** Opaque, server-assigned, unique ID for the profile */ + name: string; + /** Type of profile data collected */ + profileType: ProfileType; + /** Deployment information for the profile */ + deployment: Deployment; + /** Duration of the profile collection */ + duration: string; + /** Gzip compressed serialized profile data in protobuf format */ + profileBytes: string; + /** Additional labels for the profile */ + labels: Record; + /** Timestamp when profile collection started */ + startTime: string; +} + +/** + * Enum for different profile types supported by Cloud Profiler + */ +export enum ProfileType { + UNSPECIFIED = "PROFILE_TYPE_UNSPECIFIED", + CPU = "CPU", + WALL = "WALL", + HEAP = "HEAP", + THREADS = "THREADS", + CONTENTION = "CONTENTION", + PEAK_HEAP = "PEAK_HEAP", + HEAP_ALLOC = "HEAP_ALLOC", +} + +/** + * Interface for deployment information + */ +export interface Deployment { + /** Project ID where the deployment is running */ + projectId: string; + /** Target name for the deployment (e.g., service name) */ + target: string; + /** Additional labels for the deployment */ + labels: Record; +} + +/** + * Response from the list profiles API + */ +export interface ListProfilesResponse { + /** List of profiles found */ + profiles: Profile[]; + /** Token for pagination to next page */ + nextPageToken?: string; + /** Number of profiles that couldn't be fetched */ + skippedProfiles?: number; +} + +/** + * Profile analysis result + */ +export interface ProfileAnalysis { + profile: Profile; + analysisType: string; + insights: string[]; + recommendations: string[]; + metrics: ProfileMetrics; +} + +/** + * Metrics extracted from profile analysis + */ +export interface ProfileMetrics { + duration: number; // in seconds + samplingRate?: number; + hotspots: number; + memoryUsage?: number; + cpuUsage?: number; +} + +/** + * Formats a profile summary for display + */ +export function formatProfileSummary(profile: Profile): string { + const profileName = profile.name || "Unknown"; + const profileType = profile.profileType || "Unknown"; + const deployment = profile.deployment || {}; + const target = deployment.target || "Unknown"; + const projectId = deployment.projectId || "Unknown"; + const startTime = profile.startTime + ? new Date(profile.startTime).toLocaleString() + : "Unknown"; + const duration = profile.duration || "Unknown"; + + let summary = `## Profile: ${profileName.split("/").pop()}\n\n`; + summary += `**Type:** ${getProfileTypeDescription(profileType)}\n`; + summary += `**Target:** ${target}\n`; + summary += `**Project:** ${projectId}\n`; + summary += `**Start Time:** ${startTime}\n`; + summary += `**Duration:** ${formatDuration(duration)}\n`; + + // Add labels if available + if (profile.labels && Object.keys(profile.labels).length > 0) { + summary += `**Labels:**\n`; + Object.entries(profile.labels).forEach(([key, value]) => { + summary += ` - ${key}: ${value}\n`; + }); + } + + // Add deployment labels if available + if (deployment.labels && Object.keys(deployment.labels).length > 0) { + summary += `**Deployment Labels:**\n`; + Object.entries(deployment.labels).forEach(([key, value]) => { + summary += ` - ${key}: ${value}\n`; + }); + } + + return summary; +} + +/** + * Gets a human-readable description for profile types + */ +export function getProfileTypeDescription( + profileType: ProfileType | string, +): string { + switch (profileType) { + case ProfileType.CPU: + return "CPU Time - Shows where your application spends CPU time"; + case ProfileType.WALL: + return "Wall Time - Shows elapsed time including I/O waits and blocking calls"; + case ProfileType.HEAP: + return "Heap Memory - Shows memory allocations and usage patterns"; + case ProfileType.THREADS: + return "Threads/Goroutines - Shows thread or goroutine activity and concurrency"; + case ProfileType.CONTENTION: + return "Contention - Shows lock contention and synchronisation overhead"; + case ProfileType.PEAK_HEAP: + return "Peak Heap - Shows maximum heap memory usage"; + case ProfileType.HEAP_ALLOC: + return "Heap Allocations - Shows memory allocation patterns and frequency"; + default: + return `${profileType} - Profile data type`; + } +} + +/** + * Formats duration string from ISO 8601 format to human readable + */ +export function formatDuration(duration: string): string { + if (!duration) return "Unknown"; + + // Parse ISO 8601 duration (e.g., "PT30S" = 30 seconds) + const match = duration.match(/PT(\d+(?:\.\d+)?)([HMS])/); + if (!match) return duration; + + const value = parseFloat(match[1]); + const unit = match[2]; + + switch (unit) { + case "S": + return `${value} seconds`; + case "M": + return `${value} minutes`; + case "H": + return `${value} hours`; + default: + return duration; + } +} + +/** + * Analyses multiple profiles to provide insights and recommendations + */ +export function analyseProfilePatterns(profiles: Profile[]): string { + if (!profiles || profiles.length === 0) { + return "No profiles found in the specified criteria."; + } + + let analysis = `# Profile Analysis and Performance Insights\n\n`; + + // Profile summary statistics + const totalProfiles = profiles.length; + const profileTypes = [...new Set(profiles.map((p) => p.profileType))]; + const targets = [ + ...new Set(profiles.map((p) => p.deployment?.target).filter(Boolean)), + ]; + + analysis += `## Summary\n\n`; + analysis += `- **Total Profiles:** ${totalProfiles}\n`; + analysis += `- **Profile Types:** ${profileTypes.length} (${profileTypes.join(", ")})\n`; + analysis += `- **Targets:** ${targets.length} (${targets.join(", ")})\n\n`; + + // Profile type distribution + const typeDistribution = profiles.reduce( + (acc, profile) => { + acc[profile.profileType] = (acc[profile.profileType] || 0) + 1; + return acc; + }, + {} as Record, + ); + + analysis += `## Profile Type Distribution\n\n`; + Object.entries(typeDistribution) + .sort(([, a], [, b]) => b - a) + .forEach(([type, count]) => { + const percentage = Math.round((count / totalProfiles) * 100); + analysis += `- **${getProfileTypeDescription(type)}:** ${count} profiles (${percentage}%)\n`; + }); + + analysis += `\n`; + + // Recent activity analysis + const recentProfiles = profiles + .filter((p) => p.startTime) + .sort( + (a, b) => + new Date(b.startTime).getTime() - new Date(a.startTime).getTime(), + ) + .slice(0, 5); + + if (recentProfiles.length > 0) { + analysis += `## Recent Profile Activity\n\n`; + recentProfiles.forEach((profile, index) => { + const timeAgo = getTimeAgo(profile.startTime); + analysis += `${index + 1}. **${profile.deployment?.target || "Unknown Target"}** - ${getProfileTypeDescription(profile.profileType)} (${timeAgo})\n`; + }); + analysis += `\n`; + } + + // Performance analysis by profile type + analysis += `## Performance Analysis by Profile Type\n\n`; + + profileTypes.forEach((type) => { + const typeProfiles = profiles.filter((p) => p.profileType === type); + analysis += `### ${getProfileTypeDescription(type)}\n\n`; + analysis += getProfileTypeAnalysis(type, typeProfiles); + analysis += `\n`; + }); + + // Recommendations + analysis += `## Recommendations\n\n`; + analysis += getPerformanceRecommendations(profiles, typeDistribution); + + return analysis; +} + +/** + * Gets time ago string from timestamp + */ +function getTimeAgo(timestamp: string): string { + const now = new Date().getTime(); + const time = new Date(timestamp).getTime(); + const diffMinutes = Math.floor((now - time) / (1000 * 60)); + + if (diffMinutes < 60) { + return `${diffMinutes} minutes ago`; + } else if (diffMinutes < 1440) { + return `${Math.floor(diffMinutes / 60)} hours ago`; + } else { + return `${Math.floor(diffMinutes / 1440)} days ago`; + } +} + +/** + * Provides analysis specific to profile type + */ +function getProfileTypeAnalysis( + profileType: ProfileType | string, + profiles: Profile[], +): string { + const count = profiles.length; + let analysis = `**Found ${count} ${profileType} profiles**\n\n`; + + switch (profileType) { + case ProfileType.CPU: + analysis += `**CPU Profiling Analysis:**\n`; + analysis += `- Identifies hotspots and CPU-intensive functions\n`; + analysis += `- Look for functions with high self-time vs. total time\n`; + analysis += `- Consider optimising frequently called functions\n`; + analysis += `- Check for inefficient algorithms or loops\n`; + break; + + case ProfileType.HEAP: + analysis += `**Memory Profiling Analysis:**\n`; + analysis += `- Shows memory allocation patterns and potential leaks\n`; + analysis += `- Look for objects with high allocation rates\n`; + analysis += `- Identify memory retention issues\n`; + analysis += `- Check for unnecessary object creation\n`; + break; + + case ProfileType.WALL: + analysis += `**Wall Time Profiling Analysis:**\n`; + analysis += `- Shows real elapsed time including I/O operations\n`; + analysis += `- Identifies blocking operations and wait times\n`; + analysis += `- Look for slow network calls or database queries\n`; + analysis += `- Check for inefficient synchronous operations\n`; + break; + + case ProfileType.CONTENTION: + analysis += `**Lock Contention Analysis:**\n`; + analysis += `- Shows synchronisation overhead and lock conflicts\n`; + analysis += `- Look for heavily contended locks or mutexes\n`; + analysis += `- Consider lock-free algorithms or reduced lock scope\n`; + analysis += `- Check for deadlock potential\n`; + break; + + case ProfileType.THREADS: + analysis += `**Thread/Goroutine Analysis:**\n`; + analysis += `- Shows concurrency patterns and thread utilisation\n`; + analysis += `- Look for thread pool exhaustion or oversaturation\n`; + analysis += `- Check for optimal concurrency levels\n`; + analysis += `- Identify blocking goroutines or threads\n`; + break; + + default: + analysis += `**General Analysis:**\n`; + analysis += `- Review profile data for performance patterns\n`; + analysis += `- Look for resource usage anomalies\n`; + analysis += `- Compare with baseline performance metrics\n`; + break; + } + + return analysis; +} + +/** + * Generates performance recommendations based on profile analysis + */ +function getPerformanceRecommendations( + _profiles: Profile[], + typeDistribution: Record, +): string { + let recommendations = ""; + + // Recommendations based on profile types present + if (typeDistribution[ProfileType.CPU] > 0) { + recommendations += `**CPU Optimisation:**\n`; + recommendations += `- Profile your application to identify CPU hotspots\n`; + recommendations += `- Consider algorithm optimisations for high-usage functions\n`; + recommendations += `- Review and optimise nested loops and recursive functions\n\n`; + } + + if (typeDistribution[ProfileType.HEAP] > 0) { + recommendations += `**Memory Management:**\n`; + recommendations += `- Monitor memory allocation patterns for optimisation opportunities\n`; + recommendations += `- Consider object pooling for frequently allocated objects\n`; + recommendations += `- Review memory retention and garbage collection patterns\n\n`; + } + + if (typeDistribution[ProfileType.WALL] > 0) { + recommendations += `**I/O and Latency Optimisation:**\n`; + recommendations += `- Profile I/O operations to identify bottlenecks\n`; + recommendations += `- Consider asynchronous operations for blocking calls\n`; + recommendations += `- Implement caching for frequently accessed data\n\n`; + } + + if (typeDistribution[ProfileType.CONTENTION] > 0) { + recommendations += `**Concurrency Optimisation:**\n`; + recommendations += `- Review lock usage and consider reducing critical sections\n`; + recommendations += `- Evaluate lock-free data structures and algorithms\n`; + recommendations += `- Analyse thread synchronisation patterns\n\n`; + } + + // General recommendations + recommendations += `**General Performance Best Practices:**\n`; + recommendations += `- Establish baseline performance metrics\n`; + recommendations += `- Set up continuous profiling for production monitoring\n`; + recommendations += `- Correlate profile data with application metrics and logs\n`; + recommendations += `- Use profile data to guide performance testing scenarios\n`; + + return recommendations; +} + +/** + * Gets Google Cloud authentication for Profiler API access + */ +export async function getProfilerAuth() { + const auth = await initGoogleAuth(true); + if (!auth) { + throw new Error("Google Cloud authentication not available"); + } + + const client = await auth.getClient(); + const token = await client.getAccessToken(); + + return { auth, token: token.token }; +} diff --git a/src/services/spanner/query-count.ts b/src/services/spanner/query-count.ts index 8310170..50852d8 100644 --- a/src/services/spanner/query-count.ts +++ b/src/services/spanner/query-count.ts @@ -56,7 +56,7 @@ interface TimeSeriesData { */ export function registerSpannerQueryCountTool(server: McpServer): void { server.tool( - "spanner-query-count", + "gcp-spanner-query-count", { instanceId: z .string() diff --git a/src/services/spanner/resources.ts b/src/services/spanner/resources.ts index fcdf0e2..aa99c6e 100644 --- a/src/services/spanner/resources.ts +++ b/src/services/spanner/resources.ts @@ -19,7 +19,7 @@ import { logger } from "../../utils/logger.js"; export function registerSpannerResources(server: McpServer): void { // Register a resource for database schema server.resource( - "spanner-schema", + "gcp-spanner-database-schema", new ResourceTemplate( "gcp-spanner://{projectId}/{instanceId}/{databaseId}/schema", { list: undefined }, @@ -81,7 +81,7 @@ export function registerSpannerResources(server: McpServer): void { // Register a resource for table data preview server.resource( - "table-preview", + "gcp-spanner-table-preview", new ResourceTemplate( "gcp-spanner://{projectId}/{instanceId}/{databaseId}/tables/{tableName}/preview", { list: undefined }, @@ -189,7 +189,7 @@ export function registerSpannerResources(server: McpServer): void { // Register a resource for listing available tables server.resource( - "spanner-tables", + "gcp-spanner-database-tables", new ResourceTemplate( "gcp-spanner://{projectId}/{instanceId}/{databaseId}/tables", { list: undefined }, @@ -296,7 +296,7 @@ export function registerSpannerResources(server: McpServer): void { // Register a resource for listing available instances server.resource( - "spanner-instances", + "gcp-spanner-list-instances", new ResourceTemplate("gcp-spanner://{projectId}/instances", { list: undefined, }), @@ -383,7 +383,7 @@ export function registerSpannerResources(server: McpServer): void { // Register a resource for listing available databases server.resource( - "spanner-databases", + "gcp-spanner-list-databases", new ResourceTemplate("gcp-spanner://{projectId}/{instanceId}/databases", { list: undefined, }), diff --git a/src/services/spanner/tools.ts b/src/services/spanner/tools.ts index d0506b3..d501c5b 100644 --- a/src/services/spanner/tools.ts +++ b/src/services/spanner/tools.ts @@ -54,7 +54,7 @@ async function getDetailedSchemaForQueryGeneration( export function registerSpannerTools(server: McpServer): void { // Tool to execute SQL queries server.tool( - "execute-spanner-query", + "gcp-spanner-execute-query", { sql: z.string().describe("The SQL query to execute"), instanceId: z @@ -150,7 +150,7 @@ export function registerSpannerTools(server: McpServer): void { // Tool to list tables server.tool( - "list-spanner-tables", + "gcp-spanner-list-tables", { instanceId: z .string() @@ -243,7 +243,7 @@ export function registerSpannerTools(server: McpServer): void { // Tool to list instances server.tool( - "list-spanner-instances", + "gcp-spanner-list-instances", // Define an empty schema with a dummy parameter that's optional // This ensures compatibility with clients that expect an object parameter { @@ -342,7 +342,7 @@ export function registerSpannerTools(server: McpServer): void { // Tool to list databases server.tool( - "list-spanner-databases", + "gcp-spanner-list-databases", { instanceId: z.string().describe("Spanner instance ID"), }, @@ -432,7 +432,7 @@ export function registerSpannerTools(server: McpServer): void { // Tool to execute natural language queries against Spanner server.tool( - "natural-language-spanner-query", + "gcp-spanner-query-natural-language", { query: z .string() diff --git a/src/services/trace/resources.ts b/src/services/trace/resources.ts index 2aaab68..e67699d 100644 --- a/src/services/trace/resources.ts +++ b/src/services/trace/resources.ts @@ -19,7 +19,7 @@ import { logger } from "../../utils/logger.js"; export function registerTraceResources(server: McpServer): void { // Resource to get a specific trace by ID server.resource( - "trace-by-id", + "gcp-trace-get-by-id", new ResourceTemplate("gcp-trace://{projectId}/traces/{traceId}", { list: undefined, }), @@ -114,7 +114,7 @@ export function registerTraceResources(server: McpServer): void { // Resource to get logs associated with a trace server.resource( - "trace-logs", + "gcp-trace-related-logs", new ResourceTemplate("gcp-trace://{projectId}/traces/{traceId}/logs", { list: undefined, }), @@ -261,7 +261,7 @@ Filter used: ${traceFilter}`, // Resource to list recent failed traces server.resource( - "recent-failed-traces", + "gcp-trace-recent-failed", new ResourceTemplate("gcp-trace://{projectId}/recent-failed", { list: undefined, }), diff --git a/src/services/trace/tools.ts b/src/services/trace/tools.ts index 313f167..8e6e81c 100644 --- a/src/services/trace/tools.ts +++ b/src/services/trace/tools.ts @@ -22,7 +22,7 @@ import { stateManager } from "../../utils/state-manager.js"; export async function registerTraceTools(server: McpServer): Promise { // Tool to get a trace by ID server.tool( - "get-trace", + "gcp-trace-get-trace", { traceId: z.string().describe("The trace ID to retrieve"), projectId: z @@ -268,7 +268,7 @@ export async function registerTraceTools(server: McpServer): Promise { // Tool to list recent traces server.tool( - "list-traces", + "gcp-trace-list-traces", { projectId: z .string() @@ -500,7 +500,7 @@ export async function registerTraceTools(server: McpServer): Promise { // Tool to find traces associated with logs server.tool( - "find-traces-from-logs", + "gcp-trace-find-from-logs", { projectId: z .string() @@ -755,7 +755,7 @@ export async function registerTraceTools(server: McpServer): Promise { // Tool to analyze a trace using natural language server.tool( - "natural-language-trace-query", + "gcp-trace-query-natural-language", { query: z .string() diff --git a/src/utils/project-tools.ts b/src/utils/project-tools.ts index 5c25b37..d2a7802 100644 --- a/src/utils/project-tools.ts +++ b/src/utils/project-tools.ts @@ -17,7 +17,7 @@ import { logger } from "./logger.js"; export function registerProjectTools(server: McpServer): void { // Tool to set the default project ID server.registerTool( - "set-project-id", + "gcp-utils-set-project-id", { title: "Set Project ID", description: @@ -56,7 +56,7 @@ export function registerProjectTools(server: McpServer): void { // Tool to get the current project ID server.registerTool( - "get-project-id", + "gcp-utils-get-project-id", { title: "Get Project ID", description: diff --git a/test/integration/mcp-protocol.test.ts b/test/integration/mcp-protocol.test.ts index da41380..ff1aa67 100644 --- a/test/integration/mcp-protocol.test.ts +++ b/test/integration/mcp-protocol.test.ts @@ -66,7 +66,7 @@ describe('MCP Protocol Compliance', () => { registerIamTools(mockMcpServer as any); const toolCall = mockMcpServer.registerTool.mock.calls.find( - call => call[0] === 'get-project-iam-policy' + call => call[0] === 'gcp-iam-get-project-policy' ); expect(toolCall).toBeDefined(); @@ -109,7 +109,7 @@ describe('MCP Protocol Compliance', () => { registerIamTools(mockMcpServer as any); const toolCall = mockMcpServer.registerTool.mock.calls.find( - call => call[0] === 'test-project-permissions' + call => call[0] === 'gcp-iam-test-project-permissions' ); const toolHandler = toolCall[2]; @@ -132,7 +132,7 @@ describe('MCP Protocol Compliance', () => { registerIamTools(mockMcpServer as any); const toolCall = mockMcpServer.registerTool.mock.calls.find( - call => call[0] === 'test-project-permissions' + call => call[0] === 'gcp-iam-test-project-permissions' ); const toolHandler = toolCall[2]; @@ -152,7 +152,7 @@ describe('MCP Protocol Compliance', () => { registerIamTools(mockMcpServer as any); const toolCall = mockMcpServer.registerTool.mock.calls.find( - call => call[0] === 'test-resource-permissions' + call => call[0] === 'gcp-iam-test-resource-permissions' ); const toolHandler = toolCall[2]; @@ -173,7 +173,7 @@ describe('MCP Protocol Compliance', () => { registerIamTools(mockMcpServer as any); const toolCall = mockMcpServer.registerTool.mock.calls.find( - call => call[0] === 'get-project-iam-policy' + call => call[0] === 'gcp-iam-get-project-policy' ); const toolHandler = toolCall[2]; diff --git a/test/integration/security-validation.test.ts b/test/integration/security-validation.test.ts index a0c1ac7..41e9eb1 100644 --- a/test/integration/security-validation.test.ts +++ b/test/integration/security-validation.test.ts @@ -21,7 +21,7 @@ describe('Security Validation', () => { registerIamTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'get-project-iam-policy' + call => call[0] === 'gcp-iam-get-project-policy' ); const toolHandler = toolCall[2]; @@ -51,7 +51,7 @@ describe('Security Validation', () => { registerIamTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'test-project-permissions' + call => call[0] === 'gcp-iam-test-project-permissions' ); const toolHandler = toolCall[2]; @@ -73,7 +73,7 @@ describe('Security Validation', () => { registerIamTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'test-resource-permissions' + call => call[0] === 'gcp-iam-test-resource-permissions' ); const toolHandler = toolCall[2]; @@ -147,7 +147,7 @@ describe('Security Validation', () => { registerIamTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'get-project-iam-policy' + call => call[0] === 'gcp-iam-get-project-policy' ); const toolHandler = toolCall[2]; @@ -199,7 +199,7 @@ describe('Security Validation', () => { registerIamTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'analyse-permission-gaps' + call => call[0] === 'gcp-iam-analyse-permission-gaps' ); const toolHandler = toolCall[2]; @@ -224,7 +224,7 @@ describe('Security Validation', () => { registerIamTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'test-project-permissions' + call => call[0] === 'gcp-iam-test-project-permissions' ); const toolHandler = toolCall[2]; @@ -250,7 +250,7 @@ describe('Security Validation', () => { registerIamTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'test-project-permissions' + call => call[0] === 'gcp-iam-test-project-permissions' ); const toolHandler = toolCall[2]; diff --git a/test/unit/services/error-reporting/tools.test.ts b/test/unit/services/error-reporting/tools.test.ts index 36d6489..27a7765 100644 --- a/test/unit/services/error-reporting/tools.test.ts +++ b/test/unit/services/error-reporting/tools.test.ts @@ -141,19 +141,19 @@ describe('Error Reporting Tools', () => { registerErrorReportingTools(mockServer as any); expect(mockServer.tool).toHaveBeenCalledWith( - 'list-error-groups', + 'gcp-error-reporting-list-groups', expect.any(Object), expect.any(Function) ); expect(mockServer.tool).toHaveBeenCalledWith( - 'get-error-group-details', + 'gcp-error-reporting-get-group-details', expect.any(Object), expect.any(Function) ); expect(mockServer.tool).toHaveBeenCalledWith( - 'analyse-error-trends', + 'gcp-error-reporting-analyse-trends', expect.any(Object), expect.any(Function) ); @@ -171,7 +171,7 @@ describe('Error Reporting Tools', () => { registerErrorReportingTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'list-error-groups' + call => call[0] === 'gcp-error-reporting-list-groups' ); expect(toolCall).toBeDefined(); @@ -220,7 +220,7 @@ describe('Error Reporting Tools', () => { registerErrorReportingTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'list-error-groups' + call => call[0] === 'gcp-error-reporting-list-groups' ); const toolHandler = toolCall![2]; @@ -244,7 +244,7 @@ describe('Error Reporting Tools', () => { registerErrorReportingTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'list-error-groups' + call => call[0] === 'gcp-error-reporting-list-groups' ); const toolHandler = toolCall![2]; @@ -263,7 +263,7 @@ describe('Error Reporting Tools', () => { registerErrorReportingTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'list-error-groups' + call => call[0] === 'gcp-error-reporting-list-groups' ); const toolHandler = toolCall![2]; @@ -310,7 +310,7 @@ describe('Error Reporting Tools', () => { registerErrorReportingTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'get-error-group-details' + call => call[0] === 'gcp-error-reporting-get-group-details' ); const toolHandler = toolCall![2]; @@ -363,7 +363,7 @@ describe('Error Reporting Tools', () => { registerErrorReportingTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'get-error-group-details' + call => call[0] === 'gcp-error-reporting-get-group-details' ); const toolHandler = toolCall![2]; @@ -387,7 +387,7 @@ describe('Error Reporting Tools', () => { registerErrorReportingTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'get-error-group-details' + call => call[0] === 'gcp-error-reporting-get-group-details' ); const toolHandler = toolCall![2]; @@ -408,7 +408,7 @@ describe('Error Reporting Tools', () => { registerErrorReportingTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'analyse-error-trends' + call => call[0] === 'gcp-error-reporting-analyse-trends' ); const toolHandler = toolCall![2]; @@ -438,7 +438,7 @@ describe('Error Reporting Tools', () => { registerErrorReportingTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'analyse-error-trends' + call => call[0] === 'gcp-error-reporting-analyse-trends' ); const toolHandler = toolCall![2]; @@ -465,7 +465,7 @@ describe('Error Reporting Tools', () => { registerErrorReportingTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'analyse-error-trends' + call => call[0] === 'gcp-error-reporting-analyse-trends' ); const toolHandler = toolCall![2]; @@ -498,7 +498,7 @@ describe('Error Reporting Tools', () => { registerErrorReportingTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'analyse-error-trends' + call => call[0] === 'gcp-error-reporting-analyse-trends' ); const toolHandler = toolCall![2]; @@ -519,7 +519,7 @@ describe('Error Reporting Tools', () => { registerErrorReportingTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'list-error-groups' + call => call[0] === 'gcp-error-reporting-list-groups' ); const toolHandler = toolCall![2]; diff --git a/test/unit/services/iam/tools.test.ts b/test/unit/services/iam/tools.test.ts index 3336c00..6dc16b3 100644 --- a/test/unit/services/iam/tools.test.ts +++ b/test/unit/services/iam/tools.test.ts @@ -27,25 +27,25 @@ describe('IAM Tools', () => { registerIamTools(mockServer as any); expect(mockServer.registerTool).toHaveBeenCalledWith( - 'get-project-iam-policy', + 'gcp-iam-get-project-policy', expect.any(Object), expect.any(Function) ); expect(mockServer.registerTool).toHaveBeenCalledWith( - 'test-project-permissions', + 'gcp-iam-test-project-permissions', expect.any(Object), expect.any(Function) ); expect(mockServer.registerTool).toHaveBeenCalledWith( - 'test-resource-permissions', + 'gcp-iam-test-resource-permissions', expect.any(Object), expect.any(Function) ); expect(mockServer.registerTool).toHaveBeenCalledWith( - 'validate-deployment-permissions', + 'gcp-iam-validate-deployment-permissions', expect.any(Object), expect.any(Function) ); @@ -58,7 +58,7 @@ describe('IAM Tools', () => { // Get the registered tool handler const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'get-project-iam-policy' + call => call[0] === 'gcp-iam-get-project-policy' ); expect(toolCall).toBeDefined(); @@ -77,7 +77,7 @@ describe('IAM Tools', () => { registerIamTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'test-project-permissions' + call => call[0] === 'gcp-iam-test-project-permissions' ); expect(toolCall).toBeDefined(); @@ -103,7 +103,7 @@ describe('IAM Tools', () => { registerIamTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'validate-deployment-permissions' + call => call[0] === 'gcp-iam-validate-deployment-permissions' ); expect(toolCall).toBeDefined(); @@ -125,7 +125,7 @@ describe('IAM Tools', () => { registerIamTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'validate-deployment-permissions' + call => call[0] === 'gcp-iam-validate-deployment-permissions' ); const toolHandler = toolCall![2]; @@ -143,7 +143,7 @@ describe('IAM Tools', () => { registerIamTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'analyse-permission-gaps' + call => call[0] === 'gcp-iam-analyse-permission-gaps' ); expect(toolCall).toBeDefined(); @@ -168,7 +168,7 @@ describe('IAM Tools', () => { registerIamTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'get-project-iam-policy' + call => call[0] === 'gcp-iam-get-project-policy' ); const toolHandler = toolCall![2]; diff --git a/test/unit/services/logging/tools.test.ts b/test/unit/services/logging/tools.test.ts index 0bebef4..fb971e1 100644 --- a/test/unit/services/logging/tools.test.ts +++ b/test/unit/services/logging/tools.test.ts @@ -27,13 +27,13 @@ describe('Logging Tools', () => { registerLoggingTools(mockServer as any); expect(mockServer.registerTool).toHaveBeenCalledWith( - 'query-logs', + 'gcp-logging-query-logs', expect.any(Object), expect.any(Function) ); expect(mockServer.registerTool).toHaveBeenCalledWith( - 'search-logs-comprehensive', + 'gcp-logging-search-comprehensive', expect.any(Object), expect.any(Function) ); @@ -45,7 +45,7 @@ describe('Logging Tools', () => { registerLoggingTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'query-logs' + call => call[0] === 'gcp-logging-query-logs' ); expect(toolCall).toBeDefined(); @@ -67,7 +67,7 @@ describe('Logging Tools', () => { registerLoggingTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'search-logs-comprehensive' + call => call[0] === 'gcp-logging-search-comprehensive' ); expect(toolCall).toBeDefined(); @@ -92,7 +92,7 @@ describe('Logging Tools', () => { registerLoggingTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'query-logs' + call => call[0] === 'gcp-logging-query-logs' ); const toolHandler = toolCall![2]; diff --git a/test/unit/services/monitoring/tools.test.ts b/test/unit/services/monitoring/tools.test.ts index 904008b..8f1e8be 100644 --- a/test/unit/services/monitoring/tools.test.ts +++ b/test/unit/services/monitoring/tools.test.ts @@ -27,13 +27,13 @@ describe('Monitoring Tools', () => { await registerMonitoringTools(mockServer as any); expect(mockServer.tool).toHaveBeenCalledWith( - 'query-metrics', + 'gcp-monitoring-query-metrics', expect.any(Object), expect.any(Function) ); expect(mockServer.tool).toHaveBeenCalledWith( - 'list-metric-types', + 'gcp-monitoring-list-metric-types', expect.any(Object), expect.any(Function) ); @@ -52,7 +52,7 @@ describe('Monitoring Tools', () => { await registerMonitoringTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'list-metric-types' + call => call[0] === 'gcp-monitoring-list-metric-types' ); expect(toolCall).toBeDefined(); @@ -82,7 +82,7 @@ describe('Monitoring Tools', () => { await registerMonitoringTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'query-metrics' + call => call[0] === 'gcp-monitoring-query-metrics' ); expect(toolCall).toBeDefined(); @@ -106,7 +106,7 @@ describe('Monitoring Tools', () => { await registerMonitoringTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'list-metric-types' + call => call[0] === 'gcp-monitoring-list-metric-types' ); const toolHandler = toolCall![2]; diff --git a/test/unit/services/profiler/tools.test.ts b/test/unit/services/profiler/tools.test.ts new file mode 100644 index 0000000..9c81cc0 --- /dev/null +++ b/test/unit/services/profiler/tools.test.ts @@ -0,0 +1,387 @@ +/** + * Tests for Profiler service tools + */ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Import mocks first +import '../../../mocks/google-cloud-mocks.js'; +import { createMockMcpServer } from '../../../utils/test-helpers.js'; + +// Create specific Profiler auth mock +const mockProfilerAuth = { + getClient: vi.fn().mockResolvedValue({ + getAccessToken: vi.fn().mockResolvedValue({ token: 'mock-token' }) + }), + getProjectId: vi.fn().mockResolvedValue('test-project'), +}; + +// Mock the auth module specifically for Profiler +vi.mock('../../../../src/utils/auth.js', () => ({ + initGoogleAuth: vi.fn().mockResolvedValue(mockProfilerAuth), + getProjectId: vi.fn().mockResolvedValue('test-project'), +})); + +// Mock global fetch +global.fetch = vi.fn(); +const mockFetch = fetch as any; + +// Mock profiler data +const mockProfiles = [ + { + name: 'projects/test-project/profiles/profile-123', + profileType: 'CPU', + deployment: { + projectId: 'test-project', + target: 'test-service', + labels: { + 'version': '1.0.0' + } + }, + duration: 'PT60S', + profileBytes: 'gzipped-profile-data', + labels: { + 'language': 'go' + }, + startTime: '2024-01-01T12:00:00Z' + }, + { + name: 'projects/test-project/profiles/profile-456', + profileType: 'HEAP', + deployment: { + projectId: 'test-project', + target: 'worker-service', + labels: { + 'version': '2.0.0' + } + }, + duration: 'PT30S', + profileBytes: 'gzipped-heap-data', + labels: { + 'language': 'java' + }, + startTime: '2024-01-01T11:00:00Z' + } +]; + +describe('Profiler Tools', () => { + let mockServer: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + mockServer = createMockMcpServer(); + + // Reset auth mock to ensure it always returns the mocked auth client + const mockClient = { getAccessToken: vi.fn().mockResolvedValue({ token: 'mock-token' }) }; + mockProfilerAuth.getClient.mockResolvedValue(mockClient); + mockProfilerAuth.getProjectId.mockResolvedValue('test-project'); + + // Mock successful fetch responses by default + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({ + profiles: mockProfiles + }), + text: vi.fn().mockResolvedValue('{}') + }); + }); + + afterEach(() => { + vi.resetAllMocks(); + }); + + describe('registerProfilerTools', () => { + it('should register profiler tools with MCP server', async () => { + // Ensure auth mock is set up correctly for this test + const authModule = await import('../../../../src/utils/auth.js'); + vi.mocked(authModule.initGoogleAuth).mockResolvedValue(mockProfilerAuth); + vi.mocked(authModule.getProjectId).mockResolvedValue('test-project'); + + const { registerProfilerTools } = await import('../../../../src/services/profiler/tools.js'); + + registerProfilerTools(mockServer as any); + + expect(mockServer.tool).toHaveBeenCalledWith( + 'gcp-profiler-list-profiles', + expect.any(Object), + expect.any(Function) + ); + + expect(mockServer.tool).toHaveBeenCalledWith( + 'gcp-profiler-analyse-performance', + expect.any(Object), + expect.any(Function) + ); + + expect(mockServer.tool).toHaveBeenCalledWith( + 'gcp-profiler-compare-trends', + expect.any(Object), + expect.any(Function) + ); + }); + + describe('list-profiles tool', () => { + it('should handle successful profile listing', async () => { + // Ensure auth mock is set up correctly for this test + const authModule = await import('../../../../src/utils/auth.js'); + vi.mocked(authModule.initGoogleAuth).mockResolvedValue(mockProfilerAuth); + vi.mocked(authModule.getProjectId).mockResolvedValue('test-project'); + + const { registerProfilerTools } = await import('../../../../src/services/profiler/tools.js'); + + registerProfilerTools(mockServer as any); + + const toolCall = mockServer.tool.mock.calls.find( + call => call[0] === 'gcp-profiler-list-profiles' + ); + + expect(toolCall).toBeDefined(); + + const toolHandler = toolCall![2]; + const result = await toolHandler({ + pageSize: 50 + }); + + expect(result).toBeDefined(); + expect(result.content).toBeDefined(); + expect(result.content[0].text).toContain('Profiler Analysis'); + expect(result.content[0].text).toContain('test-service'); + expect(result.content[0].text).toContain('CPU Time'); + + // Verify fetch was called with correct parameters + expect(mockFetch).toHaveBeenCalledWith( + expect.stringContaining('projects/test-project/profiles'), + expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ + 'Authorization': 'Bearer mock-token' + }) + }) + ); + }); + + it('should handle empty profiles response', async () => { + // Mock empty response + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({ profiles: [] }), + text: vi.fn().mockResolvedValue('{}') + }); + + // Ensure auth mock is set up correctly for this test + const authModule = await import('../../../../src/utils/auth.js'); + vi.mocked(authModule.initGoogleAuth).mockResolvedValue(mockProfilerAuth); + vi.mocked(authModule.getProjectId).mockResolvedValue('test-project'); + + const { registerProfilerTools } = await import('../../../../src/services/profiler/tools.js'); + + registerProfilerTools(mockServer as any); + + const toolCall = mockServer.tool.mock.calls.find( + call => call[0] === 'gcp-profiler-list-profiles' + ); + + const toolHandler = toolCall![2]; + const result = await toolHandler({ + pageSize: 50 + }); + + expect(result.content[0].text).toContain('No profiles found'); + }); + + it('should handle API errors gracefully', async () => { + // Mock API error + mockFetch.mockResolvedValue({ + ok: false, + status: 403, + text: vi.fn().mockResolvedValue('Permission denied') + }); + + // Ensure auth mock is set up correctly for this test + const authModule = await import('../../../../src/utils/auth.js'); + vi.mocked(authModule.initGoogleAuth).mockResolvedValue(mockProfilerAuth); + vi.mocked(authModule.getProjectId).mockResolvedValue('test-project'); + + const { registerProfilerTools } = await import('../../../../src/services/profiler/tools.js'); + + registerProfilerTools(mockServer as any); + + const toolCall = mockServer.tool.mock.calls.find( + call => call[0] === 'gcp-profiler-list-profiles' + ); + + const toolHandler = toolCall![2]; + + await expect(toolHandler({ pageSize: 50 })).rejects.toThrow(); + }); + + it('should handle profile type filtering correctly', async () => { + // Ensure auth mock is set up correctly for this test + const authModule = await import('../../../../src/utils/auth.js'); + vi.mocked(authModule.initGoogleAuth).mockResolvedValue(mockProfilerAuth); + vi.mocked(authModule.getProjectId).mockResolvedValue('test-project'); + + const { registerProfilerTools } = await import('../../../../src/services/profiler/tools.js'); + + registerProfilerTools(mockServer as any); + + const toolCall = mockServer.tool.mock.calls.find( + call => call[0] === 'gcp-profiler-list-profiles' + ); + + const toolHandler = toolCall![2]; + await toolHandler({ + pageSize: 50, + profileType: 'CPU', + target: 'test-service' + }); + + // Check that the URL was built correctly + const fetchCall = mockFetch.mock.calls[0]; + expect(fetchCall[0]).toContain('pageSize=50'); + + // Note: Client-side filtering is applied after fetch, so we don't see filters in URL + }); + }); + + describe('analyse-profile-performance tool', () => { + it('should analyse profile performance successfully', async () => { + // Ensure auth mock is set up correctly for this test + const authModule = await import('../../../../src/utils/auth.js'); + vi.mocked(authModule.initGoogleAuth).mockResolvedValue(mockProfilerAuth); + vi.mocked(authModule.getProjectId).mockResolvedValue('test-project'); + + const { registerProfilerTools } = await import('../../../../src/services/profiler/tools.js'); + + registerProfilerTools(mockServer as any); + + const toolCall = mockServer.tool.mock.calls.find( + call => call[0] === 'gcp-profiler-analyse-performance' + ); + + const toolHandler = toolCall![2]; + const result = await toolHandler({ + profileType: 'CPU', + pageSize: 100 + }); + + expect(result.content[0].text).toContain('Profile Performance Analysis'); + expect(result.content[0].text).toContain('CPU Time - Shows where your application spends CPU time'); + expect(result.content[0].text).toContain('Performance Insights'); + expect(result.content[0].text).toContain('Actionable Recommendations'); + + // Verify correct API call + const fetchCall = mockFetch.mock.calls[0]; + expect(fetchCall[0]).toContain('pageSize=100'); + }); + + it('should handle no profiles found for analysis', async () => { + // Mock empty response + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({ profiles: [] }), + text: vi.fn().mockResolvedValue('{}') + }); + + // Ensure auth mock is set up correctly for this test + const authModule = await import('../../../../src/utils/auth.js'); + vi.mocked(authModule.initGoogleAuth).mockResolvedValue(mockProfilerAuth); + vi.mocked(authModule.getProjectId).mockResolvedValue('test-project'); + + const { registerProfilerTools } = await import('../../../../src/services/profiler/tools.js'); + + registerProfilerTools(mockServer as any); + + const toolCall = mockServer.tool.mock.calls.find( + call => call[0] === 'gcp-profiler-analyse-performance' + ); + + const toolHandler = toolCall![2]; + const result = await toolHandler({ + profileType: 'HEAP' + }); + + expect(result.content[0].text).toContain('No profiles found for analysis'); + }); + }); + + describe('compare-profile-trends tool', () => { + it('should compare profile trends successfully', async () => { + // Ensure auth mock is set up correctly for this test + const authModule = await import('../../../../src/utils/auth.js'); + vi.mocked(authModule.initGoogleAuth).mockResolvedValue(mockProfilerAuth); + vi.mocked(authModule.getProjectId).mockResolvedValue('test-project'); + + const { registerProfilerTools } = await import('../../../../src/services/profiler/tools.js'); + + registerProfilerTools(mockServer as any); + + const toolCall = mockServer.tool.mock.calls.find( + call => call[0] === 'gcp-profiler-compare-trends' + ); + + const toolHandler = toolCall![2]; + const result = await toolHandler({ + target: 'test-service', + pageSize: 200 + }); + + expect(result.content[0].text).toContain('Profile Trend Analysis'); + expect(result.content[0].text).toContain('Analysed: 1 profiles'); // After filtering for test-service + + // Verify correct API call with larger page size for trends + const fetchCall = mockFetch.mock.calls[0]; + expect(fetchCall[0]).toContain('pageSize=200'); + }); + + it('should handle empty trends data', async () => { + // Mock empty response + mockFetch.mockResolvedValue({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValue({ profiles: [] }), + text: vi.fn().mockResolvedValue('{}') + }); + + // Ensure auth mock is set up correctly for this test + const authModule = await import('../../../../src/utils/auth.js'); + vi.mocked(authModule.initGoogleAuth).mockResolvedValue(mockProfilerAuth); + vi.mocked(authModule.getProjectId).mockResolvedValue('test-project'); + + const { registerProfilerTools } = await import('../../../../src/services/profiler/tools.js'); + + registerProfilerTools(mockServer as any); + + const toolCall = mockServer.tool.mock.calls.find( + call => call[0] === 'gcp-profiler-compare-trends' + ); + + const toolHandler = toolCall![2]; + const result = await toolHandler({ + profileType: 'CPU' + }); + + expect(result.content[0].text).toContain('No profiles found for trend analysis'); + }); + }); + + it('should handle authentication errors', async () => { + // Mock auth failure + mockProfilerAuth.getClient.mockRejectedValue(new Error('Auth failed')); + + const { registerProfilerTools } = await import('../../../../src/services/profiler/tools.js'); + + registerProfilerTools(mockServer as any); + + const toolCall = mockServer.tool.mock.calls.find( + call => call[0] === 'gcp-profiler-list-profiles' + ); + + const toolHandler = toolCall![2]; + + await expect(toolHandler({ pageSize: 50 })).rejects.toThrow(); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/services/profiler/types.test.ts b/test/unit/services/profiler/types.test.ts new file mode 100644 index 0000000..b019693 --- /dev/null +++ b/test/unit/services/profiler/types.test.ts @@ -0,0 +1,295 @@ +/** + * Tests for Profiler service types and utilities + */ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Import mocks first +import '../../../mocks/google-cloud-mocks.js'; + +// Create specific Profiler auth mock +const mockProfilerAuth = { + getClient: vi.fn().mockResolvedValue({ + getAccessToken: vi.fn().mockResolvedValue({ token: 'mock-token' }) + }), + getProjectId: vi.fn().mockResolvedValue('test-project'), +}; + +// Mock the auth module specifically for Profiler +vi.mock('../../../../src/utils/auth.js', () => ({ + initGoogleAuth: vi.fn().mockResolvedValue(mockProfilerAuth), + getProjectId: vi.fn().mockResolvedValue('test-project'), +})); + +describe('Profiler Types and Utilities', () => { + beforeEach(() => { + vi.clearAllMocks(); + const mockClient = { getAccessToken: vi.fn().mockResolvedValue({ token: 'mock-token' }) }; + mockProfilerAuth.getClient.mockResolvedValue(mockClient); + mockProfilerAuth.getProjectId.mockResolvedValue('test-project'); + }); + + describe('formatProfileSummary', () => { + it('should format profile summary correctly', async () => { + const { formatProfileSummary, ProfileType } = await import('../../../../src/services/profiler/types.js'); + + const mockProfile = { + name: 'projects/test-project/profiles/test-profile-123', + profileType: ProfileType.CPU, + deployment: { + projectId: 'test-project', + target: 'test-service', + labels: { + 'env': 'production', + 'version': '1.0.0' + } + }, + duration: 'PT60S', + profileBytes: 'gzipped-data', + labels: { + 'language': 'go', + 'source': 'manual' + }, + startTime: '2024-01-01T12:00:00Z' + }; + + const formatted = formatProfileSummary(mockProfile); + + expect(formatted).toMatch(/^## Profile:/); // Starts with ## Profile: + expect(formatted).toContain('test-profile-123'); // Contains profile ID + expect(formatted).toContain('CPU Time - Shows where your application spends CPU time'); // Contains type description + expect(formatted).toContain('test-service'); // Contains target + expect(formatted).toContain('test-project'); // Contains project + expect(formatted).toContain('60 seconds'); // Contains formatted duration + expect(formatted).toContain('language: go'); // Contains labels + expect(formatted).toContain('env: production'); // Contains deployment labels + }); + + it('should handle profile with minimal data', async () => { + const { formatProfileSummary, ProfileType } = await import('../../../../src/services/profiler/types.js'); + + const minimalProfile = { + name: 'projects/test-project/profiles/minimal', + profileType: ProfileType.HEAP, + deployment: { + projectId: 'test-project', + target: 'minimal-service', + labels: {} + }, + duration: 'PT30S', + profileBytes: 'data', + labels: {}, + startTime: '2024-01-01T09:00:00Z' + }; + + const formatted = formatProfileSummary(minimalProfile); + + expect(formatted).toContain('minimal'); + expect(formatted).toContain('Heap Memory'); // Description for HEAP type + expect(formatted).toContain('minimal-service'); + expect(formatted).toContain('30 seconds'); + }); + + it('should handle profile with missing optional fields', async () => { + const { formatProfileSummary, ProfileType } = await import('../../../../src/services/profiler/types.js'); + + const profileWithoutOptionals = { + name: 'projects/test-project/profiles/basic', + profileType: ProfileType.WALL, + deployment: { + projectId: 'test-project', + target: 'basic-service', + labels: {} + }, + duration: 'PT120S', + profileBytes: 'data', + labels: {}, + startTime: '2024-01-01T10:00:00Z' + }; + + const formatted = formatProfileSummary(profileWithoutOptionals); + + expect(formatted).toContain('basic'); + expect(formatted).toContain('Wall Time'); // Description for WALL type + expect(formatted).toContain('basic-service'); + expect(formatted).toContain('120 seconds'); + }); + + it('should handle undefined/null fields safely', async () => { + const { formatProfileSummary } = await import('../../../../src/services/profiler/types.js'); + + const profileWithNulls = { + name: '', + profileType: 'UNKNOWN_TYPE' as any, + deployment: { + projectId: '', + target: '', + labels: {} + }, + duration: '', + profileBytes: '', + labels: {}, + startTime: '' + }; + + const formatted = formatProfileSummary(profileWithNulls); + + expect(formatted).toContain('Unknown'); // Default values + }); + }); + + describe('getProfileTypeDescription', () => { + it('should return correct descriptions for profile types', async () => { + const { getProfileTypeDescription, ProfileType } = await import('../../../../src/services/profiler/types.js'); + + expect(getProfileTypeDescription(ProfileType.CPU)).toContain('CPU Time'); + expect(getProfileTypeDescription(ProfileType.HEAP)).toContain('Heap Memory'); + expect(getProfileTypeDescription(ProfileType.WALL)).toContain('Wall Time'); + expect(getProfileTypeDescription(ProfileType.CONTENTION)).toContain('Contention'); + expect(getProfileTypeDescription(ProfileType.THREADS)).toContain('Threads'); + expect(getProfileTypeDescription('UNKNOWN')).toContain('UNKNOWN'); + }); + }); + + describe('formatDuration', () => { + it('should format ISO 8601 durations correctly', async () => { + const { formatDuration } = await import('../../../../src/services/profiler/types.js'); + + expect(formatDuration('PT30S')).toBe('30 seconds'); + expect(formatDuration('PT5M')).toBe('5 minutes'); + expect(formatDuration('PT2H')).toBe('2 hours'); + expect(formatDuration('PT90S')).toBe('90 seconds'); + expect(formatDuration('invalid')).toBe('invalid'); + expect(formatDuration('')).toBe('Unknown'); + }); + }); + + describe('analyseProfilePatterns', () => { + it('should analyse profile patterns and provide insights', async () => { + const { analyseProfilePatterns, ProfileType } = await import('../../../../src/services/profiler/types.js'); + + const mockProfiles = [ + { + name: 'projects/test/profiles/profile-1', + profileType: ProfileType.CPU, + deployment: { + projectId: 'test-project', + target: 'api-service', + labels: {} + }, + duration: 'PT60S', + profileBytes: 'data', + labels: {}, + startTime: new Date().toISOString() // Recent profile + }, + { + name: 'projects/test/profiles/profile-2', + profileType: ProfileType.HEAP, + deployment: { + projectId: 'test-project', + target: 'worker-service', + labels: {} + }, + duration: 'PT30S', + profileBytes: 'data', + labels: {}, + startTime: '2024-01-01T09:00:00Z' // Older profile + } + ]; + + const analysis = analyseProfilePatterns(mockProfiles); + + expect(analysis).toContain('Profile Analysis and Performance Insights'); + expect(analysis).toContain('**Total Profiles:** 2'); + expect(analysis).toContain('**Profile Types:** 2'); + expect(analysis).toContain('**Targets:** 2'); + expect(analysis).toContain('Profile Type Distribution'); + expect(analysis).toContain('CPU Time'); // Should contain CPU description + expect(analysis).toContain('Heap Memory'); // Should contain Heap description + expect(analysis).toContain('Recent Profile Activity'); + expect(analysis).toContain('Performance Analysis by Profile Type'); + expect(analysis).toContain('Recommendations'); + }); + + it('should handle empty profile array', async () => { + const { analyseProfilePatterns } = await import('../../../../src/services/profiler/types.js'); + + const analysis = analyseProfilePatterns([]); + + expect(analysis).toBe('No profiles found in the specified criteria.'); + }); + + it('should group profiles by type correctly', async () => { + const { analyseProfilePatterns, ProfileType } = await import('../../../../src/services/profiler/types.js'); + + const profilesMultipleTypes = [ + { + name: 'profile-1', + profileType: ProfileType.CPU, + deployment: { projectId: 'test', target: 'service-a', labels: {} }, + duration: 'PT60S', + profileBytes: 'data', + labels: {}, + startTime: '2024-01-01T09:00:00Z' + }, + { + name: 'profile-2', + profileType: ProfileType.CPU, + deployment: { projectId: 'test', target: 'service-b', labels: {} }, + duration: 'PT60S', + profileBytes: 'data', + labels: {}, + startTime: '2024-01-01T09:00:00Z' + }, + { + name: 'profile-3', + profileType: ProfileType.HEAP, + deployment: { projectId: 'test', target: 'service-a', labels: {} }, + duration: 'PT60S', + profileBytes: 'data', + labels: {}, + startTime: '2024-01-01T09:00:00Z' + } + ]; + + const analysis = analyseProfilePatterns(profilesMultipleTypes); + + expect(analysis).toContain('**Profile Types:** 2 (CPU, HEAP)'); + expect(analysis).toContain('**Targets:** 2 (service-a, service-b)'); + }); + }); + + describe('getProfilerAuth', () => { + it('should return authentication client and token', async () => { + const { getProfilerAuth } = await import('../../../../src/services/profiler/types.js'); + + const authResult = await getProfilerAuth(); + + expect(authResult).toBeDefined(); + expect(authResult.auth).toBeDefined(); + expect(authResult.token).toBe('mock-token'); + }); + + it('should handle authentication failure', async () => { + const { getProfilerAuth } = await import('../../../../src/services/profiler/types.js'); + + // Mock auth failure + const mockClient = { getAccessToken: vi.fn().mockRejectedValue(new Error('Auth failed')) }; + mockProfilerAuth.getClient.mockResolvedValue(mockClient); + + await expect(getProfilerAuth()).rejects.toThrow('Auth failed'); + }); + + it('should handle missing auth client', async () => { + // Re-import the module with mocked auth returning null + vi.doMock('../../../../src/utils/auth.js', () => ({ + initGoogleAuth: vi.fn().mockResolvedValue(null), + getProjectId: vi.fn().mockResolvedValue('test-project'), + })); + + // Need to re-import after mocking + const { getProfilerAuth } = await import('../../../../src/services/profiler/types.js?timestamp=' + Date.now()); + + await expect(getProfilerAuth()).rejects.toThrow('Google Cloud authentication not available'); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/services/spanner/tools.test.ts b/test/unit/services/spanner/tools.test.ts index 32a7f61..a16e0a8 100644 --- a/test/unit/services/spanner/tools.test.ts +++ b/test/unit/services/spanner/tools.test.ts @@ -49,13 +49,13 @@ describe('Spanner Tools', () => { registerSpannerTools(mockServer as any); expect(mockServer.tool).toHaveBeenCalledWith( - 'execute-spanner-query', + 'gcp-spanner-execute-query', expect.any(Object), expect.any(Function) ); expect(mockServer.tool).toHaveBeenCalledWith( - 'list-spanner-databases', + 'gcp-spanner-list-databases', expect.any(Object), expect.any(Function) ); @@ -67,7 +67,7 @@ describe('Spanner Tools', () => { registerSpannerTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'execute-spanner-query' + call => call[0] === 'gcp-spanner-execute-query' ); expect(toolCall).toBeDefined(); @@ -89,7 +89,7 @@ describe('Spanner Tools', () => { registerSpannerTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'list-spanner-databases' + call => call[0] === 'gcp-spanner-list-databases' ); expect(toolCall).toBeDefined(); @@ -122,7 +122,7 @@ describe('Spanner Tools', () => { registerSpannerTools(mockServer as any); const toolCall = mockServer.tool.mock.calls.find( - call => call[0] === 'execute-spanner-query' + call => call[0] === 'gcp-spanner-execute-query' ); const toolHandler = toolCall![2]; diff --git a/test/unit/utils/project-tools.test.ts b/test/unit/utils/project-tools.test.ts index 27734e1..c002f4c 100644 --- a/test/unit/utils/project-tools.test.ts +++ b/test/unit/utils/project-tools.test.ts @@ -22,13 +22,13 @@ describe('Project Tools', () => { registerProjectTools(mockServer as any); expect(mockServer.registerTool).toHaveBeenCalledWith( - 'set-project-id', + 'gcp-utils-set-project-id', expect.any(Object), expect.any(Function) ); expect(mockServer.registerTool).toHaveBeenCalledWith( - 'get-project-id', + 'gcp-utils-get-project-id', expect.any(Object), expect.any(Function) ); @@ -40,7 +40,7 @@ describe('Project Tools', () => { registerProjectTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'set-project-id' + call => call[0] === 'gcp-utils-set-project-id' ); expect(toolCall).toBeDefined(); @@ -59,7 +59,7 @@ describe('Project Tools', () => { registerProjectTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'get-project-id' + call => call[0] === 'gcp-utils-get-project-id' ); expect(toolCall).toBeDefined(); @@ -78,7 +78,7 @@ describe('Project Tools', () => { registerProjectTools(mockServer as any); const toolCall = mockServer.registerTool.mock.calls.find( - call => call[0] === 'set-project-id' + call => call[0] === 'gcp-utils-set-project-id' ); const toolHandler = toolCall![2];