adloop 0.13.2__tar.gz → 0.13.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {adloop-0.13.2 → adloop-0.13.3}/PKG-INFO +7 -5
- {adloop-0.13.2 → adloop-0.13.3}/README.md +6 -4
- {adloop-0.13.2 → adloop-0.13.3}/pyproject.toml +1 -1
- {adloop-0.13.2 → adloop-0.13.3}/pyproject.toml.orig +1 -1
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ads/read.py +46 -4
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/config.py +32 -12
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/rules/adloop.md +23 -10
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/server.py +103 -67
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/__init__.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/__main__.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/_mcp_patches.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ads/__init__.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ads/client.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ads/currency.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ads/enums.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ads/forecast.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ads/gaql.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ads/pmax.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ads/write.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/auth.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/cli.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/crossref.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/diagnostics.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ga4/__init__.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ga4/client.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ga4/reports.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ga4/tracking.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/ga4/write.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/gsc/__init__.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/gsc/client.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/gsc/reports.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/gtm/__init__.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/gtm/client.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/gtm/read.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/merchant/__init__.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/merchant/client.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/merchant/read.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/pagespeed.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/rules/__init__.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/rules/commands/analyze-performance.md +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/rules/commands/budget-plan.md +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/rules/commands/create-ad.md +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/rules/commands/create-campaign.md +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/rules/commands/optimize-campaign.md +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/rules_install.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/runtime.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/safety/__init__.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/safety/audit.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/safety/guards.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/safety/preview.py +0 -0
- {adloop-0.13.2 → adloop-0.13.3}/src/adloop/tracking.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: adloop
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.3
|
|
4
4
|
Summary: The AI command center for Google Ads, GA4, and tracking code.
|
|
5
5
|
Keywords: mcp,google-ads,google-analytics,ga4,cursor,marketing
|
|
6
6
|
Author: Daniel Klose
|
|
@@ -24,6 +24,8 @@ Project-URL: Changelog, https://github.com/kLOsk/adloop/releases
|
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Description-Content-Type: text/markdown
|
|
26
26
|
|
|
27
|
+
<!-- mcp-name: com.getadloop/adloop -->
|
|
28
|
+
|
|
27
29
|
<div align="center">
|
|
28
30
|
|
|
29
31
|
# AdLoop
|
|
@@ -40,12 +42,12 @@ Description-Content-Type: text/markdown
|
|
|
40
42
|
|
|
41
43
|
An MCP server that gives your AI assistant read + write access to Google Ads and GA4 — with safety guardrails that prevent accidental spend.
|
|
42
44
|
|
|
43
|
-
**[☁️ Skip the setup — use AdLoop Cloud (free
|
|
45
|
+
**[☁️ Skip the setup — use AdLoop Cloud (free plan, no card)](https://getadloop.com)** · or self-host: `pip install adloop`
|
|
44
46
|
|
|
45
47
|
</div>
|
|
46
48
|
|
|
47
49
|
> [!TIP]
|
|
48
|
-
> **[AdLoop Cloud](https://getadloop.com) is the hosted version of this project
|
|
50
|
+
> **[AdLoop Cloud](https://getadloop.com) is the hosted version of this project, with a free plan that needs no credit card.** Connect Google in two clicks and use the full toolset from claude.ai, ChatGPT, Claude Code, Cursor, or Gemini. No Google Cloud project, no developer token, no OAuth verification wait. EU-hosted, GDPR-first, DPA included.
|
|
49
51
|
|
|
50
52
|
> 📚 **Documentation: [docs.getadloop.com](https://docs.getadloop.com)** — setup guides per AI client, toolsets, the safety model, and troubleshooting for both editions.
|
|
51
53
|
|
|
@@ -63,7 +65,7 @@ Both versions run the same tools with the same safety model. The difference is w
|
|
|
63
65
|
| **Works with** | claude.ai, ChatGPT, Claude Code, Cursor, Gemini | Claude Code, Cursor, Claude Desktop, any local MCP client |
|
|
64
66
|
| **Where your data flows** | EU servers (Germany), GDPR-first, DPA included | 100% your machine — nothing leaves it |
|
|
65
67
|
| **Updates** | Automatic | `pip install -U adloop` |
|
|
66
|
-
| **Price** | Free
|
|
68
|
+
| **Price** | Free plan, no card; [paid plans](https://getadloop.com/preise) for more accounts and volume | Free forever (MIT) |
|
|
67
69
|
|
|
68
70
|
**Not sure? [Start with Cloud](https://getadloop.com)** — it's the fastest way to see what AdLoop can do, and it's the only way to use AdLoop from claude.ai or ChatGPT. Self-host when you want everything on your own machine or need to modify the code. And if you're here to hack on AdLoop itself: welcome, keep scrolling.
|
|
69
71
|
|
|
@@ -517,7 +519,7 @@ What's been shipped and what's next:
|
|
|
517
519
|
- ~~Claude Code support~~ ✓ — `CLAUDE.md`, `.mcp.json`, `.claude/rules/`, `.claude/commands/`, CLI wizard snippets
|
|
518
520
|
- **Claude Desktop one-click install** — `adloop install claude-desktop` (and/or a `.dxt` extension bundle) that writes the AdLoop MCP entry into `claude_desktop_config.json` automatically, so Claude Desktop + Cowork users don't have to hand-edit JSON
|
|
519
521
|
- ~~PyPI package~~ ✓ — `pip install adloop`
|
|
520
|
-
- ~~[AdLoop Cloud](https://getadloop.com)~~ ✓ — the hosted version
|
|
522
|
+
- ~~[AdLoop Cloud](https://getadloop.com)~~ ✓ — the hosted version: no Google Cloud project, no developer token, connect Google in two clicks (EU-hosted, GDPR-first)
|
|
521
523
|
- ~~Headless server support~~ ✓ — manual URL copy-paste flow for servers without a browser
|
|
522
524
|
- ~~Behavioral eval suites~~ ✓ — 28 prompt-and-expectation tests covering read, write, tracking, and planning workflows
|
|
523
525
|
- ~~Google Tag Manager integration~~ ✓ — read tools for tags, triggers, variables, workspaces, and version history, plus the `audit_event_coverage` three-way join across codebase events, GTM tags, and GA4 actual fires
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<!-- mcp-name: com.getadloop/adloop -->
|
|
2
|
+
|
|
1
3
|
<div align="center">
|
|
2
4
|
|
|
3
5
|
# AdLoop
|
|
@@ -14,12 +16,12 @@
|
|
|
14
16
|
|
|
15
17
|
An MCP server that gives your AI assistant read + write access to Google Ads and GA4 — with safety guardrails that prevent accidental spend.
|
|
16
18
|
|
|
17
|
-
**[☁️ Skip the setup — use AdLoop Cloud (free
|
|
19
|
+
**[☁️ Skip the setup — use AdLoop Cloud (free plan, no card)](https://getadloop.com)** · or self-host: `pip install adloop`
|
|
18
20
|
|
|
19
21
|
</div>
|
|
20
22
|
|
|
21
23
|
> [!TIP]
|
|
22
|
-
> **[AdLoop Cloud](https://getadloop.com) is the hosted version of this project
|
|
24
|
+
> **[AdLoop Cloud](https://getadloop.com) is the hosted version of this project, with a free plan that needs no credit card.** Connect Google in two clicks and use the full toolset from claude.ai, ChatGPT, Claude Code, Cursor, or Gemini. No Google Cloud project, no developer token, no OAuth verification wait. EU-hosted, GDPR-first, DPA included.
|
|
23
25
|
|
|
24
26
|
> 📚 **Documentation: [docs.getadloop.com](https://docs.getadloop.com)** — setup guides per AI client, toolsets, the safety model, and troubleshooting for both editions.
|
|
25
27
|
|
|
@@ -37,7 +39,7 @@ Both versions run the same tools with the same safety model. The difference is w
|
|
|
37
39
|
| **Works with** | claude.ai, ChatGPT, Claude Code, Cursor, Gemini | Claude Code, Cursor, Claude Desktop, any local MCP client |
|
|
38
40
|
| **Where your data flows** | EU servers (Germany), GDPR-first, DPA included | 100% your machine — nothing leaves it |
|
|
39
41
|
| **Updates** | Automatic | `pip install -U adloop` |
|
|
40
|
-
| **Price** | Free
|
|
42
|
+
| **Price** | Free plan, no card; [paid plans](https://getadloop.com/preise) for more accounts and volume | Free forever (MIT) |
|
|
41
43
|
|
|
42
44
|
**Not sure? [Start with Cloud](https://getadloop.com)** — it's the fastest way to see what AdLoop can do, and it's the only way to use AdLoop from claude.ai or ChatGPT. Self-host when you want everything on your own machine or need to modify the code. And if you're here to hack on AdLoop itself: welcome, keep scrolling.
|
|
43
45
|
|
|
@@ -491,7 +493,7 @@ What's been shipped and what's next:
|
|
|
491
493
|
- ~~Claude Code support~~ ✓ — `CLAUDE.md`, `.mcp.json`, `.claude/rules/`, `.claude/commands/`, CLI wizard snippets
|
|
492
494
|
- **Claude Desktop one-click install** — `adloop install claude-desktop` (and/or a `.dxt` extension bundle) that writes the AdLoop MCP entry into `claude_desktop_config.json` automatically, so Claude Desktop + Cowork users don't have to hand-edit JSON
|
|
493
495
|
- ~~PyPI package~~ ✓ — `pip install adloop`
|
|
494
|
-
- ~~[AdLoop Cloud](https://getadloop.com)~~ ✓ — the hosted version
|
|
496
|
+
- ~~[AdLoop Cloud](https://getadloop.com)~~ ✓ — the hosted version: no Google Cloud project, no developer token, connect Google in two clicks (EU-hosted, GDPR-first)
|
|
495
497
|
- ~~Headless server support~~ ✓ — manual URL copy-paste flow for servers without a browser
|
|
496
498
|
- ~~Behavioral eval suites~~ ✓ — 28 prompt-and-expectation tests covering read, write, tracking, and planning workflows
|
|
497
499
|
- ~~Google Tag Manager integration~~ ✓ — read tools for tags, triggers, variables, workspaces, and version history, plus the `audit_event_coverage` three-way join across codebase events, GTM tags, and GA4 actual fires
|
|
@@ -278,10 +278,24 @@ def get_keyword_performance(
|
|
|
278
278
|
return {"keywords": rows, "total_keywords": len(rows)}
|
|
279
279
|
|
|
280
280
|
qs_field = "ad_group_criterion.quality_info.quality_score"
|
|
281
|
+
|
|
282
|
+
def _rated_score(r: dict) -> int | None:
|
|
283
|
+
"""The Quality Score, or None when Google has not assigned one.
|
|
284
|
+
|
|
285
|
+
Google never issues a real score of 0: both null and 0 mean "not
|
|
286
|
+
enough impressions to rate". Counting those as "< 5" reports an
|
|
287
|
+
ordinary unrated long tail as an account-wide relevance problem and
|
|
288
|
+
sends the assistant off fixing ads and landing pages that are fine.
|
|
289
|
+
"""
|
|
290
|
+
score = r.get(qs_field)
|
|
291
|
+
|
|
292
|
+
return score if isinstance(score, int) and score > 0 else None
|
|
293
|
+
|
|
281
294
|
low_quality = [
|
|
282
295
|
r for r in rows
|
|
283
|
-
if r
|
|
296
|
+
if (score := _rated_score(r)) is not None and score < 5
|
|
284
297
|
]
|
|
298
|
+
unrated_count = sum(1 for r in rows if _rated_score(r) is None)
|
|
285
299
|
zero_conv_spenders = [
|
|
286
300
|
r for r in rows
|
|
287
301
|
if (r.get("metrics.cost_micros") or 0) > 0
|
|
@@ -304,6 +318,11 @@ def get_keyword_performance(
|
|
|
304
318
|
f"{len(low_quality)} keyword(s) have quality score < 5 — fix ad "
|
|
305
319
|
f"relevance and landing pages before adding keywords or budget."
|
|
306
320
|
)
|
|
321
|
+
if unrated_count:
|
|
322
|
+
insights.append(
|
|
323
|
+
f"{unrated_count} keyword(s) have no quality score yet (too few "
|
|
324
|
+
f"impressions to rate). They are excluded from the count above."
|
|
325
|
+
)
|
|
307
326
|
if zero_conv_spenders:
|
|
308
327
|
wasted = round(
|
|
309
328
|
sum((r.get("metrics.cost_micros") or 0) for r in zero_conv_spenders)
|
|
@@ -325,6 +344,7 @@ def get_keyword_performance(
|
|
|
325
344
|
),
|
|
326
345
|
"keywords_top_spend": top,
|
|
327
346
|
"low_quality_score": [_kw(r) for r in low_quality[:10]],
|
|
347
|
+
"unrated_quality_score": unrated_count,
|
|
328
348
|
"zero_conversion_spenders": [_kw(r) for r in zero_conv_spenders[:10]],
|
|
329
349
|
"insights": insights,
|
|
330
350
|
"note": _compact_note(len(top), len(rows), "get_keyword_performance"),
|
|
@@ -430,7 +450,7 @@ def get_search_terms(
|
|
|
430
450
|
return {
|
|
431
451
|
"compact": True,
|
|
432
452
|
"total_search_terms": len(rows),
|
|
433
|
-
"totals": _compact_totals(rows, currency_code),
|
|
453
|
+
"totals": _compact_totals(rows, currency_code, row_limit=200),
|
|
434
454
|
"search_terms_top_clicks": top,
|
|
435
455
|
"waste_candidates": [_term(r) for r in waste[:10]],
|
|
436
456
|
"top_converters": [_term(r) for r in converters[:5]],
|
|
@@ -923,8 +943,15 @@ def _date_clause(start: str, end: str) -> str:
|
|
|
923
943
|
return "AND segments.date DURING LAST_30_DAYS"
|
|
924
944
|
|
|
925
945
|
|
|
926
|
-
def _compact_totals(
|
|
927
|
-
|
|
946
|
+
def _compact_totals(
|
|
947
|
+
rows: list[dict], currency_code: str, *, row_limit: int | None = None
|
|
948
|
+
) -> dict:
|
|
949
|
+
"""Deterministic aggregates over the rows given.
|
|
950
|
+
|
|
951
|
+
Not account-level whenever the query behind them carries a LIMIT: pass
|
|
952
|
+
``row_limit`` for those reports so a truncated total is marked partial
|
|
953
|
+
instead of being read as the account's real cost and conversions.
|
|
954
|
+
"""
|
|
928
955
|
cost = sum((r.get("metrics.cost_micros") or 0) for r in rows) / 1_000_000
|
|
929
956
|
clicks = sum((r.get("metrics.clicks") or 0) for r in rows)
|
|
930
957
|
impressions = sum((r.get("metrics.impressions") or 0) for r in rows)
|
|
@@ -940,6 +967,21 @@ def _compact_totals(rows: list[dict], currency_code: str) -> dict:
|
|
|
940
967
|
totals["cpa"] = round(cost / conversions, 2)
|
|
941
968
|
if impressions > 0:
|
|
942
969
|
totals["ctr_pct"] = round(clicks / impressions * 100, 2)
|
|
970
|
+
|
|
971
|
+
# Hitting the ceiling exactly is indistinguishable from stopping there, so
|
|
972
|
+
# this errs toward declaring partial. Silently under-reporting cost is the
|
|
973
|
+
# worse failure: these numbers are labelled "totals" and get compared
|
|
974
|
+
# against campaign-level truth.
|
|
975
|
+
if row_limit is not None and len(rows) >= row_limit:
|
|
976
|
+
totals["partial"] = True
|
|
977
|
+
totals["rows_counted"] = len(rows)
|
|
978
|
+
totals["partial_reason"] = (
|
|
979
|
+
f"The underlying query returns at most {row_limit} rows, ranked by "
|
|
980
|
+
f"the report's sort metric, and this account reached that ceiling. "
|
|
981
|
+
f"These totals cover those rows only, not the whole account. Use "
|
|
982
|
+
f"get_campaign_performance for account-level figures."
|
|
983
|
+
)
|
|
984
|
+
|
|
943
985
|
return totals
|
|
944
986
|
|
|
945
987
|
|
|
@@ -82,6 +82,26 @@ def _resolve_path(path_str: str) -> Path:
|
|
|
82
82
|
return Path(os.path.expandvars(os.path.expanduser(path_str)))
|
|
83
83
|
|
|
84
84
|
|
|
85
|
+
def _text(raw: dict, key: str, default: str = "") -> str:
|
|
86
|
+
"""Read a string setting, treating blank as absent.
|
|
87
|
+
|
|
88
|
+
``raw.get(key, default)`` only falls back when the key is *missing*, so an
|
|
89
|
+
explicit ``token_path: ""`` — easy to produce from a template with blank
|
|
90
|
+
placeholders — passed straight through. ``Path("")`` is ``Path(".")``, the
|
|
91
|
+
working directory always exists, and adloop then tried to read the cwd as
|
|
92
|
+
a token file and died with ``[Errno 1] Operation not permitted: '.'``,
|
|
93
|
+
which points nowhere near the config that caused it.
|
|
94
|
+
|
|
95
|
+
Also coerces non-strings, so ``customer_id: 1234567890`` in YAML arrives
|
|
96
|
+
as text rather than an int.
|
|
97
|
+
"""
|
|
98
|
+
value = raw.get(key, default)
|
|
99
|
+
|
|
100
|
+
if value is None:
|
|
101
|
+
return default
|
|
102
|
+
|
|
103
|
+
return str(value).strip() or default
|
|
104
|
+
|
|
85
105
|
def load_config(config_path: str | None = None) -> AdLoopConfig:
|
|
86
106
|
"""Load configuration from YAML file.
|
|
87
107
|
|
|
@@ -112,34 +132,34 @@ def load_config(config_path: str | None = None) -> AdLoopConfig:
|
|
|
112
132
|
|
|
113
133
|
return AdLoopConfig(
|
|
114
134
|
google=GoogleConfig(
|
|
115
|
-
project_id=google_raw
|
|
116
|
-
credentials_path=google_raw
|
|
117
|
-
token_path=google_raw
|
|
135
|
+
project_id=_text(google_raw, "project_id"),
|
|
136
|
+
credentials_path=_text(google_raw, "credentials_path"),
|
|
137
|
+
token_path=_text(google_raw, "token_path", "~/.adloop/token.json"),
|
|
118
138
|
),
|
|
119
139
|
ga4=GA4Config(
|
|
120
|
-
property_id=ga4_raw
|
|
140
|
+
property_id=_text(ga4_raw, "property_id"),
|
|
121
141
|
),
|
|
122
142
|
ads=AdsConfig(
|
|
123
|
-
developer_token=ads_raw
|
|
124
|
-
customer_id=ads_raw
|
|
125
|
-
login_customer_id=ads_raw
|
|
143
|
+
developer_token=_text(ads_raw, "developer_token"),
|
|
144
|
+
customer_id=_text(ads_raw, "customer_id"),
|
|
145
|
+
login_customer_id=_text(ads_raw, "login_customer_id"),
|
|
126
146
|
),
|
|
127
147
|
gsc=GscConfig(
|
|
128
|
-
site_url=gsc_raw
|
|
148
|
+
site_url=_text(gsc_raw, "site_url"),
|
|
129
149
|
),
|
|
130
150
|
gtm=GtmConfig(
|
|
131
|
-
account_id=
|
|
132
|
-
container_id=
|
|
151
|
+
account_id=_text(gtm_raw, "account_id"),
|
|
152
|
+
container_id=_text(gtm_raw, "container_id"),
|
|
133
153
|
),
|
|
134
154
|
pagespeed=PageSpeedConfig(
|
|
135
|
-
api_key=
|
|
155
|
+
api_key=_text(pagespeed_raw, "api_key"),
|
|
136
156
|
),
|
|
137
157
|
safety=SafetyConfig(
|
|
138
158
|
max_daily_budget=safety_raw.get("max_daily_budget", 50.0),
|
|
139
159
|
max_bid_increase_pct=safety_raw.get("max_bid_increase_pct", 100),
|
|
140
160
|
require_dry_run=safety_raw.get("require_dry_run", True),
|
|
141
161
|
two_phase_apply=safety_raw.get("two_phase_apply", False),
|
|
142
|
-
log_file=safety_raw
|
|
162
|
+
log_file=_text(safety_raw, "log_file", "~/.adloop/audit.log"),
|
|
143
163
|
blocked_operations=safety_raw.get("blocked_operations", []),
|
|
144
164
|
),
|
|
145
165
|
source_path=resolved,
|
|
@@ -655,16 +655,29 @@ For cities/regions, use `run_gaql` with `geo_target_constant` resource or look u
|
|
|
655
655
|
|
|
656
656
|
### Common Language IDs (languageConstants)
|
|
657
657
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
|
663
|
-
|
|
664
|
-
|
|
|
665
|
-
|
|
|
666
|
-
|
|
|
667
|
-
|
|
|
658
|
+
Verified against the `language_constant` resource. Google accepts a wrong
|
|
659
|
+
language ID silently, so confirm anything not listed here with:
|
|
660
|
+
`SELECT language_constant.id, language_constant.code, language_constant.name FROM language_constant`
|
|
661
|
+
|
|
662
|
+
| ID | Code | Language |
|
|
663
|
+
|----|------|----------|
|
|
664
|
+
| 1000 | en | English |
|
|
665
|
+
| 1001 | de | German |
|
|
666
|
+
| 1002 | fr | French |
|
|
667
|
+
| 1003 | es | Spanish |
|
|
668
|
+
| 1004 | it | Italian |
|
|
669
|
+
| 1005 | ja | Japanese |
|
|
670
|
+
| 1009 | da | Danish |
|
|
671
|
+
| 1010 | nl | Dutch |
|
|
672
|
+
| 1011 | fi | Finnish |
|
|
673
|
+
| 1014 | pt | Portuguese |
|
|
674
|
+
| 1015 | sv | Swedish |
|
|
675
|
+
| 1017 | zh_CN | Chinese (simplified) |
|
|
676
|
+
| 1019 | ar | Arabic |
|
|
677
|
+
| 1021 | cs | Czech |
|
|
678
|
+
| 1030 | pl | Polish |
|
|
679
|
+
| 1031 | ru | Russian |
|
|
680
|
+
| 1037 | tr | Turkish |
|
|
668
681
|
|
|
669
682
|
## Marketing Best Practices
|
|
670
683
|
|
|
@@ -285,7 +285,7 @@ def _safe(fn: Callable) -> Callable:
|
|
|
285
285
|
# ---------------------------------------------------------------------------
|
|
286
286
|
|
|
287
287
|
|
|
288
|
-
@mcp.tool(annotations=_READONLY, tags={"core"})
|
|
288
|
+
@mcp.tool(title="Connection health check", annotations=_READONLY, tags={"core"})
|
|
289
289
|
@_safe
|
|
290
290
|
def health_check() -> dict:
|
|
291
291
|
"""Test AdLoop connectivity — checks OAuth token, GA4 API, and Google Ads API.
|
|
@@ -372,7 +372,7 @@ def health_check() -> dict:
|
|
|
372
372
|
# ---------------------------------------------------------------------------
|
|
373
373
|
|
|
374
374
|
|
|
375
|
-
@mcp.tool(annotations=_READONLY, tags={"ga4"})
|
|
375
|
+
@mcp.tool(title="List Analytics properties", annotations=_READONLY, tags={"ga4"})
|
|
376
376
|
@_safe
|
|
377
377
|
def get_account_summaries() -> dict:
|
|
378
378
|
"""List all GA4 accounts and properties accessible by the authenticated user.
|
|
@@ -385,7 +385,7 @@ def get_account_summaries() -> dict:
|
|
|
385
385
|
return _impl(current_config())
|
|
386
386
|
|
|
387
387
|
|
|
388
|
-
@mcp.tool(annotations=_READONLY, tags={"ga4"})
|
|
388
|
+
@mcp.tool(title="Analytics report", annotations=_READONLY, tags={"ga4"})
|
|
389
389
|
@_safe
|
|
390
390
|
def run_ga4_report(
|
|
391
391
|
dimensions: _StrListOpt = None,
|
|
@@ -416,7 +416,7 @@ def run_ga4_report(
|
|
|
416
416
|
)
|
|
417
417
|
|
|
418
418
|
|
|
419
|
-
@mcp.tool(annotations=_READONLY, tags={"ga4"})
|
|
419
|
+
@mcp.tool(title="Analytics realtime report", annotations=_READONLY, tags={"ga4"})
|
|
420
420
|
@_safe
|
|
421
421
|
def run_realtime_report(
|
|
422
422
|
dimensions: _StrListOpt = None,
|
|
@@ -439,7 +439,7 @@ def run_realtime_report(
|
|
|
439
439
|
)
|
|
440
440
|
|
|
441
441
|
|
|
442
|
-
@mcp.tool(annotations=_READONLY, tags={"ga4"})
|
|
442
|
+
@mcp.tool(title="List Analytics events", annotations=_READONLY, tags={"ga4"})
|
|
443
443
|
@_safe
|
|
444
444
|
def get_tracking_events(
|
|
445
445
|
date_range_start: str = "28daysAgo",
|
|
@@ -466,7 +466,7 @@ def get_tracking_events(
|
|
|
466
466
|
# ---------------------------------------------------------------------------
|
|
467
467
|
|
|
468
468
|
|
|
469
|
-
@mcp.tool(annotations=_READONLY, tags={"gsc"})
|
|
469
|
+
@mcp.tool(title="List Search Console properties", annotations=_READONLY, tags={"gsc"})
|
|
470
470
|
@_safe
|
|
471
471
|
def list_gsc_sites() -> dict:
|
|
472
472
|
"""List all Google Search Console properties the authenticated user can access.
|
|
@@ -480,7 +480,7 @@ def list_gsc_sites() -> dict:
|
|
|
480
480
|
return _impl(current_config())
|
|
481
481
|
|
|
482
482
|
|
|
483
|
-
@mcp.tool(annotations=_READONLY, tags={"gsc"})
|
|
483
|
+
@mcp.tool(title="Search Console report", annotations=_READONLY, tags={"gsc"})
|
|
484
484
|
@_safe
|
|
485
485
|
def run_gsc_report(
|
|
486
486
|
site_url: str = "",
|
|
@@ -530,7 +530,7 @@ def run_gsc_report(
|
|
|
530
530
|
# ---------------------------------------------------------------------------
|
|
531
531
|
|
|
532
532
|
|
|
533
|
-
@mcp.tool(annotations=_READONLY, tags={"web"})
|
|
533
|
+
@mcp.tool(title="PageSpeed analysis", annotations=_READONLY, tags={"web"})
|
|
534
534
|
@_safe
|
|
535
535
|
def analyze_page_speed(url: str, strategy: str = "mobile") -> dict:
|
|
536
536
|
"""Run PageSpeed Insights for a landing page — Lighthouse + real-user data.
|
|
@@ -549,7 +549,11 @@ def analyze_page_speed(url: str, strategy: str = "mobile") -> dict:
|
|
|
549
549
|
return _impl(current_config(), url=url, strategy=strategy)
|
|
550
550
|
|
|
551
551
|
|
|
552
|
-
@mcp.tool(
|
|
552
|
+
@mcp.tool(
|
|
553
|
+
title="List Merchant Center accounts",
|
|
554
|
+
annotations=_READONLY,
|
|
555
|
+
tags={"merchant"},
|
|
556
|
+
)
|
|
553
557
|
@_safe
|
|
554
558
|
def list_merchant_accounts() -> dict:
|
|
555
559
|
"""List Google Merchant Center accounts the connected user can access.
|
|
@@ -562,7 +566,7 @@ def list_merchant_accounts() -> dict:
|
|
|
562
566
|
return _impl(current_config())
|
|
563
567
|
|
|
564
568
|
|
|
565
|
-
@mcp.tool(annotations=_READONLY, tags={"merchant"})
|
|
569
|
+
@mcp.tool(title="Merchant Center feed health", annotations=_READONLY, tags={"merchant"})
|
|
566
570
|
@_safe
|
|
567
571
|
def get_merchant_feed_health(account_id: str) -> dict:
|
|
568
572
|
"""Merchant Center feed health — disapproved products + account issues.
|
|
@@ -581,7 +585,7 @@ def get_merchant_feed_health(account_id: str) -> dict:
|
|
|
581
585
|
return _impl(current_config(), account_id=account_id)
|
|
582
586
|
|
|
583
587
|
|
|
584
|
-
@mcp.tool(annotations=_READONLY, tags={"ads"})
|
|
588
|
+
@mcp.tool(title="List Google Ads accounts", annotations=_READONLY, tags={"ads"})
|
|
585
589
|
@_safe
|
|
586
590
|
def list_accounts(limit: int = 200) -> dict:
|
|
587
591
|
"""List accessible Google Ads accounts.
|
|
@@ -600,7 +604,7 @@ def list_accounts(limit: int = 200) -> dict:
|
|
|
600
604
|
return _impl(current_config(), limit=limit)
|
|
601
605
|
|
|
602
606
|
|
|
603
|
-
@mcp.tool(annotations=_READONLY, tags={"ads"})
|
|
607
|
+
@mcp.tool(title="Campaign performance", annotations=_READONLY, tags={"ads"})
|
|
604
608
|
@_safe
|
|
605
609
|
def get_campaign_performance(
|
|
606
610
|
customer_id: str = "",
|
|
@@ -629,7 +633,7 @@ def get_campaign_performance(
|
|
|
629
633
|
)
|
|
630
634
|
|
|
631
635
|
|
|
632
|
-
@mcp.tool(annotations=_READONLY, tags={"ads"})
|
|
636
|
+
@mcp.tool(title="Ad performance", annotations=_READONLY, tags={"ads"})
|
|
633
637
|
@_safe
|
|
634
638
|
def get_ad_performance(
|
|
635
639
|
customer_id: str = "",
|
|
@@ -657,7 +661,7 @@ def get_ad_performance(
|
|
|
657
661
|
)
|
|
658
662
|
|
|
659
663
|
|
|
660
|
-
@mcp.tool(annotations=_READONLY, tags={"ads"})
|
|
664
|
+
@mcp.tool(title="Keyword performance", annotations=_READONLY, tags={"ads"})
|
|
661
665
|
@_safe
|
|
662
666
|
def get_keyword_performance(
|
|
663
667
|
customer_id: str = "",
|
|
@@ -685,7 +689,7 @@ def get_keyword_performance(
|
|
|
685
689
|
)
|
|
686
690
|
|
|
687
691
|
|
|
688
|
-
@mcp.tool(annotations=_READONLY, tags={"ads"})
|
|
692
|
+
@mcp.tool(title="Search terms report", annotations=_READONLY, tags={"ads"})
|
|
689
693
|
@_safe
|
|
690
694
|
def get_search_terms(
|
|
691
695
|
customer_id: str = "",
|
|
@@ -713,7 +717,7 @@ def get_search_terms(
|
|
|
713
717
|
)
|
|
714
718
|
|
|
715
719
|
|
|
716
|
-
@mcp.tool(annotations=_READONLY, tags={"ads"})
|
|
720
|
+
@mcp.tool(title="List negative keywords", annotations=_READONLY, tags={"ads"})
|
|
717
721
|
@_safe
|
|
718
722
|
def get_negative_keywords(
|
|
719
723
|
customer_id: str = "",
|
|
@@ -733,7 +737,11 @@ def get_negative_keywords(
|
|
|
733
737
|
)
|
|
734
738
|
|
|
735
739
|
|
|
736
|
-
@mcp.tool(
|
|
740
|
+
@mcp.tool(
|
|
741
|
+
title="List shared negative keyword lists",
|
|
742
|
+
annotations=_READONLY,
|
|
743
|
+
tags={"ads"},
|
|
744
|
+
)
|
|
737
745
|
@_safe
|
|
738
746
|
def get_negative_keyword_lists(
|
|
739
747
|
customer_id: str = "",
|
|
@@ -749,7 +757,11 @@ def get_negative_keyword_lists(
|
|
|
749
757
|
return _impl(current_config(), customer_id=customer_id or current_config().ads.customer_id)
|
|
750
758
|
|
|
751
759
|
|
|
752
|
-
@mcp.tool(
|
|
760
|
+
@mcp.tool(
|
|
761
|
+
title="Keywords in a negative keyword list",
|
|
762
|
+
annotations=_READONLY,
|
|
763
|
+
tags={"ads"},
|
|
764
|
+
)
|
|
753
765
|
@_safe
|
|
754
766
|
def get_negative_keyword_list_keywords(
|
|
755
767
|
shared_set_id: str,
|
|
@@ -768,7 +780,11 @@ def get_negative_keyword_list_keywords(
|
|
|
768
780
|
)
|
|
769
781
|
|
|
770
782
|
|
|
771
|
-
@mcp.tool(
|
|
783
|
+
@mcp.tool(
|
|
784
|
+
title="Campaigns using a negative keyword list",
|
|
785
|
+
annotations=_READONLY,
|
|
786
|
+
tags={"ads"},
|
|
787
|
+
)
|
|
772
788
|
@_safe
|
|
773
789
|
def get_negative_keyword_list_campaigns(
|
|
774
790
|
shared_set_id: str = "",
|
|
@@ -793,7 +809,7 @@ def get_negative_keyword_list_campaigns(
|
|
|
793
809
|
# ---------------------------------------------------------------------------
|
|
794
810
|
|
|
795
811
|
|
|
796
|
-
@mcp.tool(annotations=_READONLY, tags={"ads"})
|
|
812
|
+
@mcp.tool(title="Google Ads recommendations", annotations=_READONLY, tags={"ads"})
|
|
797
813
|
@_safe
|
|
798
814
|
def get_recommendations(
|
|
799
815
|
customer_id: str = "",
|
|
@@ -822,7 +838,7 @@ def get_recommendations(
|
|
|
822
838
|
)
|
|
823
839
|
|
|
824
840
|
|
|
825
|
-
@mcp.tool(annotations=_READONLY, tags={"ads"})
|
|
841
|
+
@mcp.tool(title="Performance Max performance", annotations=_READONLY, tags={"ads"})
|
|
826
842
|
@_safe
|
|
827
843
|
def get_pmax_performance(
|
|
828
844
|
customer_id: str = "",
|
|
@@ -853,7 +869,11 @@ def get_pmax_performance(
|
|
|
853
869
|
)
|
|
854
870
|
|
|
855
871
|
|
|
856
|
-
@mcp.tool(
|
|
872
|
+
@mcp.tool(
|
|
873
|
+
title="Performance Max asset performance",
|
|
874
|
+
annotations=_READONLY,
|
|
875
|
+
tags={"ads"},
|
|
876
|
+
)
|
|
857
877
|
@_safe
|
|
858
878
|
def get_asset_performance(
|
|
859
879
|
customer_id: str = "",
|
|
@@ -882,7 +902,11 @@ def get_asset_performance(
|
|
|
882
902
|
)
|
|
883
903
|
|
|
884
904
|
|
|
885
|
-
@mcp.tool(
|
|
905
|
+
@mcp.tool(
|
|
906
|
+
title="Performance Max asset combinations",
|
|
907
|
+
annotations=_READONLY,
|
|
908
|
+
tags={"ads"},
|
|
909
|
+
)
|
|
886
910
|
@_safe
|
|
887
911
|
def get_detailed_asset_performance(
|
|
888
912
|
customer_id: str = "",
|
|
@@ -905,7 +929,7 @@ def get_detailed_asset_performance(
|
|
|
905
929
|
)
|
|
906
930
|
|
|
907
931
|
|
|
908
|
-
@mcp.tool(annotations=_READONLY, tags={"ads"})
|
|
932
|
+
@mcp.tool(title="Audience performance", annotations=_READONLY, tags={"ads"})
|
|
909
933
|
@_safe
|
|
910
934
|
def get_audience_performance(
|
|
911
935
|
customer_id: str = "",
|
|
@@ -936,7 +960,7 @@ def get_audience_performance(
|
|
|
936
960
|
)
|
|
937
961
|
|
|
938
962
|
|
|
939
|
-
@mcp.tool(annotations=_READONLY, tags={"ads"})
|
|
963
|
+
@mcp.tool(title="Demographic targeting", annotations=_READONLY, tags={"ads"})
|
|
940
964
|
@_safe
|
|
941
965
|
def get_demographic_targeting(
|
|
942
966
|
ad_group_id: str = "",
|
|
@@ -969,7 +993,11 @@ def get_demographic_targeting(
|
|
|
969
993
|
# ---------------------------------------------------------------------------
|
|
970
994
|
|
|
971
995
|
|
|
972
|
-
@mcp.tool(
|
|
996
|
+
@mcp.tool(
|
|
997
|
+
title="Campaign conversions against Analytics",
|
|
998
|
+
annotations=_READONLY,
|
|
999
|
+
tags={"tracking"},
|
|
1000
|
+
)
|
|
973
1001
|
@_safe
|
|
974
1002
|
def analyze_campaign_conversions(
|
|
975
1003
|
date_range_start: str = "",
|
|
@@ -999,7 +1027,7 @@ def analyze_campaign_conversions(
|
|
|
999
1027
|
)
|
|
1000
1028
|
|
|
1001
1029
|
|
|
1002
|
-
@mcp.tool(annotations=_READONLY, tags={"tracking"})
|
|
1030
|
+
@mcp.tool(title="Landing page analysis", annotations=_READONLY, tags={"tracking"})
|
|
1003
1031
|
@_safe
|
|
1004
1032
|
def landing_page_analysis(
|
|
1005
1033
|
date_range_start: str = "",
|
|
@@ -1025,7 +1053,7 @@ def landing_page_analysis(
|
|
|
1025
1053
|
)
|
|
1026
1054
|
|
|
1027
1055
|
|
|
1028
|
-
@mcp.tool(annotations=_READONLY, tags={"tracking"})
|
|
1056
|
+
@mcp.tool(title="Attribution check", annotations=_READONLY, tags={"tracking"})
|
|
1029
1057
|
@_safe
|
|
1030
1058
|
def attribution_check(
|
|
1031
1059
|
date_range_start: str = "",
|
|
@@ -1056,7 +1084,7 @@ def attribution_check(
|
|
|
1056
1084
|
)
|
|
1057
1085
|
|
|
1058
1086
|
|
|
1059
|
-
@mcp.tool(annotations=_READONLY, tags={"gtm"})
|
|
1087
|
+
@mcp.tool(title="Tracking coverage audit", annotations=_READONLY, tags={"gtm"})
|
|
1060
1088
|
@_safe
|
|
1061
1089
|
def audit_event_coverage(
|
|
1062
1090
|
expected_events: list[str],
|
|
@@ -1109,7 +1137,7 @@ def audit_event_coverage(
|
|
|
1109
1137
|
)
|
|
1110
1138
|
|
|
1111
1139
|
|
|
1112
|
-
@mcp.tool(annotations=_READONLY, tags={"gtm"})
|
|
1140
|
+
@mcp.tool(title="List Tag Manager accounts", annotations=_READONLY, tags={"gtm"})
|
|
1113
1141
|
@_safe
|
|
1114
1142
|
def list_gtm_accounts() -> dict:
|
|
1115
1143
|
"""List all GTM accounts the AdLoop service account / OAuth user can read.
|
|
@@ -1124,7 +1152,7 @@ def list_gtm_accounts() -> dict:
|
|
|
1124
1152
|
return _impl(current_config())
|
|
1125
1153
|
|
|
1126
1154
|
|
|
1127
|
-
@mcp.tool(annotations=_READONLY, tags={"gtm"})
|
|
1155
|
+
@mcp.tool(title="List Tag Manager containers", annotations=_READONLY, tags={"gtm"})
|
|
1128
1156
|
@_safe
|
|
1129
1157
|
def list_gtm_containers(gtm_account_id: str = "") -> dict:
|
|
1130
1158
|
"""List all containers under a GTM account.
|
|
@@ -1144,7 +1172,7 @@ def list_gtm_containers(gtm_account_id: str = "") -> dict:
|
|
|
1144
1172
|
return _impl(current_config(), account_id=gtm_account_id)
|
|
1145
1173
|
|
|
1146
1174
|
|
|
1147
|
-
@mcp.tool(annotations=_READONLY, tags={"gtm"})
|
|
1175
|
+
@mcp.tool(title="List Tag Manager tags", annotations=_READONLY, tags={"gtm"})
|
|
1148
1176
|
@_safe
|
|
1149
1177
|
def list_gtm_tags(gtm_account_id: str = "", gtm_container_id: str = "") -> dict:
|
|
1150
1178
|
"""List every tag in the LIVE GTM container.
|
|
@@ -1164,7 +1192,7 @@ def list_gtm_tags(gtm_account_id: str = "", gtm_container_id: str = "") -> dict:
|
|
|
1164
1192
|
)
|
|
1165
1193
|
|
|
1166
1194
|
|
|
1167
|
-
@mcp.tool(annotations=_READONLY, tags={"gtm"})
|
|
1195
|
+
@mcp.tool(title="Tag Manager tag details", annotations=_READONLY, tags={"gtm"})
|
|
1168
1196
|
@_safe
|
|
1169
1197
|
def get_gtm_tag(
|
|
1170
1198
|
tag_id: str, gtm_account_id: str = "", gtm_container_id: str = ""
|
|
@@ -1189,7 +1217,7 @@ def get_gtm_tag(
|
|
|
1189
1217
|
)
|
|
1190
1218
|
|
|
1191
1219
|
|
|
1192
|
-
@mcp.tool(annotations=_READONLY, tags={"gtm"})
|
|
1220
|
+
@mcp.tool(title="List Tag Manager triggers", annotations=_READONLY, tags={"gtm"})
|
|
1193
1221
|
@_safe
|
|
1194
1222
|
def list_gtm_triggers(gtm_account_id: str = "", gtm_container_id: str = "") -> dict:
|
|
1195
1223
|
"""List every trigger in the LIVE GTM container.
|
|
@@ -1209,7 +1237,7 @@ def list_gtm_triggers(gtm_account_id: str = "", gtm_container_id: str = "") -> d
|
|
|
1209
1237
|
)
|
|
1210
1238
|
|
|
1211
1239
|
|
|
1212
|
-
@mcp.tool(annotations=_READONLY, tags={"gtm"})
|
|
1240
|
+
@mcp.tool(title="Tag Manager trigger details", annotations=_READONLY, tags={"gtm"})
|
|
1213
1241
|
@_safe
|
|
1214
1242
|
def get_gtm_trigger(
|
|
1215
1243
|
trigger_id: str, gtm_account_id: str = "", gtm_container_id: str = ""
|
|
@@ -1234,7 +1262,7 @@ def get_gtm_trigger(
|
|
|
1234
1262
|
)
|
|
1235
1263
|
|
|
1236
1264
|
|
|
1237
|
-
@mcp.tool(annotations=_READONLY, tags={"gtm"})
|
|
1265
|
+
@mcp.tool(title="List Tag Manager variables", annotations=_READONLY, tags={"gtm"})
|
|
1238
1266
|
@_safe
|
|
1239
1267
|
def list_gtm_variables(gtm_account_id: str = "", gtm_container_id: str = "") -> dict:
|
|
1240
1268
|
"""List GTM variables — both custom and enabled built-in.
|
|
@@ -1256,7 +1284,7 @@ def list_gtm_variables(gtm_account_id: str = "", gtm_container_id: str = "") ->
|
|
|
1256
1284
|
)
|
|
1257
1285
|
|
|
1258
1286
|
|
|
1259
|
-
@mcp.tool(annotations=_READONLY, tags={"gtm"})
|
|
1287
|
+
@mcp.tool(title="List Tag Manager workspaces", annotations=_READONLY, tags={"gtm"})
|
|
1260
1288
|
@_safe
|
|
1261
1289
|
def list_gtm_workspaces(gtm_account_id: str = "", gtm_container_id: str = "") -> dict:
|
|
1262
1290
|
"""List workspaces (drafts) under a GTM container.
|
|
@@ -1276,7 +1304,7 @@ def list_gtm_workspaces(gtm_account_id: str = "", gtm_container_id: str = "") ->
|
|
|
1276
1304
|
)
|
|
1277
1305
|
|
|
1278
1306
|
|
|
1279
|
-
@mcp.tool(annotations=_READONLY, tags={"gtm"})
|
|
1307
|
+
@mcp.tool(title="Tag Manager workspace changes", annotations=_READONLY, tags={"gtm"})
|
|
1280
1308
|
@_safe
|
|
1281
1309
|
def get_gtm_workspace_diff(
|
|
1282
1310
|
workspace_id: str, gtm_account_id: str = "", gtm_container_id: str = ""
|
|
@@ -1303,7 +1331,7 @@ def get_gtm_workspace_diff(
|
|
|
1303
1331
|
)
|
|
1304
1332
|
|
|
1305
1333
|
|
|
1306
|
-
@mcp.tool(annotations=_READONLY, tags={"gtm"})
|
|
1334
|
+
@mcp.tool(title="Tag Manager version history", annotations=_READONLY, tags={"gtm"})
|
|
1307
1335
|
@_safe
|
|
1308
1336
|
def list_gtm_versions(
|
|
1309
1337
|
gtm_account_id: str = "", gtm_container_id: str = "", page_size: int = 50
|
|
@@ -1329,7 +1357,7 @@ def list_gtm_versions(
|
|
|
1329
1357
|
)
|
|
1330
1358
|
|
|
1331
1359
|
|
|
1332
|
-
@mcp.tool(annotations=_READONLY, tags={"gtm"})
|
|
1360
|
+
@mcp.tool(title="Tag Manager version details", annotations=_READONLY, tags={"gtm"})
|
|
1333
1361
|
@_safe
|
|
1334
1362
|
def get_gtm_version(
|
|
1335
1363
|
container_version_id: str, gtm_account_id: str = "", gtm_container_id: str = ""
|
|
@@ -1354,7 +1382,7 @@ def get_gtm_version(
|
|
|
1354
1382
|
)
|
|
1355
1383
|
|
|
1356
1384
|
|
|
1357
|
-
@mcp.tool(annotations=_READONLY, tags={"ads"})
|
|
1385
|
+
@mcp.tool(title="Custom Google Ads query", annotations=_READONLY, tags={"ads"})
|
|
1358
1386
|
@_safe
|
|
1359
1387
|
def run_gaql(
|
|
1360
1388
|
query: str,
|
|
@@ -1383,7 +1411,7 @@ def run_gaql(
|
|
|
1383
1411
|
# ---------------------------------------------------------------------------
|
|
1384
1412
|
|
|
1385
1413
|
|
|
1386
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1414
|
+
@mcp.tool(title="Draft a campaign", annotations=_WRITE, tags={"ads"})
|
|
1387
1415
|
@_safe
|
|
1388
1416
|
def draft_campaign(
|
|
1389
1417
|
campaign_name: str,
|
|
@@ -1450,7 +1478,7 @@ def draft_campaign(
|
|
|
1450
1478
|
)
|
|
1451
1479
|
|
|
1452
1480
|
|
|
1453
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1481
|
+
@mcp.tool(title="Draft an ad group", annotations=_WRITE, tags={"ads"})
|
|
1454
1482
|
@_safe
|
|
1455
1483
|
def draft_ad_group(
|
|
1456
1484
|
campaign_id: str,
|
|
@@ -1483,7 +1511,7 @@ def draft_ad_group(
|
|
|
1483
1511
|
)
|
|
1484
1512
|
|
|
1485
1513
|
|
|
1486
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1514
|
+
@mcp.tool(title="Draft campaign changes", annotations=_WRITE, tags={"ads"})
|
|
1487
1515
|
@_safe
|
|
1488
1516
|
def update_campaign(
|
|
1489
1517
|
campaign_id: str,
|
|
@@ -1540,7 +1568,7 @@ def update_campaign(
|
|
|
1540
1568
|
)
|
|
1541
1569
|
|
|
1542
1570
|
|
|
1543
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1571
|
+
@mcp.tool(title="Draft a responsive search ad", annotations=_WRITE, tags={"ads"})
|
|
1544
1572
|
@_safe
|
|
1545
1573
|
def draft_responsive_search_ad(
|
|
1546
1574
|
ad_group_id: str,
|
|
@@ -1583,7 +1611,7 @@ def draft_responsive_search_ad(
|
|
|
1583
1611
|
)
|
|
1584
1612
|
|
|
1585
1613
|
|
|
1586
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1614
|
+
@mcp.tool(title="Draft keywords", annotations=_WRITE, tags={"ads"})
|
|
1587
1615
|
@_safe
|
|
1588
1616
|
def draft_keywords(
|
|
1589
1617
|
ad_group_id: str,
|
|
@@ -1605,7 +1633,7 @@ def draft_keywords(
|
|
|
1605
1633
|
)
|
|
1606
1634
|
|
|
1607
1635
|
|
|
1608
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1636
|
+
@mcp.tool(title="Draft negative keywords", annotations=_WRITE, tags={"ads"})
|
|
1609
1637
|
@_safe
|
|
1610
1638
|
def add_negative_keywords(
|
|
1611
1639
|
campaign_id: str,
|
|
@@ -1630,7 +1658,7 @@ def add_negative_keywords(
|
|
|
1630
1658
|
)
|
|
1631
1659
|
|
|
1632
1660
|
|
|
1633
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1661
|
+
@mcp.tool(title="Draft negative locations", annotations=_WRITE, tags={"ads"})
|
|
1634
1662
|
@_safe
|
|
1635
1663
|
def add_negative_locations(
|
|
1636
1664
|
campaign_id: str,
|
|
@@ -1654,7 +1682,11 @@ def add_negative_locations(
|
|
|
1654
1682
|
)
|
|
1655
1683
|
|
|
1656
1684
|
|
|
1657
|
-
@mcp.tool(
|
|
1685
|
+
@mcp.tool(
|
|
1686
|
+
title="Draft a shared negative keyword list",
|
|
1687
|
+
annotations=_WRITE,
|
|
1688
|
+
tags={"ads"},
|
|
1689
|
+
)
|
|
1658
1690
|
@_safe
|
|
1659
1691
|
def propose_negative_keyword_list(
|
|
1660
1692
|
campaign_id: str,
|
|
@@ -1682,7 +1714,11 @@ def propose_negative_keyword_list(
|
|
|
1682
1714
|
)
|
|
1683
1715
|
|
|
1684
1716
|
|
|
1685
|
-
@mcp.tool(
|
|
1717
|
+
@mcp.tool(
|
|
1718
|
+
title="Draft additions to a negative keyword list",
|
|
1719
|
+
annotations=_WRITE,
|
|
1720
|
+
tags={"ads"},
|
|
1721
|
+
)
|
|
1686
1722
|
@_safe
|
|
1687
1723
|
def add_to_negative_keyword_list(
|
|
1688
1724
|
shared_set_id: str,
|
|
@@ -1715,7 +1751,7 @@ def add_to_negative_keyword_list(
|
|
|
1715
1751
|
)
|
|
1716
1752
|
|
|
1717
1753
|
|
|
1718
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1754
|
+
@mcp.tool(title="Draft attaching a shared set", annotations=_WRITE, tags={"ads"})
|
|
1719
1755
|
@_safe
|
|
1720
1756
|
def attach_shared_set_to_campaigns(
|
|
1721
1757
|
shared_set_id: str,
|
|
@@ -1746,7 +1782,7 @@ def attach_shared_set_to_campaigns(
|
|
|
1746
1782
|
)
|
|
1747
1783
|
|
|
1748
1784
|
|
|
1749
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1785
|
+
@mcp.tool(title="Draft detaching a shared set", annotations=_WRITE, tags={"ads"})
|
|
1750
1786
|
@_safe
|
|
1751
1787
|
def detach_shared_set_from_campaigns(
|
|
1752
1788
|
shared_set_id: str,
|
|
@@ -1777,7 +1813,7 @@ def detach_shared_set_from_campaigns(
|
|
|
1777
1813
|
)
|
|
1778
1814
|
|
|
1779
1815
|
|
|
1780
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1816
|
+
@mcp.tool(title="Draft demographic targeting", annotations=_WRITE, tags={"ads"})
|
|
1781
1817
|
@_safe
|
|
1782
1818
|
def draft_demographic_targeting(
|
|
1783
1819
|
customer_id: str = "",
|
|
@@ -1829,7 +1865,7 @@ def draft_demographic_targeting(
|
|
|
1829
1865
|
)
|
|
1830
1866
|
|
|
1831
1867
|
|
|
1832
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1868
|
+
@mcp.tool(title="Draft ad group changes", annotations=_WRITE, tags={"ads"})
|
|
1833
1869
|
@_safe
|
|
1834
1870
|
def update_ad_group(
|
|
1835
1871
|
ad_group_id: str,
|
|
@@ -1849,7 +1885,7 @@ def update_ad_group(
|
|
|
1849
1885
|
)
|
|
1850
1886
|
|
|
1851
1887
|
|
|
1852
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1888
|
+
@mcp.tool(title="Draft callouts", annotations=_WRITE, tags={"ads"})
|
|
1853
1889
|
@_safe
|
|
1854
1890
|
def draft_callouts(
|
|
1855
1891
|
campaign_id: str,
|
|
@@ -1867,7 +1903,7 @@ def draft_callouts(
|
|
|
1867
1903
|
)
|
|
1868
1904
|
|
|
1869
1905
|
|
|
1870
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1906
|
+
@mcp.tool(title="Draft structured snippets", annotations=_WRITE, tags={"ads"})
|
|
1871
1907
|
@_safe
|
|
1872
1908
|
def draft_structured_snippets(
|
|
1873
1909
|
campaign_id: str,
|
|
@@ -1885,7 +1921,7 @@ def draft_structured_snippets(
|
|
|
1885
1921
|
)
|
|
1886
1922
|
|
|
1887
1923
|
|
|
1888
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1924
|
+
@mcp.tool(title="Draft image assets", annotations=_WRITE, tags={"ads"})
|
|
1889
1925
|
@_safe
|
|
1890
1926
|
def draft_image_assets(
|
|
1891
1927
|
campaign_id: str,
|
|
@@ -1903,7 +1939,7 @@ def draft_image_assets(
|
|
|
1903
1939
|
)
|
|
1904
1940
|
|
|
1905
1941
|
|
|
1906
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1942
|
+
@mcp.tool(title="Draft pausing an entity", annotations=_WRITE, tags={"ads"})
|
|
1907
1943
|
@_safe
|
|
1908
1944
|
def pause_entity(
|
|
1909
1945
|
entity_type: str,
|
|
@@ -1931,7 +1967,7 @@ def pause_entity(
|
|
|
1931
1967
|
)
|
|
1932
1968
|
|
|
1933
1969
|
|
|
1934
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
1970
|
+
@mcp.tool(title="Draft enabling an entity", annotations=_WRITE, tags={"ads"})
|
|
1935
1971
|
@_safe
|
|
1936
1972
|
def enable_entity(
|
|
1937
1973
|
entity_type: str,
|
|
@@ -1959,7 +1995,7 @@ def enable_entity(
|
|
|
1959
1995
|
)
|
|
1960
1996
|
|
|
1961
1997
|
|
|
1962
|
-
@mcp.tool(annotations=_DESTRUCTIVE, tags={"ads"})
|
|
1998
|
+
@mcp.tool(title="Draft removing an entity", annotations=_DESTRUCTIVE, tags={"ads"})
|
|
1963
1999
|
@_safe
|
|
1964
2000
|
def remove_entity(
|
|
1965
2001
|
entity_type: str,
|
|
@@ -1995,7 +2031,7 @@ def remove_entity(
|
|
|
1995
2031
|
)
|
|
1996
2032
|
|
|
1997
2033
|
|
|
1998
|
-
@mcp.tool(annotations=_WRITE, tags={"ads"})
|
|
2034
|
+
@mcp.tool(title="Draft sitelinks", annotations=_WRITE, tags={"ads"})
|
|
1999
2035
|
@_safe
|
|
2000
2036
|
def draft_sitelinks(
|
|
2001
2037
|
campaign_id: str,
|
|
@@ -2028,7 +2064,7 @@ def draft_sitelinks(
|
|
|
2028
2064
|
)
|
|
2029
2065
|
|
|
2030
2066
|
|
|
2031
|
-
@mcp.tool(annotations=_DESTRUCTIVE, tags={"core"})
|
|
2067
|
+
@mcp.tool(title="Apply a previewed change", annotations=_DESTRUCTIVE, tags={"core"})
|
|
2032
2068
|
@_safe
|
|
2033
2069
|
def confirm_and_apply(
|
|
2034
2070
|
plan_id: str,
|
|
@@ -2065,7 +2101,7 @@ def confirm_and_apply(
|
|
|
2065
2101
|
# ---------------------------------------------------------------------------
|
|
2066
2102
|
|
|
2067
2103
|
|
|
2068
|
-
@mcp.tool(annotations=_WRITE, tags={"ga4"})
|
|
2104
|
+
@mcp.tool(title="Draft an Analytics key event", annotations=_WRITE, tags={"ga4"})
|
|
2069
2105
|
@_safe
|
|
2070
2106
|
def draft_key_event(
|
|
2071
2107
|
event_name: str,
|
|
@@ -2090,7 +2126,7 @@ def draft_key_event(
|
|
|
2090
2126
|
)
|
|
2091
2127
|
|
|
2092
2128
|
|
|
2093
|
-
@mcp.tool(annotations=_READONLY, tags={"tracking"})
|
|
2129
|
+
@mcp.tool(title="Validate tracking", annotations=_READONLY, tags={"tracking"})
|
|
2094
2130
|
@_safe
|
|
2095
2131
|
def validate_tracking(
|
|
2096
2132
|
expected_events: _StrList,
|
|
@@ -2118,7 +2154,7 @@ def validate_tracking(
|
|
|
2118
2154
|
)
|
|
2119
2155
|
|
|
2120
2156
|
|
|
2121
|
-
@mcp.tool(annotations=_READONLY, tags={"tracking"})
|
|
2157
|
+
@mcp.tool(title="Generate tracking code", annotations=_READONLY, tags={"tracking"})
|
|
2122
2158
|
@_safe
|
|
2123
2159
|
def generate_tracking_code(
|
|
2124
2160
|
event_name: str,
|
|
@@ -2153,7 +2189,7 @@ def generate_tracking_code(
|
|
|
2153
2189
|
# ---------------------------------------------------------------------------
|
|
2154
2190
|
|
|
2155
2191
|
|
|
2156
|
-
@mcp.tool(annotations=_READONLY, tags={"ads"})
|
|
2192
|
+
@mcp.tool(title="Budget forecast", annotations=_READONLY, tags={"ads"})
|
|
2157
2193
|
@_safe
|
|
2158
2194
|
def estimate_budget(
|
|
2159
2195
|
keywords: _DictList,
|
|
@@ -2190,7 +2226,7 @@ def estimate_budget(
|
|
|
2190
2226
|
)
|
|
2191
2227
|
|
|
2192
2228
|
|
|
2193
|
-
@mcp.tool(annotations=_READONLY, tags={"ads"})
|
|
2229
|
+
@mcp.tool(title="Keyword ideas", annotations=_READONLY, tags={"ads"})
|
|
2194
2230
|
@_safe
|
|
2195
2231
|
def discover_keywords(
|
|
2196
2232
|
seed_keywords: _StrList = [], # noqa: B006 — mutable default required for MCP JSON schema
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|