UNPKG

mcp-ga4-data

Version:

Google Analytics 4 Data API tools via Model Context Protocol

520 lines (519 loc) 23.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); // src/index.ts const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const zod_1 = require("zod"); const data_1 = require("@google-analytics/data"); const dotenv_1 = __importDefault(require("dotenv")); const google_auth_library_1 = require("google-auth-library"); // Needed for error type checking const fs = __importStar(require("fs")); const path = __importStar(require("path")); // Load environment variables from .env file if it exists try { dotenv_1.default.config({ path: process.env.ENV_FILE || '.env' }); } catch (error) { console.error("Note: No .env file found, using environment variables directly."); } // --- Configuration --- const MCP_PROTOCOL_VERSION = "1.0"; // Can also come from the SDK // --- Initialize Google Analytics Admin Client --- let analyticsDataClient = null; try { // The client automatically uses the credentials from the // GOOGLE_APPLICATION_CREDENTIALS environment variable. analyticsDataClient = new data_1.BetaAnalyticsDataClient(); console.error("Google Analytics Clients initialized."); // Log to stderr } catch (error) { console.error("Error initializing Google Clients:", error); // Stop the process if the client cannot be initialized process.exit(1); } // Read version from package.json let packageVersion = "1.0.0"; // Default version try { const packageJsonPath = path.resolve(__dirname, '../package.json'); if (fs.existsSync(packageJsonPath)) { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); packageVersion = packageJson.version || packageVersion; } } catch (error) { console.error("Kon package.json niet lezen, gebruik standaard versie:", error); } // --- Create MCP Server Instance --- const server = new mcp_js_1.McpServer({ name: "google-analytics-data", version: packageVersion, protocolVersion: MCP_PROTOCOL_VERSION, // Specify protocol version // descriptions and other metadata can be added here }); // --- Helper function for error messages --- function createErrorResponse(message, error) { let detailedMessage = message; if (error) { // Try to recognize specific Google API errors if (error.code && error.details) { // Standard gRPC error structure detailedMessage = `${message}: Google API Error ${error.code} - ${error.details}`; } else if (error instanceof Error) { detailedMessage = `${message}: ${error.message}`; } else { detailedMessage = `${message}: ${String(error)}`; } } console.error("MCP Tool Error:", detailedMessage); // Log errors to stderr return { isError: true, content: [{ type: "text", text: detailedMessage }], }; } // --- Helper function to obtain an access token for the Google API --- async function getAccessToken() { try { const auth = new google_auth_library_1.GoogleAuth({ scopes: ["https://www.googleapis.com/auth/analytics.readonly"], }); const client = await auth.getClient(); const token = await client.getAccessToken(); return token.token || ""; } catch (error) { console.error("Error getting access token:", error); throw error; } } // --- Tool: Run Report --- server.tool("ga4_data_api_run_report", "Run a report using the Google Analytics 4 Data API.", { property_id: zod_1.z.string().regex(/^\d+$/, "Property ID must be numeric").describe("The numeric ID of the GA4 property (e.g., '123456789')"), date_ranges: zod_1.z.array(zod_1.z.object({ start_date: zod_1.z.string().describe("Start date in YYYY-MM-DD format"), end_date: zod_1.z.string().describe("End date in YYYY-MM-DD format") })).describe("Date ranges for the report"), dimensions: zod_1.z.array(zod_1.z.string()).describe("List of dimensions (e.g., ['date', 'country', 'deviceCategory'])"), metrics: zod_1.z.array(zod_1.z.string()).describe("List of metrics (e.g., ['activeUsers', 'sessions', 'screenPageViews'])"), limit: zod_1.z.number().optional().describe("Optional limit for the number of rows returned"), offset: zod_1.z.number().optional().describe("Optional offset for pagination"), dimension_filter: zod_1.z.string().optional().describe("Optional JSON string with dimension filter"), metric_filter: zod_1.z.string().optional().describe("Optional JSON string with metric filter"), order_bys: zod_1.z.array(zod_1.z.object({ dimension: zod_1.z.string().optional(), metric: zod_1.z.string().optional(), desc: zod_1.z.boolean().optional() })).optional().describe("Optional ordering of results") }, async ({ property_id, date_ranges, dimensions, metrics, limit, offset, dimension_filter, metric_filter, order_bys }) => { if (!analyticsDataClient) { return createErrorResponse("GA Data Client is not initialized."); } console.error(`Running tool: ga4_data_api_run_report for property ${property_id}`); // Log to stderr try { // Prepare the request const request = { property: `properties/${property_id}`, dateRanges: date_ranges.map(range => ({ startDate: range.start_date, endDate: range.end_date })), dimensions: dimensions.map(dim => ({ name: dim })), metrics: metrics.map(metric => ({ name: metric })) }; // Add optional parameters if provided if (limit !== undefined) { request.limit = limit; } if (offset !== undefined) { request.offset = offset; } // Parse and add dimension filter if provided if (dimension_filter) { try { request.dimensionFilter = JSON.parse(dimension_filter); } catch (error) { return createErrorResponse("Invalid dimension_filter JSON format", error); } } // Parse and add metric filter if provided if (metric_filter) { try { request.metricFilter = JSON.parse(metric_filter); } catch (error) { return createErrorResponse("Invalid metric_filter JSON format", error); } } // Add order bys if provided if (order_bys && order_bys.length > 0) { request.orderBys = order_bys.map(orderBy => { const result = {}; if (orderBy.dimension) result.dimension = { dimensionName: orderBy.dimension }; if (orderBy.metric) result.metric = { metricName: orderBy.metric }; if (orderBy.desc !== undefined) result.desc = orderBy.desc; return result; }); } console.error('API Request (ga4_data_api_run_report):', JSON.stringify(request, null, 2)); // Run the report const [response] = await analyticsDataClient.runReport(request); // Format the response const formattedResponse = { dimensionHeaders: response.dimensionHeaders?.map(header => header.name) || [], metricHeaders: response.metricHeaders?.map(header => header.name) || [], rows: response.rows?.map(row => { return { dimensionValues: row.dimensionValues?.map(value => value.value) || [], metricValues: row.metricValues?.map(value => value.value) || [] }; }) || [], rowCount: response.rowCount, metadata: response.metadata, kind: response.kind }; return { content: [ { type: "text", text: JSON.stringify(formattedResponse, null, 2), }, ], }; } catch (error) { // Handle API-specific errors if (error.code === 5) { // gRPC code 5 = NOT_FOUND return createErrorResponse(`Property '${property_id}' not found.`, error); } if (error.code === 7) { // gRPC code 7 = PERMISSION_DENIED return createErrorResponse(`Permission denied to access property '${property_id}'. Check Service Account permissions in GA4.`, error); } return createErrorResponse(`Error running report for property '${property_id}'`, error); } }); // --- Tool: Run Realtime Report --- server.tool("ga4_data_api_run_realtime_report", "Run a realtime report using the Google Analytics 4 Data API.", { property_id: zod_1.z.string().regex(/^\d+$/, "Property ID must be numeric").describe("The numeric ID of the GA4 property (e.g., '123456789')"), dimensions: zod_1.z.array(zod_1.z.string()).describe("List of dimensions (e.g., ['country', 'deviceCategory'])"), metrics: zod_1.z.array(zod_1.z.string()).describe("List of metrics (e.g., ['activeUsers', 'screenPageViews'])"), limit: zod_1.z.number().optional().describe("Optional limit for the number of rows returned"), dimension_filter: zod_1.z.string().optional().describe("Optional JSON string with dimension filter"), metric_filter: zod_1.z.string().optional().describe("Optional JSON string with metric filter") }, async ({ property_id, dimensions, metrics, limit, dimension_filter, metric_filter }) => { if (!analyticsDataClient) { return createErrorResponse("GA Data Client is not initialized."); } console.error(`Running tool: ga4_data_api_run_realtime_report for property ${property_id}`); // Log to stderr try { // Prepare the request const request = { property: `properties/${property_id}`, dimensions: dimensions.map(dim => ({ name: dim })), metrics: metrics.map(metric => ({ name: metric })) }; // Add optional parameters if provided if (limit !== undefined) { request.limit = limit; } // Parse and add dimension filter if provided if (dimension_filter) { try { request.dimensionFilter = JSON.parse(dimension_filter); } catch (error) { return createErrorResponse("Invalid dimension_filter JSON format", error); } } // Parse and add metric filter if provided if (metric_filter) { try { request.metricFilter = JSON.parse(metric_filter); } catch (error) { return createErrorResponse("Invalid metric_filter JSON format", error); } } console.error('API Request (ga4_data_api_run_realtime_report):', JSON.stringify(request, null, 2)); // Run the realtime report const [response] = await analyticsDataClient.runRealtimeReport(request); // Format the response const formattedResponse = { dimensionHeaders: response.dimensionHeaders?.map(header => header.name) || [], metricHeaders: response.metricHeaders?.map(header => header.name) || [], rows: response.rows?.map(row => { return { dimensionValues: row.dimensionValues?.map(value => value.value) || [], metricValues: row.metricValues?.map(value => value.value) || [] }; }) || [], rowCount: response.rowCount, kind: response.kind }; return { content: [ { type: "text", text: JSON.stringify(formattedResponse, null, 2), }, ], }; } catch (error) { // Handle API-specific errors if (error.code === 5) { // gRPC code 5 = NOT_FOUND return createErrorResponse(`Property '${property_id}' not found.`, error); } if (error.code === 7) { // gRPC code 7 = PERMISSION_DENIED return createErrorResponse(`Permission denied to access property '${property_id}'. Check Service Account permissions in GA4.`, error); } return createErrorResponse(`Error running realtime report for property '${property_id}'`, error); } }); // --- Tool: Run Pivot Report --- server.tool("ga4_data_api_run_pivot_report", "Run a pivot report using the Google Analytics 4 Data API.", { property_id: zod_1.z.string().regex(/^\d+$/, "Property ID must be numeric").describe("The numeric ID of the GA4 property (e.g., '123456789')"), date_ranges: zod_1.z.array(zod_1.z.object({ start_date: zod_1.z.string().describe("Start date in YYYY-MM-DD format"), end_date: zod_1.z.string().describe("End date in YYYY-MM-DD format") })).describe("Date ranges for the report"), dimensions: zod_1.z.array(zod_1.z.string()).describe("List of dimensions (e.g., ['date', 'country', 'deviceCategory'])"), metrics: zod_1.z.array(zod_1.z.string()).describe("List of metrics (e.g., ['activeUsers', 'sessions', 'screenPageViews'])"), pivots: zod_1.z.array(zod_1.z.object({ field_names: zod_1.z.array(zod_1.z.string()).describe("List of dimensions to pivot on"), limit: zod_1.z.number().optional().describe("Optional limit for the number of pivot rows returned"), offset: zod_1.z.number().optional().describe("Optional offset for pagination in pivot rows"), order_bys: zod_1.z.array(zod_1.z.object({ dimension: zod_1.z.string().optional(), metric: zod_1.z.string().optional(), desc: zod_1.z.boolean().optional() })).optional().describe("Optional ordering of pivot results") })).describe("Pivot specifications"), limit: zod_1.z.number().optional().describe("Optional limit for the number of rows returned"), offset: zod_1.z.number().optional().describe("Optional offset for pagination"), dimension_filter: zod_1.z.string().optional().describe("Optional JSON string with dimension filter"), metric_filter: zod_1.z.string().optional().describe("Optional JSON string with metric filter"), order_bys: zod_1.z.array(zod_1.z.object({ dimension: zod_1.z.string().optional(), metric: zod_1.z.string().optional(), desc: zod_1.z.boolean().optional() })).optional().describe("Optional ordering of results") }, async ({ property_id, date_ranges, dimensions, metrics, pivots, limit, offset, dimension_filter, metric_filter, order_bys }) => { if (!analyticsDataClient) { return createErrorResponse("GA Data Client is not initialized."); } console.error(`Running tool: ga4_data_api_run_pivot_report for property ${property_id}`); // Log to stderr try { // Prepare the request const request = { property: `properties/${property_id}`, dateRanges: date_ranges.map(range => ({ startDate: range.start_date, endDate: range.end_date })), dimensions: dimensions.map(dim => ({ name: dim })), metrics: metrics.map(metric => ({ name: metric })), pivots: pivots.map(pivot => ({ fieldNames: pivot.field_names, limit: pivot.limit, offset: pivot.offset, orderBys: pivot.order_bys?.map(orderBy => { const result = {}; if (orderBy.dimension) result.dimension = { dimensionName: orderBy.dimension }; if (orderBy.metric) result.metric = { metricName: orderBy.metric }; if (orderBy.desc !== undefined) result.desc = orderBy.desc; return result; }) })) }; // Add optional parameters if provided if (limit !== undefined) { request.limit = limit; } if (offset !== undefined) { request.offset = offset; } // Parse and add dimension filter if provided if (dimension_filter) { try { request.dimensionFilter = JSON.parse(dimension_filter); } catch (error) { return createErrorResponse("Invalid dimension_filter JSON format", error); } } // Parse and add metric filter if provided if (metric_filter) { try { request.metricFilter = JSON.parse(metric_filter); } catch (error) { return createErrorResponse("Invalid metric_filter JSON format", error); } } // Add order bys if provided if (order_bys && order_bys.length > 0) { request.orderBys = order_bys.map(orderBy => { const result = {}; if (orderBy.dimension) result.dimension = { dimensionName: orderBy.dimension }; if (orderBy.metric) result.metric = { metricName: orderBy.metric }; if (orderBy.desc !== undefined) result.desc = orderBy.desc; return result; }); } console.error('API Request (ga4_data_api_run_pivot_report):', JSON.stringify(request, null, 2)); // Run the pivot report const [response] = await analyticsDataClient.runPivotReport(request); // Format the response const formattedResponse = { pivotHeaders: response.pivotHeaders?.map((header) => ({ dimensionValues: header.dimensionValues?.map((value) => value.value) || [], })) || [], dimensionHeaders: response.dimensionHeaders?.map(header => header.name) || [], metricHeaders: response.metricHeaders?.map(header => header.name) || [], rows: response.rows?.map(row => { return { dimensionValues: row.dimensionValues?.map(value => value.value) || [], metricValues: row.metricValues?.map(value => value.value) || [], pivotValues: row.pivotValues?.map((pivotValueGroup) => ({ values: pivotValueGroup.values?.map((value) => value.value) || [] })) || [] }; }) || [], metadata: response.metadata, kind: response.kind }; return { content: [ { type: "text", text: JSON.stringify(formattedResponse, null, 2), }, ], }; } catch (error) { // Handle API-specific errors if (error.code === 5) { // gRPC code 5 = NOT_FOUND return createErrorResponse(`Property '${property_id}' not found.`, error); } if (error.code === 7) { // gRPC code 7 = PERMISSION_DENIED return createErrorResponse(`Permission denied to access property '${property_id}'. Check Service Account permissions in GA4.`, error); } return createErrorResponse(`Error running pivot report for property '${property_id}'`, error); } }); // --- Tool: Get Metadata --- server.tool("ga4_data_api_get_metadata", "Get metadata about available dimensions and metrics in GA4.", { property_id: zod_1.z.string().regex(/^\d+$/, "Property ID must be numeric").describe("The numeric ID of the GA4 property (e.g., '123456789')") }, async ({ property_id }) => { if (!analyticsDataClient) { return createErrorResponse("GA Data Client is not initialized."); } console.error(`Running tool: ga4_data_api_get_metadata for property ${property_id}`); // Log to stderr try { // Get the metadata const [metadata] = await analyticsDataClient.getMetadata({ name: `properties/${property_id}/metadata` }); // Format the response const formattedResponse = { dimensions: metadata.dimensions?.map(dim => ({ apiName: dim.apiName, uiName: dim.uiName, description: dim.description, category: dim.category, customDefinition: dim.customDefinition })) || [], metrics: metadata.metrics?.map(metric => ({ apiName: metric.apiName, uiName: metric.uiName, description: metric.description, category: metric.category, expression: metric.expression, type: metric.type })) || [] }; return { content: [ { type: "text", text: JSON.stringify(formattedResponse, null, 2), }, ], }; } catch (error) { // Handle API-specific errors if (error.code === 5) { // gRPC code 5 = NOT_FOUND return createErrorResponse(`Property '${property_id}' not found.`, error); } if (error.code === 7) { // gRPC code 7 = PERMISSION_DENIED return createErrorResponse(`Permission denied to access property '${property_id}'. Check Service Account permissions in GA4.`, error); } return createErrorResponse(`Error getting metadata for property '${property_id}'`, error); } }); // --- Start the server with Stdio Transport --- async function main() { try { // Use StdioServerTransport like in the example const transport = new stdio_js_1.StdioServerTransport(); await server.connect(transport); console.error("Google Analytics Data MCP Server running on stdio"); // Log to stderr } catch (error) { console.error("Fatal error connecting MCP server:", error); process.exit(1); } } // Run main function main().catch((error) => { console.error("Unhandled error in main():", error); process.exit(1); });