Google Ads mit KI-Agent verwalten: MCP Server mit Geschäftsregeln
Wie verwalten Sie Google Ads mit einem KI-Agenten, ohne das Budget zu riskieren? Der Agent sollte nicht direkt in die Google Ads API schreiben. Er arbeitet über einen MCP Server, der die Geschäftsregeln hält: Gebotsgrenzen, Ausschlusslisten, einen Check vor jeder Sitzung und ein Protokoll mit Tracing für jede Änderung. Unten sehen Sie dieses Setup im Konto meines Etsy-Shops.
Warum nicht der offizielle Google Ads MCP Server
Der offizielle Google Ads MCP Server von Google ist in der aktuellen Version nur lesend. Er bietet drei Tools: list_accessible_customers, search (GAQL) und get_resource_metadata. Laut Google-Dokumentation (Stand 13.10.2026) kann er keine Gebote ändern, keine Kampagnen pausieren und keine neuen Assets anlegen.
Schreibzugriff heißt, die Mutate-Methoden der Google Ads API direkt aufzurufen. Die API führt aus, was man ihr schickt. Sie kennt keine Objektsperren: Gleichzeitige Änderungen enden in einem CONCURRENT_MODIFICATION_ERROR. Google empfiehlt für sichere Änderungen selbst Testkonten, Validierungsläufe und die Change History.
Meine Schlussfolgerung: Die Regeln meines Geschäfts gehören in eine Schicht, die ich besitze. Wie weit darf sich ein Gebot bewegen? Welche Suchanfragen sind nie erwünscht? Wie viele Anzeigen dürfen laufen? Auf keine dieser Fragen kann ein allgemeiner Server eine Antwort haben.
Das Projekt ist ein eigenes Werkzeug für meinen Etsy-Shop MoonBloom (Kinder-Tapeten, Google-Ads-Kampagnen in DE, AT, ES und FR). Der Code ist privat. Ich zeige deshalb nur kurze Fragmente und keine Repository-Verlinkung.
Wie ich den MCP Server erstellt habe: ein Tool-Aufruf von Anfang bis Ende
Der Stack: Python, FastMCP aus dem offiziellen MCP SDK (mcp 1.28.1) mit stdio-Transport, die google-ads-Clientbibliothek 31.0.0, Pydantic 2.13 sowie OpenTelemetry mit Cloud-Trace-Exporter, gRPC-Instrumentierung und Google Cloud Logging. Der Agent ist Claude Code; den Server trage ich in der .mcp.json des Projekts ein. Der Weg eines einzelnen Tool-Aufrufs:
↓
mcp_tool() registriert das Tool bei FastMCPEingabeschild (Pydantic):
build_request() prüft jede Schreibanfrage↓ Regel verletzt:
REQUEST_REJECTED, Google wird nie aufgerufenPreflight-Gate:
refusal_for()↓ Gate zu:
PREFLIGHT_REQUIRED, Google wird nie aufgerufenGeschäftslogik in
tools/*.py↓ Google-Ads-Client mit gRPC-Interceptor (
core/client.py)Ergebnis + Telemetrie:
append_audit_log↓
logs/audit.jsonl, Cloud Logging, Cloud Trace
- Tool-Aufruf. Ein Decorator
mcp_tool()inserver.pyregistriert jedes Tool bei FastMCP und versieht den Aufruf mit dem Tool-Namen. - Eingabeschild. Bei Schreibzugriffen baut
build_request()ein Pydantic-Modell (shield/models_*.py). Verletzt eine Regel die Vorgaben, landet der Aufruf alsREQUEST_REJECTEDim Journal und Google wird nie aufgerufen. Lesende Tools validieren IDs (IdArguments: nur Ziffern). - Preflight-Gate.
refusal_for()inshield/preflight.py: Bis die Log-Prüfung der Sitzung bestanden ist, liefert jedes nicht ausgenommene ToolPREFLIGHT_REQUIRED. Auch das wird im Journal vermerkt, Google bleibt unberührt. - Geschäftslogik. Sie liegt in
tools/*.py, getrennt nach campaign, adgroup, rsa, asset, negative, history, observability, eval und preflight. - Google-Ads-Client. Ein gRPC-Interceptor in
core/client.pyprotokolliert jeden RPC und wiederholt nur lesende Methoden: 2 Wiederholungen, nach 1 s und 2 s plus bis zu 0,5 s Jitter. Schreibzugriffe wiederholt der Client nie automatisch. - Ergebnis und Telemetrie.
append_audit_logschwärzt Secrets und schreibt nachlogs/audit.jsonl. Die Zeile wird nach Cloud Logging gespiegelt, der Span geht an Cloud Trace.
Tool-Katalog: 36 Tools über der Google Ads API
Insgesamt sind es 36 Tools, 24 lesend und 12 schreibend. Neben den Tools für Kampagnen, Anzeigen und Assets stehen 7 Tools für Preflight, Journal und Auswertung, denn der Agent soll sein eigenes Verhalten prüfen können.
| Gruppe | Anzahl | Alle Tools der Gruppe | Davon schreibend |
|---|---|---|---|
| Preflight, Journal, Observability | 7 | preflight_log_check, get_change_history, verify_changes_logged, get_cloud_logs, get_cloud_trace, analyze_tool_health, evaluate_agent_process |
keines, alle lesend |
| Kampagnen, Budgets, Gebote | 10 | get_campaigns, get_campaign_details, toggle_campaign_status, set_campaign_daily_budget, get_campaign_targeting_overview, get_ad_groups, step_bid_threshold, set_ad_group_cpc_bid, get_ad_group_keywords, set_keyword_status |
toggle_campaign_status, set_campaign_daily_budget, step_bid_threshold, set_ad_group_cpc_bid, set_keyword_status |
| Anzeigen | 2 | get_campaign_ads, deploy_golden_rsa |
deploy_golden_rsa |
| Assets | 7 | get_assets_associations, get_assets_performance, get_assets_diversity, link_asset_to_entity, unlink_asset_from_entity, get_campaign_images, get_campaign_assets_overview |
link_asset_to_entity, unlink_asset_from_entity |
| Suchbegriffe, Negative, Audits, Empfehlungen | 10 | apply_negative_shield, add_negative_keywords, remove_negative_keyword, mine_search_terms_and_harvest_negatives, audit_campaign_4level_shield, get_daily_performance, get_impression_share_loss, auto_fix_collisions_and_dupes, fetch_google_ai_recommendations, search_keyword_ideas |
apply_negative_shield, add_negative_keywords, remove_negative_keyword, auto_fix_collisions_and_dupes |
Geschäftsregeln im Code, nicht im Prompt
Ein Prompt kann ein Modell bitten, sich an eine Grenze zu halten. Code kann den Aufruf verweigern. Alle Regeln unten liegen deshalb im Server, nicht in einer Anweisung an den Agenten.
| Regel | Wo im Code | Was sie festlegt und was bei einem Verstoß passiert |
|---|---|---|
| Preflight-Gate | shield/preflight.py, tools/preflight_tools.py |
Antwort PREFLIGHT_REQUIRED, kein Google-Aufruf. Geprüft werden TOOL_HEALTH, OUTSIDE_CHANGES und AGENT_PROCESS über die letzten 7 Tage oder seit dem letzten akzeptierten Checkpoint (maximal 30). OK öffnet das Gate. ATTENTION hält es zu, bis der Besitzer "weiter" sagt und der Agent mit acknowledge=True erneut prüft. |
| CPC-Obergrenze (Maximize Clicks) | tools/campaign_tools.py |
Bewegt sich nur in Schritten von ±0,05 EUR und muss zwischen 0,10 und 3,00 EUR bleiben. Gilt nur für TARGET_SPEND-Kampagnen und verweigert Kampagnen ohne Obergrenze ("refusing to guess a starting value"). Außerhalb des Bereichs wird abgelehnt, es wird nichts gesendet. |
| Tagesbudget | shield/models_campaign.py, core/client.py |
1,00-100,00 EUR. Beträge werden auf ganze Cent gerundet, weil int(4.10 * 1_000_000) == 4099999 ergibt, was die API ablehnt. |
| Gebot der Anzeigengruppe | shield/models_adgroup.py |
0,05-3,00 EUR, nur für MANUAL_CPC-Kampagnen. |
| Responsive Suchanzeigen ("golden RSA") | shield/models_ad.py, tools/rsa_tools.py |
Genau 15 Headlines und 4 Beschreibungen. Headline 1 muss die Dynamic-Keyword-Insertion-Headline sein, Headlines höchstens 30 Zeichen, Beschreibungen höchstens 90. Die Ziel-URL muss den Gutschein-Parameter des Shops tragen. Pro Anzeigengruppe sind höchstens 3 RSAs aktiv: Die neue Anzeige wird angelegt und die älteste im selben Mutate-Request pausiert (atomar). |
| Negativ-Keyword-Listen | tools/negative_tools.py |
Je Sprache eine Liste: DE 58, FR 40, ES 39 Wörter. Bereits vorhandene Wörter werden übersprungen, gesendet wird in Blöcken zu 500. |
| Duplikate über Kampagnen | tools/negative_tools.py |
Keywords, die auch in der High-Intent-Kampagne existieren, werden in den anderen Kampagnen pausiert (auto_fix_collisions_and_dupes). |
| Sparse Update Masks | 7 Schreibpfade | Jedes Update sendet eine explizite Feldmaske, sodass nur das genannte Feld geändert wird. |
| Änderungen von außen | tools/history_tools.py |
Googles change_event-Historie wird mit den MUTATION-Zeilen des Journals abgeglichen. Was ein Skript, ein anderer Agent oder ein Mensch in der Oberfläche geändert hat, erscheint als OUTSIDE_CHANGES. |
| Wiederholungen | core/client.py |
Nur lesend. Schreibzugriffe wiederholt der Client nie. |
Die CPC-Obergrenze: ein Schritt, drei Prüfungen
Dieses Fragment ist der Kern des Gebotstools. Es prüft den Gebotstyp, verweigert den Schritt bei fehlender Obergrenze, statt einen Startwert zu raten, und lehnt jedes Ergebnis außerhalb von 0,10-3,00 EUR ab.
if c.bidding_strategy_type.name != "TARGET_SPEND":
raise ValueError(f"Campaign '{c.name}' uses {c.bidding_strategy_type.name}, not TARGET_SPEND (Maximize Clicks); it has no CPC ceiling to step.")
curr_micros = c.target_spend.cpc_bid_ceiling_micros
if not curr_micros:
raise ValueError(f"Campaign '{c.name}' has no CPC ceiling set (uncapped); refusing to guess a starting value.")
step_micros = 50_000 if req.direction == "+0.05" else -50_000
new_micros = curr_micros + step_micros
if not CEILING_MIN_MICROS <= new_micros <= CEILING_MAX_MICROS:
raise ValueError(f"New ceiling {new_micros / 1e6:.2f} EUR is outside the 0.10-3.00 EUR safeguard.")
Budgets in ganzen Cent
Ein klassischer Gleitkommafehler: int(4.10 * 1_000_000) ergibt 4099999, und die API lehnt das als Betrag ab, der kein Vielfaches von 0,01 EUR ist. Die Umrechnung läuft deshalb zentral im Client und rundet auf ganze Cent.
def eur_to_micros(amount_eur: float) -> int:
"""
Converts EUR to micros rounded to whole cents.
int(4.10 * 1_000_000) == 4099999, which the API rejects as not a multiple of 0.01 EUR.
"""
return int(round(amount_eur * 100)) * 10_000
Das Gate vor allen Tools
Die Funktion ist bewusst klein. Sie gibt den Ablehnungstext zurück, solange das Gate geschlossen ist und das Tool nicht ausgenommen wurde, sonst nichts. Alles Weitere, also was geprüft wird und wann das Gate aufgeht, steckt im Preflight-Check weiter unten.
def refusal_for(function: str) -> Optional[str]:
"""The refusal text when `function` must not run yet, else None."""
if not required() or is_open() or function in PREFLIGHT_EXEMPT:
return None
return REFUSAL
Tracing und Analyse nach dem Agentenlauf: der geschlossene Kreislauf
Leitplanken vor dem Aufruf reichen nicht. Ich will auch nach dem Lauf wissen, was der Agent getan hat, wie gut die Tools liefen und ob jemand am Agenten vorbei etwas geändert hat. Das Ergebnis dieser Prüfungen steuert den nächsten Lauf. So schließt sich der Kreislauf:
↓ OK öffnet das Gate
Der Agent arbeitet über die Tools
↓ jeder Aufruf wird getraced und im Journal vermerkt
Auswertung nach dem Lauf: Tool-Gesundheit, Prozess-Evaluierung, Abgleich mit der Change History
↓ die Ergebnisse stehen im Journal
Der nächste Preflight-Check liest die Auswertungen
Jeder Tool-Aufruf ist ein Trace
Ein OpenTelemetry-TracerProvider mit Cloud-Trace-Exporter (shield/cloud_sink.py) erzeugt pro Tool-Aufruf einen Span mit dem Namen tools/call <tool> und dem Attribut gen_ai.tool.name. Die gRPC-Instrumentierung hängt für jeden Google-Ads-RPC darin einen Kind-Span an, gefiltert auf Aufrufe an Google Ads. Der Agent kann einen Trace selbst mit get_cloud_trace öffnen.
Strukturierte Logs, mit Traces verknüpft
Jeder Aufruf schreibt eine JSON-Zeile: Zeitstempel, Tool, action_type (READ oder MUTATION), Argumente, Erfolg, error_code, latency_ms, sla_status, google_ads_api (Aufrufe, Millisekunden, fehlgeschlagen, wiederholt, Methoden), Request-IDs, Trace-ID und Span-ID, session_id und Operator. Secrets werden vor dem Schreiben geschwärzt. Die Zeilen werden nach Cloud Logging gespiegelt: NOTICE für eine erfolgreiche Änderung, WARNING für eine Ablehnung durch das Eingabeschild, ERROR für einen Fehler auf Googles Seite. Mit get_cloud_logs liest der Agent seine eigenen Logs.
Nach dem Lauf: Tool-Gesundheit
analyze_tool_health markiert ein Tool als langsam, wenn p95 über 2500 ms liegt und mindestens 3 Messwerte vorliegen. Einen Fehlersprung meldet es bei mindestens 3 Fehlern, mindestens dem Doppelten des vorherigen Fensters und mindestens 10 Prozent Fehlerrate. Jeder Aufruf über 2500 ms wird als DEGRADED markiert.
Nach dem Lauf: deterministische Prozess-Evaluierung (4 Regeln)
evaluate_agent_process ist eine deterministische Guardrail-Evaluierung über das Journal, ohne LLM als Richter. Vier Regeln:
- READ_FIRST: Die Sitzung muss
get_change_historylesen, bevor sie die erste Änderung macht. - ONE_LEVER: Pro Testreihe nur eine Art von Änderung (Änderungen innerhalb von 10 Minuten bilden eine Reihe), damit Ergebnisse zuordenbar bleiben.
- AUTO_FIX_ALONE:
auto_fix_collisions_and_dupespausiert Keywords ohne Probelauf und darf deshalb nie zusammen mit anderen Änderungen laufen. - VERIFY_AFTER:
verify_changes_loggedmuss zwischen 15 Minuten und 24 Stunden nach der letzten Änderung laufen, weil Googles Change History etwa 15 Minuten nachläuft.
Änderungen am Agenten vorbei
verify_changes_logged gleicht Googles change_event-Historie mit den MUTATION-Zeilen des Journals ab. Eine Änderung ohne passenden MCP-Aufruf wird gemeldet, egal ob sie von einem Skript, einem anderen Agenten oder einem Menschen in der Oberfläche stammt.
Vor dem nächsten Lauf: der Preflight-Check
preflight_log_check führt TOOL_HEALTH, OUTSIDE_CHANGES und AGENT_PROCESS aus. Bei OK öffnet sich das Gate. Bei ATTENTION bleibt jedes Anzeigen-Tool geschlossen, bis ich die Befunde geprüft habe. Damit entscheidet das Ergebnis der letzten Auswertung, ob der Agent überhaupt etwas ändern darf.
Ein echter Befund vom 05.10.2026
Zwei der drei Prüfungen lasen das Cloud-Journal (Beginn 05.10.), die dritte das lokale Journal (Beginn 01.10.). Dadurch übersah AGENT_PROCESS fünf Verstöße. Die Korrektur: Alle drei Prüfungen wählen die Journal-Quelle jetzt auf dieselbe Weise.
Journal-Schnappschuss 01.10.-13.10.2026
Stand 13:12 UTC am 13.10.2026:
| Kennzahl | Wert |
|---|---|
| Aufrufe | 237 in 15 Sitzungen |
| Lesend / Änderungen | 216 lesend, 21 Änderungen |
| Mit Trace-ID | 236 von 237 |
| Vom Schild abgelehnte Änderung | 1 |
| Latenz | Median 1,3 s, p95 5,6 s (37 Aufrufe als DEGRADED markiert) |
| Wiederholungen | 0 |
Vorfälle, die die Leitplanken abgefangen haben
Manche dieser Vorfälle hat ein Schutz abgefangen, andere waren Fehler in meinem eigenen Code. Jeder hat eine Regel hinterlassen.
- 04.08.2026Ein Helfer für Feldmasken verwarf still ein Feld mit dem Wert False. Google akzeptierte ein Update, das nichts änderte. Aufgefallen ist es beim manuellen Zurücklesen, das Update ging erneut raus, diesmal mit expliziter Maske.Regel: Sparse Update Masks auf allen Schreibpfaden.
- 01.10.2026Der RSA-Deploy pausierte alle Anzeigen, sobald 2 oder mehr existierten, und konnte die erste Headline verlieren.Regel: Headline 1 ist die DKI-Headline, höchstens 3 aktive RSAs, die älteste wird im selben Request pausiert.
- 01.10.2026Das Gebotstool änderte Gebote der Anzeigengruppe, die Maximize Clicks ignoriert.Regel: Es steuert jetzt die CPC-Obergrenze der Kampagne.
- 05.10.2026Eine Änderung der Obergrenze mit fehlerhafter Richtung lehnte das Pydantic-Schild als
REQUEST_REJECTEDab. Google wurde nie aufgerufen.Regel: bestätigt, das Eingabeschild hält. - 05.10.2026Eine nicht numerische Kampagnen-ID erreichte eine GAQL-Abfrage (BAD_NUMBER).Regel: Ein ID-Validator (nur Ziffern) läuft vor jedem lesenden Aufruf.
- 05.10.2026Die Abweichung der Journal-Quelle im Preflight (siehe oben).Regel: Alle drei Prüfungen wählen die Quelle gleich.
- 05.10. bis 12.10.2026Drei PREFLIGHT_REQUIRED-Ablehnungen: Das Gate stoppte die Tools, bis die Log-Prüfung bestanden war.Regel: bestätigt, das Gate hält.
- 13.10.2026Das Keyword-Tool lehnte eine Anfrage mit 25 Keywords (Limit 20) und einem nicht unterstützten Ländercode ab, bevor es Google aufrief.Regel: bestätigt, das Eingabeschild hält.
Ergebnisse im Live-Konto
Alle Zahlen stammen aus dem Google-Ads-Konto von MoonBloom, jeweils mit ihrem Zeitfenster. Die beiden CPC-Fenster sind nicht deckungsgleich: Im ersten liefen 2 Kampagnen, im zweiten alle 6 ausgabenwirksamen.
| Kennzahl | Wert | Zeitfenster oder Bezug |
|---|---|---|
| Durchschnittlicher CPC, vorher | 0,82 EUR | 05.07.-04.08.2026, 2 Kampagnen |
| Durchschnittlicher CPC, nachher | 0,47 EUR (276,66 EUR bei 592 Klicks) | 18.08.-17.09.2026, alle 6 ausgabenwirksamen Kampagnen |
| Tagesausgaben | etwa 8,92 EUR pro Tag über DE, AT, ES und FR | 18.08.-17.09.2026 |
| Analysierte Suchanfragen | 909 | Befund aus der Kontoprüfung |
| Gefundener Verlust | 22,07 EUR (11 Klicks, 0 Verkäufe), Anzeigengruppe pausiert | 04.08.2026 |
| Doppelte Keywords | 36 pausiert | Befund aus der Kontoprüfung |
Die Verkäufe laufen über Etsy, Google Ads rechnet sie deshalb nicht zu. Ich mache hier keine Aussagen zu ROAS oder Umsatz, sondern nur zu Klickpreis und entfernter Verschwendung.
Die Sicht ohne Code: Google Ads Kosten senken mit einem KI-Agenten →
Häufige Fragen
Was ist ein Google Ads MCP Server?
Warum nicht den offiziellen Google Ads MCP Server von Google nutzen?
Wie verhindere ich, dass ein KI-Agent in Google Ads zu viel ausgibt?
Wie verfolge ich, was ein KI-Agent getan hat?
Wie erstelle ich einen MCP Server in Python mit FastMCP?
Kann Claude Code Google Ads verwalten?
Verwendete Tools
Ich entwerfe Tool-Oberflächen für KI-Agenten, die gegen echte APIs arbeiten. Der interessante Teil ist, welche Operationen man freigibt und welche Regeln im Code stehen. Gern spreche ich Ihr System mit Ihnen durch.