diff --git a/google-analytics/server/lib/schemas.ts b/google-analytics/server/lib/schemas.ts index cb336b72..746f4576 100644 --- a/google-analytics/server/lib/schemas.ts +++ b/google-analytics/server/lib/schemas.ts @@ -10,10 +10,8 @@ import { z } from "zod"; // ── Shared primitives ──────────────────────────────────────────────────────── -/** ISO-8601 timestamp string (e.g. "2024-01-15T10:30:00Z"). */ const Timestamp = z.string(); -/** Represents a date used in annotation. */ const DateSchema = z.object({ year: z.number().int().optional(), month: z.number().int().optional(), @@ -23,36 +21,27 @@ const DateSchema = z.object({ // ── Admin API — Account Summaries ──────────────────────────────────────────── export const PropertySummarySchema = z.object({ - /** Resource name, e.g. "properties/123456". */ property: z.string(), displayName: z.string(), - /** e.g. "PROPERTY_TYPE_ORDINARY" | "PROPERTY_TYPE_ROLLUP" | "PROPERTY_TYPE_SUBPROPERTY" */ propertyType: z.string().optional(), - /** Parent resource name, e.g. "accounts/123456". */ parent: z.string().optional(), }); export const AccountSummarySchema = z.object({ - /** Resource name, e.g. "accountSummaries/123456". */ name: z.string(), - /** Account resource name, e.g. "accounts/123456". */ account: z.string().optional(), displayName: z.string().optional(), propertySummaries: z.array(PropertySummarySchema).optional(), }); -export const AccountSummariesResponseSchema = z.object({ - response: z.object({ - accountSummaries: z.array(AccountSummarySchema).optional(), - }), +export const AccountSummariesOutputSchema = z.object({ + accountSummaries: z.array(AccountSummarySchema).optional(), }); // ── Admin API — Property ───────────────────────────────────────────────────── export const PropertySchema = z.object({ - /** Resource name, e.g. "properties/123456". */ name: z.string().optional(), - /** Parent resource, e.g. "accounts/123456" or "properties/123" for sub-properties. */ parent: z.string().optional(), createTime: Timestamp.optional(), updateTime: Timestamp.optional(), @@ -60,68 +49,43 @@ export const PropertySchema = z.object({ industryCategory: z.string().optional(), timeZone: z.string().optional(), currencyCode: z.string().optional(), - /** e.g. "GOOGLE_ANALYTICS_STANDARD" | "GOOGLE_ANALYTICS_360" */ serviceLevel: z.string().optional(), - /** e.g. "PROPERTY_TYPE_ORDINARY" | "PROPERTY_TYPE_ROLLUP" | "PROPERTY_TYPE_SUBPROPERTY" */ propertyType: z.string().optional(), account: z.string().optional(), deleteTime: Timestamp.optional(), expireTime: Timestamp.optional(), }); -export const PropertyResponseSchema = z.object({ - response: PropertySchema, -}); - // ── Admin API — Custom Dimensions & Metrics ────────────────────────────────── export const CustomDimensionSchema = z.object({ - /** Resource name, e.g. "properties/123/customDimensions/456". */ name: z.string().optional(), - /** The parameter name used in events / user properties. */ parameterName: z.string(), displayName: z.string().optional(), description: z.string().optional(), - /** "EVENT" | "USER" | "ITEM" */ scope: z.string().optional(), disallowAdsPersonalization: z.boolean().optional(), }); export const CustomMetricSchema = z.object({ - /** Resource name. */ name: z.string().optional(), parameterName: z.string(), displayName: z.string().optional(), description: z.string().optional(), - /** e.g. "STANDARD" | "CURRENCY" | "FEET" | "METERS" | "KILOMETERS" | "MILES" | "MILLISECONDS" | "SECONDS" | "MINUTES" | "HOURS" */ measurementUnit: z.string().optional(), - /** "EVENT" | "ITEM" */ scope: z.string().optional(), - /** e.g. ["COST_DATA", "REVENUE_DATA"] */ restrictedMetricType: z.array(z.string()).optional(), }); -export const CustomDimensionsListSchema = z.object({ +export const CustomDimensionsAndMetricsOutputSchema = z.object({ customDimensions: z.array(CustomDimensionSchema).optional(), -}); - -export const CustomMetricsListSchema = z.object({ customMetrics: z.array(CustomMetricSchema).optional(), }); -export const CustomDimensionsAndMetricsResponseSchema = z.object({ - /** Result of listCustomDimensions — `{ customDimensions: [...] }` */ - dimensions: CustomDimensionsListSchema, - /** Result of listCustomMetrics — `{ customMetrics: [...] }` */ - metrics: CustomMetricsListSchema, -}); - // ── Admin API — Google Ads Links ───────────────────────────────────────────── export const GoogleAdsLinkSchema = z.object({ - /** Resource name. */ name: z.string().optional(), - /** Google Ads Customer ID (without hyphens). */ customerId: z.string().optional(), canManageClients: z.boolean().optional(), adsPersonalizationEnabled: z.boolean().optional(), @@ -130,29 +94,23 @@ export const GoogleAdsLinkSchema = z.object({ updateTime: Timestamp.optional(), }); -export const GoogleAdsLinksResponseSchema = z.object({ - response: z.object({ - googleAdsLinks: z.array(GoogleAdsLinkSchema).optional(), - }), +export const GoogleAdsLinksOutputSchema = z.object({ + googleAdsLinks: z.array(GoogleAdsLinkSchema).optional(), }); // ── Admin API — Property Annotations ───────────────────────────────────────── export const ReportingDataAnnotationSchema = z.object({ - /** Resource name. */ name: z.string().optional(), annotationDate: DateSchema.optional(), title: z.string().optional(), description: z.string().optional(), systemGenerated: z.boolean().optional(), - /** "RED" | "ORANGE" | "YELLOW" | "GREEN" | "BLUE" | "PURPLE" | "PINK" */ color: z.string().optional(), }); -export const PropertyAnnotationsResponseSchema = z.object({ - response: z.object({ - reportingDataAnnotations: z.array(ReportingDataAnnotationSchema).optional(), - }), +export const PropertyAnnotationsOutputSchema = z.object({ + reportingDataAnnotations: z.array(ReportingDataAnnotationSchema).optional(), }); // ── Data API — shared building blocks ──────────────────────────────────────── @@ -163,7 +121,6 @@ export const DimensionHeaderSchema = z.object({ export const MetricHeaderSchema = z.object({ name: z.string(), - /** e.g. "TYPE_INTEGER" | "TYPE_FLOAT" | "TYPE_SECONDS" | "TYPE_MILLISECONDS" | "TYPE_MINUTES" | "TYPE_HOURS" | "TYPE_STANDARD" | "TYPE_CURRENCY" | "TYPE_FEET" | "TYPE_MILES" | "TYPE_METERS" | "TYPE_KILOMETERS" */ type: z.string().optional(), }); @@ -188,7 +145,6 @@ export const ResponseMetaDataSchema = z.object({ samplingMetadatas: z.array(z.unknown()).optional(), }); -/** Quota state returned when `returnPropertyQuota: true`. */ export const PropertyQuotaSchema = z .object({ tokensPerDay: z @@ -214,7 +170,7 @@ export const PropertyQuotaSchema = z // ── Data API — runReport ────────────────────────────────────────────────────── -export const RunReportResponseSchema = z.object({ +export const RunReportOutputSchema = z.object({ dimensionHeaders: z.array(DimensionHeaderSchema).optional(), metricHeaders: z.array(MetricHeaderSchema).optional(), rows: z.array(RowSchema).optional(), @@ -224,17 +180,12 @@ export const RunReportResponseSchema = z.object({ rowCount: z.number().optional(), metadata: ResponseMetaDataSchema.optional(), propertyQuota: PropertyQuotaSchema.optional(), - /** Always "analyticsData#runReport". */ kind: z.string().optional(), }); -export const RunReportOutputSchema = z.object({ - response: RunReportResponseSchema, -}); - // ── Data API — runRealtimeReport ───────────────────────────────────────────── -export const RunRealtimeReportResponseSchema = z.object({ +export const RunRealtimeReportOutputSchema = z.object({ dimensionHeaders: z.array(DimensionHeaderSchema).optional(), metricHeaders: z.array(MetricHeaderSchema).optional(), rows: z.array(RowSchema).optional(), @@ -243,26 +194,17 @@ export const RunRealtimeReportResponseSchema = z.object({ minimums: z.array(RowSchema).optional(), rowCount: z.number().optional(), propertyQuota: PropertyQuotaSchema.optional(), - /** Always "analyticsData#runRealtimeReport". */ kind: z.string().optional(), }); -export const RunRealtimeReportOutputSchema = z.object({ - response: RunRealtimeReportResponseSchema, -}); - // ── Data API — runFunnelReport ──────────────────────────────────────────────── -export const FunnelResponseMetaDataSchema = z.object({ +const FunnelResponseMetaDataSchema = z.object({ samplingMetadatas: z.array(z.unknown()).optional(), schemaRestrictionResponse: z.unknown().optional(), }); -/** - * Funnel response rows share the same Row structure (dimensionValues + - * metricValues), grouped in funnelTable and funnelVisualization sub-objects. - */ -export const FunnelSubReportSchema = z.object({ +const FunnelSubReportSchema = z.object({ dimensionHeaders: z.array(DimensionHeaderSchema).optional(), metricHeaders: z.array(MetricHeaderSchema).optional(), rows: z.array(RowSchema).optional(), @@ -273,14 +215,9 @@ export const FunnelSubReportSchema = z.object({ metadata: FunnelResponseMetaDataSchema.optional(), }); -export const RunFunnelReportResponseSchema = z.object({ +export const RunFunnelReportOutputSchema = z.object({ funnelTable: FunnelSubReportSchema.optional(), funnelVisualization: FunnelSubReportSchema.optional(), propertyQuota: PropertyQuotaSchema.optional(), - /** Always "analyticsData#runFunnelReport". */ kind: z.string().optional(), }); - -export const RunFunnelReportOutputSchema = z.object({ - response: RunFunnelReportResponseSchema, -}); diff --git a/google-analytics/server/tools/accounts.ts b/google-analytics/server/tools/accounts.ts index 7b0650b3..c283fed1 100644 --- a/google-analytics/server/tools/accounts.ts +++ b/google-analytics/server/tools/accounts.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { createPrivateTool } from "@decocms/runtime/tools"; import type { Env } from "../../shared/deco.gen.ts"; import { GaClient } from "../lib/ga-client.ts"; -import { AccountSummariesResponseSchema } from "../lib/schemas.ts"; +import { AccountSummariesOutputSchema } from "../lib/schemas.ts"; export const getAccountSummariesTool = (env: Env) => createPrivateTool({ @@ -10,13 +10,12 @@ export const getAccountSummariesTool = (env: Env) => description: "Retrieves information about the user's Google Analytics accounts and properties.", inputSchema: z.object({}), - outputSchema: AccountSummariesResponseSchema, + outputSchema: AccountSummariesOutputSchema, execute: async () => { const client = GaClient.fromEnv(env); - try { const result = await client.listAccountSummaries(); - return AccountSummariesResponseSchema.parse({ response: result }); + return AccountSummariesOutputSchema.parse(result); } catch (error) { throw new Error( `Failed to retrieve account summaries: ${error instanceof Error ? error.message : String(error)}`, diff --git a/google-analytics/server/tools/ads.ts b/google-analytics/server/tools/ads.ts index bc10cff3..90b17ece 100644 --- a/google-analytics/server/tools/ads.ts +++ b/google-analytics/server/tools/ads.ts @@ -2,12 +2,10 @@ import { z } from "zod"; import { createPrivateTool } from "@decocms/runtime/tools"; import type { Env } from "../../shared/deco.gen.ts"; import { GaClient } from "../lib/ga-client.ts"; -import { GoogleAdsLinksResponseSchema } from "../lib/schemas.ts"; +import { GoogleAdsLinksOutputSchema } from "../lib/schemas.ts"; const propertySchema = z - .string() - .describe( "GA4 Property identifier — 'properties/1234567' or just '1234567'.", ); @@ -18,12 +16,12 @@ export const listGoogleAdsLinksTool = (env: Env) => description: "Returns a list of links to Google Ads accounts for a GA4 property.", inputSchema: z.object({ property: propertySchema }), - outputSchema: GoogleAdsLinksResponseSchema, + outputSchema: GoogleAdsLinksOutputSchema, execute: async ({ context: args }) => { const client = GaClient.fromEnv(env); try { const result = await client.listGoogleAdsLinks(args.property); - return GoogleAdsLinksResponseSchema.parse({ response: result }); + return GoogleAdsLinksOutputSchema.parse(result); } catch (error) { throw new Error( `Failed to retrieve Google Ads links: ${error instanceof Error ? error.message : String(error)}`, diff --git a/google-analytics/server/tools/annotations.ts b/google-analytics/server/tools/annotations.ts index 81bcce5d..b60b1892 100644 --- a/google-analytics/server/tools/annotations.ts +++ b/google-analytics/server/tools/annotations.ts @@ -2,12 +2,10 @@ import { z } from "zod"; import { createPrivateTool } from "@decocms/runtime/tools"; import type { Env } from "../../shared/deco.gen.ts"; import { GaClient } from "../lib/ga-client.ts"; -import { PropertyAnnotationsResponseSchema } from "../lib/schemas.ts"; +import { PropertyAnnotationsOutputSchema } from "../lib/schemas.ts"; const propertySchema = z - .string() - .describe( "GA4 Property identifier — 'properties/1234567' or just '1234567'.", ); @@ -18,12 +16,12 @@ export const listPropertyAnnotationsTool = (env: Env) => description: "Returns timestamped annotations for a GA4 property — useful for correlating traffic changes with events like campaign launches, site releases, or data collection changes.", inputSchema: z.object({ property: propertySchema }), - outputSchema: PropertyAnnotationsResponseSchema, + outputSchema: PropertyAnnotationsOutputSchema, execute: async ({ context: args }) => { const client = GaClient.fromEnv(env); try { const result = await client.listPropertyAnnotations(args.property); - return PropertyAnnotationsResponseSchema.parse({ response: result }); + return PropertyAnnotationsOutputSchema.parse(result); } catch (error) { throw new Error( `Failed to retrieve property annotations: ${error instanceof Error ? error.message : String(error)}`, diff --git a/google-analytics/server/tools/properties.ts b/google-analytics/server/tools/properties.ts index 8859f14c..e021e1f1 100644 --- a/google-analytics/server/tools/properties.ts +++ b/google-analytics/server/tools/properties.ts @@ -3,14 +3,12 @@ import { createPrivateTool } from "@decocms/runtime/tools"; import type { Env } from "../../shared/deco.gen.ts"; import { GaClient } from "../lib/ga-client.ts"; import { - PropertyResponseSchema, - CustomDimensionsAndMetricsResponseSchema, + PropertySchema, + CustomDimensionsAndMetricsOutputSchema, } from "../lib/schemas.ts"; const propertySchema = z - .string() - .describe( "GA4 Property identifier — 'properties/1234567' or just '1234567'.", ); @@ -21,12 +19,12 @@ export const getPropertyDetailsTool = (env: Env) => description: "Returns metadata and configuration details about a GA4 property.", inputSchema: z.object({ property: propertySchema }), - outputSchema: PropertyResponseSchema, + outputSchema: PropertySchema, execute: async ({ context: args }) => { const client = GaClient.fromEnv(env); try { const result = await client.getProperty(args.property); - return PropertyResponseSchema.parse({ response: result }); + return PropertySchema.parse(result); } catch (error) { throw new Error( `Failed to retrieve property details: ${error instanceof Error ? error.message : String(error)}`, @@ -41,19 +39,17 @@ export const getCustomDimensionsAndMetricsTool = (env: Env) => description: "Retrieves the custom dimensions and custom metrics configured for a GA4 property.", inputSchema: z.object({ property: propertySchema }), - outputSchema: CustomDimensionsAndMetricsResponseSchema, + outputSchema: CustomDimensionsAndMetricsOutputSchema, execute: async ({ context: args }) => { const client = GaClient.fromEnv(env); try { - // Both calls are independent — run in parallel to halve latency. - const [dimensions, metrics] = await Promise.all([ + const [dimensionsResult, metricsResult] = await Promise.all([ client.listCustomDimensions(args.property), client.listCustomMetrics(args.property), ]); - - return CustomDimensionsAndMetricsResponseSchema.parse({ - dimensions, - metrics, + return CustomDimensionsAndMetricsOutputSchema.parse({ + ...(dimensionsResult as object), + ...(metricsResult as object), }); } catch (error) { throw new Error( diff --git a/google-analytics/server/tools/reports.ts b/google-analytics/server/tools/reports.ts index d31cbb0e..5c98b135 100644 --- a/google-analytics/server/tools/reports.ts +++ b/google-analytics/server/tools/reports.ts @@ -8,18 +8,28 @@ import { RunRealtimeReportOutputSchema, } from "../lib/schemas.ts"; +// Some MCP clients serialize arrays/objects as JSON strings instead of native +// types. This preprocessor parses the string before Zod validates it. +const fromJson = (schema: T) => + z.preprocess((val) => { + if (val === null) return undefined; + if (typeof val !== "string") return val; + try { + const parsed = JSON.parse(val); + return parsed === null ? undefined : parsed; + } catch { + return val; + } + }, schema); + const DateRangeSchema = z.object({ startDate: z - .string() - .describe( "Inclusive start date in YYYY-MM-DD format, or relative values: 'today', 'yesterday', 'NdaysAgo'.", ), endDate: z - .string() - .describe( "Inclusive end date in YYYY-MM-DD format, or relative values: 'today', 'yesterday'.", ), @@ -30,17 +40,13 @@ const MetricSchema = z.object({ name: z.string() }); // FilterExpression and OrderBy are passed as raw JSON objects matching the GA4 REST API schema. const FilterExpressionSchema = z - .record(z.string(), z.unknown()) - .describe( "GA4 FilterExpression object. See https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/FilterExpression", ); const OrderBySchema = z - .record(z.string(), z.unknown()) - .describe( "GA4 OrderBy object. See https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/OrderBy", ); @@ -52,89 +58,48 @@ export const runReportTool = (env: Env) => "Runs a Google Analytics 4 report using the Data API. Returns dimensions, metrics, and row data for the specified property and date range.", inputSchema: z.object({ property: z - .string() - .describe( "GA4 Property identifier — 'properties/1234567' or just '1234567'.", ), - dateRanges: z - - .array(DateRangeSchema) - - .min(1) - - .describe("One or more date ranges to include in the report."), - dimensions: z - - .array(DimensionSchema) - + dateRanges: fromJson(z.array(DateRangeSchema).min(1)).describe( + "One or more date ranges to include in the report.", + ), + dimensions: fromJson(z.array(DimensionSchema)) .optional() - .describe( "Dimensions to group results by, e.g. [{ name: 'sessionSource' }].", ), - metrics: z - - .array(MetricSchema) - - .min(1) - - .describe("Metrics to aggregate, e.g. [{ name: 'activeUsers' }]."), - dimensionFilter: FilterExpressionSchema.optional().describe( - "Optional filter to restrict dimension values.", - ), - metricFilter: FilterExpressionSchema.optional().describe( - "Optional filter to restrict metric values.", + metrics: fromJson(z.array(MetricSchema).min(1)).describe( + "Metrics to aggregate, e.g. [{ name: 'activeUsers' }].", ), - orderBys: z - - .array(OrderBySchema) - + dimensionFilter: fromJson(FilterExpressionSchema) + .optional() + .describe("Optional filter to restrict dimension values."), + metricFilter: fromJson(FilterExpressionSchema) + .optional() + .describe("Optional filter to restrict metric values."), + orderBys: fromJson(z.array(OrderBySchema)) .optional() - .describe("Optional ordering for returned rows."), - limit: z - - .number() - - .int() - - .positive() - + limit: fromJson(z.number().int().positive()) .optional() - .describe( "Maximum number of rows to return. Defaults to 10,000; max 250,000.", ), - offset: z - - .number() - - .int() - - .nonnegative() - + offset: fromJson(z.number().int().nonnegative()) .optional() - .describe( "Row offset for pagination (0-based). Use with limit to page through results.", ), currencyCode: z - .string() - .optional() - .describe( "Optional ISO 4217 currency code for revenue metrics, e.g. 'USD'.", ), - returnPropertyQuota: z - - .boolean() - + returnPropertyQuota: fromJson(z.boolean()) .optional() - .describe( "If true, includes the current GA4 property quota state in the response.", ), @@ -157,12 +122,10 @@ export const runReportTool = (env: Env) => if (args.offset !== undefined) body.offset = args.offset; if (args.currencyCode !== undefined) body.currencyCode = args.currencyCode; - if (args.returnPropertyQuota !== undefined) { + if (args.returnPropertyQuota !== undefined) body.returnPropertyQuota = args.returnPropertyQuota; - } const response = await client.runReport(args.property, body); - - return RunReportOutputSchema.parse({ response }); + return RunReportOutputSchema.parse(response); } catch (error) { throw new Error( `Failed to run report: ${error instanceof Error ? error.message : String(error)}`, @@ -173,32 +136,19 @@ export const runReportTool = (env: Env) => const FunnelStepSchema = z.object({ name: z - .string() - .describe("Display name for this funnel step (shown in reports)."), - filterExpression: z - - .record(z.string(), z.unknown()) - - .describe( - "Required. Condition that qualifies users for this step. See https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/FunnelStep#FunnelFilterExpression", - ), - isDirectlyFollowedBy: z - - .boolean() - + filterExpression: fromJson(z.record(z.string(), z.unknown())).describe( + "Required. Condition that qualifies users for this step. See https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/FunnelStep#FunnelFilterExpression", + ), + isDirectlyFollowedBy: fromJson(z.boolean()) .optional() - .describe( "If true, this step must immediately follow the previous step (no intervening events). Defaults to false.", ), withinDurationFromPriorStep: z - .string() - .optional() - .describe( "Time window within which this step must occur after the prior step, e.g. '3600s' for 1 hour.", ), @@ -211,74 +161,34 @@ export const runFunnelReportTool = (env: Env) => "Runs a Google Analytics 4 funnel report. Analyzes how users progress through a sequence of steps (e.g. landing page → product page → checkout → purchase). Returns per-step completion counts and drop-off rates.", inputSchema: z.object({ property: z - .string() - .describe( "GA4 Property identifier — 'properties/1234567' or just '1234567'.", ), - funnelSteps: z - - .array(FunnelStepSchema) - - .min(2) - - .describe( - "Ordered list of funnel steps. Each step is a GA4 FunnelStep object with a name and filterExpression.", - ), - dateRanges: z - - .array(DateRangeSchema) - - .min(1) - - .describe("One or more date ranges to include in the report."), - funnelBreakdown: z - - .record(z.string(), z.unknown()) - + funnelSteps: fromJson(z.array(FunnelStepSchema).min(2)).describe( + "Ordered list of funnel steps. Each step is a GA4 FunnelStep object with a name and filterExpression.", + ), + dateRanges: fromJson(z.array(DateRangeSchema).min(1)).describe( + "One or more date ranges to include in the report.", + ), + funnelBreakdown: fromJson(z.record(z.string(), z.unknown())) .optional() - .describe( "Optional FunnelBreakdown — adds a sub-dimension to the funnel table rows.", ), - funnelNextAction: z - - .record(z.string(), z.unknown()) - + funnelNextAction: fromJson(z.record(z.string(), z.unknown())) .optional() - .describe( "Optional FunnelNextAction — adds a next-action dimension showing what users do after abandoning a step.", ), - limit: z - - .number() - - .int() - - .positive() - + limit: fromJson(z.number().int().positive()) .optional() - .describe("Maximum number of rows to return."), - offset: z - - .number() - - .int() - - .nonnegative() - + offset: fromJson(z.number().int().nonnegative()) .optional() - .describe("Row offset for pagination."), - returnPropertyQuota: z - - .boolean() - + returnPropertyQuota: fromJson(z.boolean()) .optional() - .describe("If true, includes current GA4 property quota in response."), }), outputSchema: RunFunnelReportOutputSchema, @@ -298,8 +208,7 @@ export const runFunnelReportTool = (env: Env) => if (args.returnPropertyQuota !== undefined) body.returnPropertyQuota = args.returnPropertyQuota; const response = await client.runFunnelReport(args.property, body); - - return RunFunnelReportOutputSchema.parse({ response }); + return RunFunnelReportOutputSchema.parse(response); } catch (error) { throw new Error( `Failed to run funnel report: ${error instanceof Error ? error.message : String(error)}`, @@ -315,61 +224,33 @@ export const runRealtimeReportTool = (env: Env) => "Runs a Google Analytics 4 realtime report. Returns live data from the last 30 minutes.", inputSchema: z.object({ property: z - .string() - .describe( "GA4 Property identifier — 'properties/1234567' or just '1234567'.", ), - dimensions: z - - .array(DimensionSchema) - + dimensions: fromJson(z.array(DimensionSchema)) .optional() - .describe("Dimensions to group results by."), - metrics: z.array(MetricSchema).min(1).describe("Metrics to aggregate."), - dimensionFilter: FilterExpressionSchema.optional().describe( - "Optional filter to restrict dimension values.", + metrics: fromJson(z.array(MetricSchema).min(1)).describe( + "Metrics to aggregate.", ), - metricFilter: FilterExpressionSchema.optional().describe( - "Optional filter to restrict metric values.", - ), - orderBys: z - - .array(OrderBySchema) - + dimensionFilter: fromJson(FilterExpressionSchema) + .optional() + .describe("Optional filter to restrict dimension values."), + metricFilter: fromJson(FilterExpressionSchema) + .optional() + .describe("Optional filter to restrict metric values."), + orderBys: fromJson(z.array(OrderBySchema)) .optional() - .describe("Optional ordering for returned rows."), - limit: z - - .number() - - .int() - - .positive() - + limit: fromJson(z.number().int().positive()) .optional() - .describe("Maximum number of rows to return."), - offset: z - - .number() - - .int() - - .nonnegative() - + offset: fromJson(z.number().int().nonnegative()) .optional() - .describe("Row offset for pagination."), - returnPropertyQuota: z - - .boolean() - + returnPropertyQuota: fromJson(z.boolean()) .optional() - .describe("If true, includes quota state in the response."), }), outputSchema: RunRealtimeReportOutputSchema, @@ -387,12 +268,10 @@ export const runRealtimeReportTool = (env: Env) => if (args.orderBys !== undefined) body.orderBys = args.orderBys; if (args.limit !== undefined) body.limit = args.limit; if (args.offset !== undefined) body.offset = args.offset; - if (args.returnPropertyQuota !== undefined) { + if (args.returnPropertyQuota !== undefined) body.returnPropertyQuota = args.returnPropertyQuota; - } const response = await client.runRealtimeReport(args.property, body); - - return RunRealtimeReportOutputSchema.parse({ response }); + return RunRealtimeReportOutputSchema.parse(response); } catch (error) { throw new Error( `Failed to run realtime report: ${error instanceof Error ? error.message : String(error)}`,