Skip to content

Commit 3afb237

Browse files
committed
feat: add batch_run_reports tool
Add a new `batch_run_reports` tool that wraps the Data API v1beta `batchRunReports` endpoint to run up to 5 reports in a single API call. Each report in the batch accepts the same parameters as the existing `run_report` tool (dimensions, metrics, date_ranges, filters, etc.), making it easy to upgrade from sequential `run_report` calls.
1 parent 6818a99 commit 3afb237

3 files changed

Lines changed: 591 additions & 0 deletions

File tree

analytics_mcp/coordinator.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
run_conversions_report,
5555
_run_conversions_report_description,
5656
)
57+
from analytics_mcp.tools.reporting.batch import (
58+
batch_run_reports,
59+
_batch_run_reports_description,
60+
)
5761

5862
run_report_with_description = FunctionTool(run_report)
5963
run_report_with_description.description = _run_report_description()
@@ -69,6 +73,10 @@
6973
run_conversions_report_with_description.description = (
7074
_run_conversions_report_description()
7175
)
76+
batch_run_reports_with_description = FunctionTool(batch_run_reports)
77+
batch_run_reports_with_description.description = (
78+
_batch_run_reports_description()
79+
)
7280

7381
# Instantiate the ADK tools
7482
tools = [
@@ -81,6 +89,7 @@
8189
run_realtime_report_with_description,
8290
run_funnel_report_with_description,
8391
run_conversions_report_with_description,
92+
batch_run_reports_with_description,
8493
]
8594

8695
tool_map = {t.name: t for t in tools}
@@ -150,6 +159,11 @@ def sanitize_mcp_schema_properties(node: dict) -> None:
150159
"metrics",
151160
"conversion_spec",
152161
]
162+
elif tool.name == "batch_run_reports":
163+
tool.inputSchema["required"] = [
164+
"property_id",
165+
"requests",
166+
]
153167

154168

155169
@app.list_tools()
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# Copyright 2025 Google LLC All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Tools for running batch reports using the Data API."""
16+
17+
import asyncio
18+
from typing import Any, Dict, List
19+
20+
from analytics_mcp.tools.reporting.metadata import (
21+
get_date_ranges_hints,
22+
get_dimension_filter_hints,
23+
get_metric_filter_hints,
24+
get_order_bys_hints,
25+
)
26+
from analytics_mcp.tools.utils import (
27+
construct_property_rn,
28+
proto_to_dict,
29+
)
30+
from analytics_mcp.tools.client import create_data_api_client
31+
from google.analytics import data_v1beta
32+
33+
34+
def _batch_run_reports_description() -> str:
35+
"""Returns the description for the `batch_run_reports` tool."""
36+
return f"""
37+
{batch_run_reports.__doc__}
38+
39+
## Hints for arguments
40+
41+
Here are some hints that outline the expected format and
42+
requirements for arguments. Each object in the `requests`
43+
list uses the same argument formats as the `run_report` tool.
44+
45+
### Hints for `dimensions`
46+
47+
The `dimensions` list must consist solely of either of the
48+
following:
49+
50+
1. Standard dimensions defined in the HTML table at
51+
https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema#dimensions.
52+
These dimensions are available to *every* property.
53+
2. Custom dimensions for the `property_id`. Use the
54+
`get_custom_dimensions_and_metrics` tool to retrieve the
55+
list of custom dimensions for a property.
56+
57+
### Hints for `metrics`
58+
59+
The `metrics` list must consist solely of either of the
60+
following:
61+
62+
1. Standard metrics defined in the HTML table at
63+
https://developers.google.com/analytics/devguides/reporting/data/v1/api-schema#metrics.
64+
These metrics are available to *every* property.
65+
2. Custom metrics for the `property_id`. Use the
66+
`get_custom_dimensions_and_metrics` tool to retrieve the
67+
list of custom metrics for a property.
68+
69+
70+
### Hints for `date_ranges`:
71+
{get_date_ranges_hints()}
72+
73+
### Hints for `dimension_filter`:
74+
{get_dimension_filter_hints()}
75+
76+
### Hints for `metric_filter`:
77+
{get_metric_filter_hints()}
78+
79+
### Hints for `order_bys`:
80+
{get_order_bys_hints()}
81+
82+
"""
83+
84+
85+
def _build_report_request(
86+
property_rn: str, report: Dict[str, Any]
87+
) -> data_v1beta.RunReportRequest:
88+
"""Builds a RunReportRequest proto from a report specification dict.
89+
90+
Args:
91+
property_rn: The property resource name (e.g. "properties/12345").
92+
report: A dict with keys matching the `run_report` tool's
93+
parameters: `dimensions`, `metrics`, `date_ranges`, and
94+
optionally `dimension_filter`, `metric_filter`, `order_bys`,
95+
`limit`, `offset`, `currency_code`, `return_property_quota`.
96+
97+
Returns:
98+
A RunReportRequest proto.
99+
"""
100+
request = data_v1beta.RunReportRequest(
101+
property=property_rn,
102+
dimensions=[
103+
data_v1beta.Dimension(name=d) for d in report["dimensions"]
104+
],
105+
metrics=[data_v1beta.Metric(name=m) for m in report["metrics"]],
106+
date_ranges=[data_v1beta.DateRange(dr) for dr in report["date_ranges"]],
107+
return_property_quota=report.get("return_property_quota", False),
108+
)
109+
110+
dimension_filter = report.get("dimension_filter")
111+
if dimension_filter:
112+
request.dimension_filter = data_v1beta.FilterExpression(
113+
dimension_filter
114+
)
115+
116+
metric_filter = report.get("metric_filter")
117+
if metric_filter:
118+
request.metric_filter = data_v1beta.FilterExpression(metric_filter)
119+
120+
order_bys = report.get("order_bys")
121+
if order_bys:
122+
request.order_bys = [data_v1beta.OrderBy(ob) for ob in order_bys]
123+
124+
limit = report.get("limit")
125+
if limit:
126+
request.limit = limit
127+
128+
offset = report.get("offset")
129+
if offset:
130+
request.offset = offset
131+
132+
currency_code = report.get("currency_code")
133+
if currency_code:
134+
request.currency_code = currency_code
135+
136+
return request
137+
138+
139+
async def batch_run_reports(
140+
property_id: int | str,
141+
requests: List[Dict[str, Any]],
142+
) -> Dict[str, Any]:
143+
"""Runs multiple Google Analytics Data API reports in a single request.
144+
145+
Use this tool instead of calling `run_report` multiple times when you
146+
need data from several reports for the same property. This reduces
147+
latency by combining up to 5 reports into one API call.
148+
149+
Each object in the `requests` list accepts the same arguments as the
150+
`run_report` tool.
151+
152+
Note that the reference docs at
153+
https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta
154+
all use camelCase field names, but field names passed to this method
155+
should be in snake_case since the tool is using the protocol buffers
156+
(protobuf) format. The protocol buffers for the Data API are available
157+
at
158+
https://github.com/googleapis/googleapis/tree/master/google/analytics/data/v1beta.
159+
160+
Args:
161+
property_id: The Google Analytics property ID. Accepted formats
162+
are:
163+
- A number
164+
- A string consisting of 'properties/' followed by a number
165+
requests: A list of 1 to 5 report request objects. Each object
166+
must contain the following required keys:
167+
- `dimensions`: A list of dimensions to include in the report.
168+
- `metrics`: A list of metrics to include in the report.
169+
- `date_ranges`: A list of date ranges
170+
(https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/DateRange)
171+
to include in the report.
172+
173+
Each object may also contain the following optional keys:
174+
- `dimension_filter`: A Data API FilterExpression
175+
(https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/FilterExpression)
176+
to apply to the dimensions.
177+
- `metric_filter`: A Data API FilterExpression to apply to the
178+
metrics.
179+
- `order_bys`: A list of Data API OrderBy
180+
(https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/OrderBy)
181+
objects.
182+
- `limit`: The maximum number of rows to return (max 250,000).
183+
- `offset`: The row count of the start row (0-indexed).
184+
- `currency_code`: An ISO4217 currency code (e.g. "USD").
185+
- `return_property_quota`: Whether to return property quota
186+
information in the response (default: false).
187+
"""
188+
if not isinstance(requests, list):
189+
raise ValueError("requests must be a list.")
190+
if not requests:
191+
raise ValueError("requests must contain at least one report request.")
192+
if len(requests) > 5:
193+
raise ValueError(
194+
"requests must contain at most 5 report requests. "
195+
f"Got {len(requests)}."
196+
)
197+
198+
for i, report in enumerate(requests):
199+
if not isinstance(report, dict):
200+
raise ValueError(f"Request {i + 1} must be a dictionary.")
201+
for key in ("dimensions", "metrics", "date_ranges"):
202+
if key not in report:
203+
raise ValueError(
204+
f"Request {i + 1} is missing required key " f"'{key}'."
205+
)
206+
if not isinstance(report[key], list):
207+
raise ValueError(f"Request {i + 1} '{key}' must be a list.")
208+
209+
property_rn = construct_property_rn(property_id)
210+
211+
batch_request = data_v1beta.BatchRunReportsRequest(
212+
property=property_rn,
213+
requests=[_build_report_request(property_rn, r) for r in requests],
214+
)
215+
216+
def _sync_call():
217+
return create_data_api_client().batch_run_reports(batch_request)
218+
219+
response = await asyncio.to_thread(_sync_call)
220+
221+
return proto_to_dict(response)

0 commit comments

Comments
 (0)