Skip to content

Changelog

  • schema_validate parcourt les nœuds JSON-LD @graph, y compris dans un tableau racine, sans compter le conteneur non typé comme un schéma valide.
  • applicationCategory et operatingSystem sont signalés comme propriétés recommandées de SoftwareApplication, sans invalider leur absence. La réponse distingue explicitement la présence de champs vérifiée localement de l’éligibilité Google aux résultats enrichis, non évaluée.
  • heading_audit ne signale plus TL;DR comme un titre vide.
  • Ajout d’un guide d’installation central, d’un guide Bing et de prompts par fournisseur avec des limites explicites sur l’indexation, les preuves et les mutations.
  • Publication d’un portail Starlight bilingue sur search-console.bruniaux.com/docs/ et /fr/docs/, avec recherche, navigation latérale, sommaire par page et liens de langue associés.
  • Les sources anglaises restent canoniques et sont publiées depuis une liste fermée. Les plans internes, exports machine-readable et rapports de validation bruts restent exclus du site.
  • La landing dirige désormais l’installation, la configuration Google et Bing, les exemples, l’architecture, les limites de preuve, le changelog et la licence vers les pages publiques du site.
  • Deux illustrations conceptuelles générées avec Gemini complètent des schémas HTML déterministes pour les flux de preuves et les actions protégées.
  • Les titres de la landing et des scénarios utilisent des formulations factuelles et des chiffres explicites. La FAQ pose des questions directes en anglais et en français.
  • Les pages documentaires publient désormais un hreflang="x-default" vers leur version anglaise, et le site expose un llms.txt public limité aux ressources destinées aux utilisateurs.
  • Surface portée à 81 tools. Elle ajoute 19 tools Bing Webmaster, dont 15 lectures et 4 écritures protégées, ainsi que compare_search_engines pour rapprocher les métriques Google et Bing sans fusionner leurs positions.
  • Les analyses quick_wins, seo_striking_distance et prune_candidates acceptent les données Bing lorsque les métriques requises sont présentes. Les analyses qui exigent des périodes exactes ou une dimension page-requête en masse refusent explicitement Bing.
  • 851 tests passent et 851 tests sont collectés sur ce checkout.
  • Les lectures Bing gardent leurs fenêtres observées, dérivent le CTR à partir des clics et impressions, et laissent les métriques propres au fournisseur dans provider_metrics.
  • Les quatre écritures Bing sont couvertes par des tests avec réponses simulées. Aucun appel de mutation Bing ou IndexNow n’a été exécuté pendant ce gate local, leur comportement runtime reste UNVERIFIED_RUNTIME.
  • Le README présente désormais gsc-mcp-tools==1.2.0 comme la première version publiée avec la surface Bing et relie le contrat API Bing.
  • Les références machine-readable, la description du paquet et les mots-clés de découverte sont alignés sur 81 tools, 851 tests et les fournisseurs Google/Bing.
  • La description et les topics GitHub mentionnent Bing Webmaster Tools, IndexNow et le SEO technique. Les labels de workflow des issues restent inchangés.
  • La configuration recommandée installe désormais l’outil une fois, utilise l’exécutable direct et limite Codex aux projets concernés afin d’éviter un processus uvx supplémentaire et un serveur global par tâche active.
  • La publication PyPI vérifie le tag, exécute les tests, contrôle le wheel construit puis utilise Trusted Publishing avec un jeton OIDC temporaire.
  • Les écritures OAuth utilisent un remplacement atomique pour éviter de laisser un fichier de jeton partiellement écrit.
  • Les soumissions IndexNow et Bing valident plus strictement le protocole, l’origine, les délimiteurs vides et les réponses du fournisseur.
  • Les comparaisons multi-moteurs conservent les structures sérialisables, les positions absentes et omettent les deltas non comparables.
  • Le point d’entrée module de la CLI et l’affichage des signes % dans l’aide fonctionnent à nouveau.
  • Les fenêtres Bing ne sont pas supposées exactes et égales aux fenêtres Google. compare_search_engines omet les deltas lorsque cette condition n’est pas prouvée.
  • GetKeywordStats et GetRelatedKeywords ne sont pas exposés après des réponses HTTP 400 lors du canari réel expurgé.
  • Les sémantiques total-versus-restant des quotas Bing, les crawl issues non vides, les lignes imbriquées de backlinks et RemoveFeed restent non vérifiées en production.
  • Une réponse de soumission acceptée ne prouve ni crawl ni indexation.

Dogfooding des 4 tools ajoutés en 1.1.0 sur une propriété réelle (cc.bruniaux.com, 400+ pages) : link_equity_map a échoué sur 3 pages des 25 ciblées.

internal_links_audit et link_equity_map traitaient un redirect 301/302 comme un échec de crawl

  • safe_fetch_html (url_safety.py) fixe volontairement follow_redirects=False : suivre un redirect avec le client httpx standard connecterait au serveur cible sans revalider son IP, ce qui contourne le DNS-pinning anti-SSRF. resp.raise_for_status() traite donc tout 3xx comme une erreur (httpx.HTTPStatusError), remontée comme échec de fetch.
  • Sur cc.bruniaux.com, Search Console référence encore 3 pages en http:// alors que le site sert du https:// (redirect 301 permanent, scheme uniquement, même host et même path). link_equity_map classait les 3 dans pages_failed au lieu de les crawler.
  • Fix : _fetch_following_redirects() dans links.py boucle sur safe_fetch_html (bornée à 5 sauts), en relisant l’en-tête Location sur l’exception httpx.HTTPStatusError interceptée. Chaque saut repasse par safe_fetch_html, donc chaque nouvelle cible est re-vérifiée par le DNS-pinning : un redirect vers une IP privée ou un endpoint de metadata cloud reste refusé au même titre qu’une requête directe. internal_links_audit et link_equity_map utilisent maintenant cette fonction ; les autres tools (page_technical_audit, schema_validate, sitemap_audit, …) gardent follow_redirects=False sans changement, un redirect y est un résultat d’audit à signaler, pas une erreur de fetch à masquer.
  • 4 tests ajoutés dans test_links.py : redirect http→https suivi avec succès, boucle bornée à 5 sauts (au-delà, fetch_error avec message explicite), redirect vers un hôte bloqué toujours refusé (SSRF préservé), page link_equity_map récupérée via redirect sortant de pages_failed. 611 tests passent (607 + 4), aucune régression sur les 32 tests existants de test_links.py.

Incident de production : tout uvx gsc-mcp-tools lancé sans lock depuis le 2026-07-28 (sortie de mcp 2.0.0) crashait à l’import, avant même l’ouverture du stdio MCP.

Dépendance mcp sans borne haute, résolue vers une v2 incompatible

  • pyproject.toml : mcp[cli]>=1.0.0 n’avait pas de borne haute. uv résolvait la dernière version publiée, mcp 2.1.1, qui a supprimé mcp.server.fastmcp.FastMCP (renommé MCPServer, changement d’API mentionné dans le message d’erreur du SDK). src/gsc_mcp/server.py:6 importe encore from mcp.server.fastmcp import FastMCP, donc le process mourait à l’import avec ModuleNotFoundError, avant toute négociation MCP. Côté client (Claude Code), ça remonte comme un simple CONNECTION_CLOSED, sans indiquer la cause.
  • Fix : mcp[cli]>=1.0.0,<2 dans pyproject.toml. Migration vers MCPServer non faite dans ce correctif, seul le pin de sécurité est appliqué.
  • Détecté en testant uvx gsc-mcp-tools en direct depuis une session externe, hors de tout environnement de dev avec un mcp 1.x déjà en cache.

61 tools (+4), 607 tests (+61). Maillage interne et structure de titres, déclenchés par un appel avec un consultant SEO externe (2026-08-06) : deux leviers de ranking sur lesquels aucun tool existant ne mesurait rien.

Links : nouveau module, 2 tools sans auth Google

  • internal_links_audit(url) : src/gsc_mcp/tools/links.py, nouveau module. _LinkParser (stack-based, gère les conteneurs imbriqués comme un <nav> dans un <header>) tague chaque <a href> avec la zone sémantique où il se trouve (body, nav, footer, header, aside ; role="navigation"/contentinfo/banner/complementary traités comme leurs balises équivalentes). Sortie principale : footer_only_targets, les cibles internes liées depuis la nav/footer/aside et depuis le corps d’aucune page. Détecte aussi les ancres génériques et vides (FR+EN), les liens internes en rel=nofollow, les self-links. Verdicts : healthy | issues_found | fetch_error.
  • link_equity_map(site, days=90, max_pages=25, delay_seconds=0.2) : prend les pages les plus vues dans GSC, les crawle avec le même parser zoné, construit le graphe de liens internes, et croise avec les positions. underlinked_striking_distance (position 11-20 sans lien entrant en corps de page) est le mouvement de ranking le moins cher disponible. Reporte aussi orphan_candidates, footer_only_targets (site-wide) et hub_pages. max_pages plafonné à 100 ; pages_crawled/pages_failed/coverage_note toujours retournés, aucune couverture silencieusement tronquée.

Content : heading_audit, un 4e tool sans auth

  • heading_audit(url) : _HeadingParser dédié dans content.py (le parser de drift.py ne couvre que h1-h3 et jette l’ordre du document ; son format de sortie est persisté dans les baselines drift, donc laissé intact). Vérifie l’unicité du H1, les sauts de niveau (H2 direct à H4), la duplication mot pour mot du title et du H1 (gâche un second angle sur le mot clé visé), les titres qui ne portent aucune information (liste noire FR+EN), et la densité de mots par H2. Verdicts : healthy | issues_found | fetch_error.

SEO : prune_candidates, garde-fou anti-suppression

  • prune_candidates(site, days=180) : classe les pages en has_traffic, impressions_no_clicks, low_impressions, zero_impressions sur 180 jours de données GSC, et ne retourne volontairement aucune liste « à supprimer ». Une page avec des clics ne peut jamais apparaître comme candidate, par construction. Cas de non-régression : tests/test_seo.py::test_prune_solar_panel_local_pages_are_protected, 300 pages locales courtes indexées et amenant du trafic, le genre de pages qu’un outil basé sur la longueur du contenu recommande de supprimer.
  • CLAUDE.md : nouvelle règle « No destructive recommendation without data », aucun appelant (agent ou skill) ne peut proposer une suppression, un noindex ou une consolidation sans avoir lu clics/impressions/statut d’indexation sur au moins 90 jours.

Détection de questions en français

  • cross.py : content_brief ne classait les requêtes-questions que sur who/what/when/where/why/how, un site francophone remontait donc systématiquement zéro question détectée. Ajout de 12 mots FR, des formes multi-mots (est-ce que), et des variantes sans accent.

4 skills projet + corpus de routage

  • .claude/skills/internal-linking-audit/, heading-audit/, link-equity-map/, onpage-audit/ : le dernier enchaîne les 4 nouveaux tools plus page_technical_audit, content_quality, schema_validate, content_brief et crux_page_vitals sur une URL, avec la règle anti-suppression appliquée à chaque recommandation.
  • .claude/routing-corpus/ : scénarios positifs/négatifs pour le routage BM25 des 4 nouveaux skills, au format lu par le corpus de routage global (~/.claude/hooks/routing/). Nécessite une version du routeur qui scanne aussi $CLAUDE_PROJECT_DIR/.claude/routing-corpus/ en plus du corpus global, changement hors dépôt.
  • scripts/routing-eval.js : recalcule les scores BM25 et les seuils de calibration sans toucher au cache du routeur global, pour mesurer les collisions entre cibles avant de committer un nouveau corpus.

Deux skills invisibles depuis leur création

  • .claude/skills/content-opportunities/SKILL.md : un : dans la description cassait le parsing YAML du frontmatter ; le skill retombait sur son titre H1 comme description et ne s’est jamais auto-déclenché correctement.
  • .claude/agents/gsc-content-optimizer.md : même cause, mais l’agent n’apparaissait pas du tout dans la liste des agents disponibles.

Déclenchement en français

  • 9 skills (ai-overviews-impact, cannibalization-check, content-opportunities, indexing-audit, page-deep-dive, schema-audit, seo-weekly-report, sitemap-audit, traffic-drop-diagnosis) et 9 agents gsc-* : descriptions enrichies avec des verbatims français réels pour le matching du modèle. Distinct du routage BM25, qui indexe des scénarios séparés, pas les descriptions.

  • README.md, pyproject.toml, src/gsc_mcp/cli.py, docs/machine-readable/llms.txt : comptes 57 → 61 tools, 546 → 607 tests, partout où ils apparaissaient (badges, docstrings, tableau des tools, module map, section decision tree).

57 → 61 tools. Test count : 546 → 607.

Durcissement suite à un scouting concurrentiel (6 MCP Google Ads/GA4/GSC analysés). Pas de nouveau tool.

Auth : fallback sur refresh token révoqué

  • auth.py : _get_oauth_creds catch désormais google.auth.exceptions.RefreshError autour de creds.refresh(Request()). Un refresh token révoqué ou expiré côté Google supprime le token en cache et retombe sur le flux de ré-auth normal, au lieu de lever la même exception à chaque appel jusqu’à intervention manuelle.
  • tests/test_auth.py : test_oauth_creds_refresh_error_falls_back_to_reauth couvre le nouveau chemin (token supprimé, fallback vers RuntimeError("No credentials...") quand GSC_CREDENTIALS_PATH est absent).

Documentation : protocole de confirmation pour les tools d’écriture

  • CLAUDE.md : nouvelle section décrivant les 4 étapes attendues des agents/skills appelants avant d’invoquer un tool qui mute un état externe (submit_url, submit_batch, sitemaps_delete, indexnow_submit, submit_sitemap) : état actuel, rayon d’impact, confirmation explicite, vérification post-appel. Convention d’appel documentée, pas de changement de comportement dans les tools eux-mêmes.
  • docs/machine-readable/llms.txt : section WRITE-TOOL PROTOCOL ajoutée en écho, plus une ligne sur le fallback RefreshError dans le bloc AUTH.
  • pyproject.toml : version 0.6.2 → 1.0.1 (rattrapage du drift : les waves 0.7.0 à 1.0.0 avaient mis à jour CHANGELOG.md sans bumper pyproject.toml). Description mise à jour pour refléter les 57 tools actuels au lieu des 43 d’origine.
  • README.md : badge tests 545 → 546, ligne “Latest” mise à jour.
  • Catégorisation centralisée des erreurs HTTP (403/404 → message actionnable) dans retry.py, sur le modèle du fork Google fourdots/Google-Marketing-MCPs. ai_overviews_impact (analytics.py:314) catch déjà HttpError au cas par cas pour transformer un 403 en réponse structurée ; généraliser dans retry.py aurait cassé ce pattern existant pour un gain flou.

57 tools (inchangé). Test count : 545 → 546.

57 tools (+3), 545 tests (+27). Wave C : trois nouveaux outils Technical + enrichissement schema_validate.

Technical : ai_visibility_audit

  • ai_visibility_audit(url) : lit {origin}/robots.txt via safe_fetch_html + parse urllib.robotparser. Vérifie 9 crawlers IA connus : GPTBot, Anthropic-ai, Claude-User, PerplexityBot, CCBot, Google-Extended, cohere-ai, Bytespider, OAI-SearchBot. Vérifie aussi la présence de {origin}/llms.txt (fichier MCP/discoverability). Verdicts : open (tous autorisés ou pas de robots.txt) | partial (certains bloqués) | closed (tous bloqués) | fetch_error.

Technical : gbp_deprecation_lint

  • gbp_deprecation_lint(url) : fetch via safe_fetch_html + scan regex sur 5 patterns de features GBP dépréciées : liens .business.site (GBP Websites sunset mars 2024), Reserve with Google (déprécié juin 2025), widget appointments GBP, Google Maps Reserve flow, GBP chat widget. Verdicts : clean | deprecated_found | fetch_error. Pas d’auth requise.

Technical : pagespeed_audit

  • pagespeed_audit(url, strategy="mobile") : appelle l’API PageSpeed Insights v5 via httpx. Retourne score Lighthouse performance, 6 métriques CWV (FCP, LCP, TBT, CLS, Speed Index, TTI), et 3 opportunités d’amélioration prioritaires. Requiert GOOGLE_API_KEY env var ; retourne verdict="missing_key" si absente. Verdicts : good (≥90) | needs_improvement (50-89) | poor (<50) | missing_key | fetch_error.

schema_validate : détection des types dépréciés pour les rich results

  • Ajout de _DEPRECATED_RICH_RESULTS : FAQPage (mai 2026), HowTo (sept 2023), ClaimReview, EstimatedSalary, VehicleListing, SpecialAnnouncement (juin 2025). Chaque schema détecté inclut désormais un champ deprecated_rich_result (string ou null).

  • registry.py : +3 tools. Docstring : 54 → 57.

  • properties.py : _ALL_TOOLS += 3 entrées. Docstring get_capabilities : 54 → 57.

  • tests/test_registry.py et tests/test_properties.py : compteurs 54 → 57.

  • README.md : badge tests 518 → 545, comptes 54 → 57.

54 → 57 tools. Test count : 518 → 545.

54 tools (+4), 518 tests (+51). Wave B de la seconde intégration claude-seo (MIT uniquement).

Content : preload_audit

  • preload_audit(url) : fetch via safe_httpx_get (retourne le httpx.Response complet, donc headers inclus, SSRF-safe). Détecte : blocs <script type="speculationrules"> parsés en JSON pour les actions prefetch/prerender, header HTTP Speculation-Rules (Chrome 122+), <link rel="preload"> avec extraction des attributs as/href/fetchpriority, <link rel="prerender"> déprécié (sunset Chrome 120), bloqueur bfcache cache-control: no-store. Génère une liste d’issues avec sévérité (high/medium/low) et check. Verdicts : optimised (prefetch+prerender + pas de bfcache killer) | improvements_available (règles présentes mais issues) | not_implemented (aucune Speculation Rules) | fetch_error.

CrUX : crux_lcp_subparts

  • crux_lcp_subparts(url, form_factor="PHONE") : même endpoint CrUX que crux_page_vitals, requête 5 métriques simultanément (LCP global + 4 subparts). Retourne lcp_p75_ms, lcp_rating, et subparts : ttfb_ms, resource_load_delay_ms, resource_load_duration_ms, render_delay_ms, dominant_phase (nom court du subpart avec la valeur p75 la plus élevée). Clé API absente = verdict="missing_key" (pas de RuntimeError, contrairement à crux_page_vitals). Verdicts : good | needs_improvement | poor | not_enough_data | missing_key | fetch_error.

Indexing : indexnow_submit

  • indexnow_submit(site, key, urls) : POST vers https://api.indexnow.org/indexnow. Protocole open source, consommé par Bing, Yandex, Seznam, Naver (pas Google). validate_url_strict sur chaque URL avant envoi (SSRF-safe), URLs invalides comptées dans skipped_invalid. Host extrait du paramètre site via urlparse. keyLocation dérivé automatiquement : {site}/{key}.txt. Sans @with_retry (pas une API Google). HTTP 200/202 = ok si aucun skip, partial si skips ; autres codes = error. Verdicts : ok | partial | error.

SEO : parasite_risk

  • parasite_risk(site, urls) : analyse pure de chemins URL, sans fetch HTTP. Détecte les patterns de la politique Google du 2024-11-19 sur le site-reputation abuse. Trois niveaux de risque par URL : high (/sponsored/, /affiliate/, /partner/, /brand-studio/, /paid-content/, /native-advertising/, sections commerce produit best-deals/top-picks), medium (/advisor/, /underscored/, /select/, /commerce/ d’après Forbes Advisor / CNN Underscored / WSJ), low (query params ?ref=, ?aff=, ?partner=). site_risk = risque maximum sur toutes les URLs. Verdicts : clean | at_risk | high_risk. Pas d’auth requise. Adapté de claude-seo (agricidaniel, MIT).
  • registry.py : import + enregistrement des 4 nouveaux tools. Docstring : 50 → 54 tools.
  • properties.py : _ALL_TOOLS += 4 entrées. Docstring get_capabilities : 50 → 54.
  • tests/test_registry.py et tests/test_properties.py : compteurs 50 → 54.
  • README.md : badge tests 467 → 518, comptes tools 50 → 54 (intro, section Tools, CLI, feature set), 4 nouvelles lignes dans le tableau.
  • docs/machine-readable/llms.txt : module map complété pour les 4 nouveaux tools, comptes 50 → 54 et 467 → 518, patch points ajoutés.

50 → 54 tools. Test count : 467 → 518.

50 tools (+3), 467 tests (+38). Wave A de la seconde intégration claude-seo (MIT uniquement).

Content : 3 nouveaux tools sans auth Google

  • content_quality(url) : fetch via safe_fetch_html + extraction du texte visible (stdlib html.parser, skip des blocs script/style/nav/footer). Score quatre axes : filler phrases (liste MIT de claude-seo, _AI_PATTERNS CC BY-SA 4.0 volontairement exclu), densité informationnelle (entités nommées + nombres pour 100 tokens), répétition de bigrammes, contenu thin (<300 tokens). Score global pondéré (filler 35%, densité 35%, répétition 20%, longueur 10%). Flags : filler, low-density, repetitive, thin-content. Verdicts : good | needs_work | thin_content | fetch_error.
  • hreflang_audit(url) : fetch + _MetaParser stdlib. Vérifie le self-referencing tag, la présence d’x-default, les codes ISO 639-1 (détecte jp→ja, eng→en à trois lettres), les régions ISO 3166-1 Alpha-2 (détecte UK→GB), la cohérence de protocole HTTP/HTTPS sur le set d’alternates. Audit de la page cible uniquement, les return tags bidirectionnels nécessitent un fetch séparé. Verdicts : valid | issues_found | no_hreflang | fetch_error.
  • page_technical_audit(url) : validate_url_strict + httpx.Client(follow_redirects=False). Audite : longueur du title (30-60), longueur de la meta description (50-160), directive meta robots (noindex = criticité haute), présence et cohérence du canonical, viewport, attribut lang sur <html>, trois security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy), détection de redirect (3xx + cible), accès Googlebot en robots.txt (fetch via safe_fetch_html + parse via urllib.robotparser.parse() stdlib pour rester dans la couche SSRF). Verdicts : healthy | issues_found | fetch_error.
  • src/gsc_mcp/tools/content.py : module dédié avec deux parsers stdlib (_TextExtractor et _MetaParser, héritant de html.parser.HTMLParser) et la constante _FILLER_PHRASES (35 patterns, adaptés de claude-seo, MIT, agricidaniel).
  • registry.py : import + enregistrement des 3 nouveaux tools (content_quality, hreflang_audit, page_technical_audit). Docstring : 47 → 50 tools.
  • properties.py : _ALL_TOOLS += 3 entrées. Docstring get_capabilities : 47 → 50.
  • README.md : badge tests 429 → 467, comptes tools 47 → 50 (intro, section Tools, CLI, paragraphe feature set), nouvelle famille “Content” dans le tableau.
  • docs/machine-readable/llms.txt : module map complété pour content.py, comptes 47 → 50 et 429 → 467, patch points de test ajoutés.

47 → 50 tools. Test count : 429 → 467.

47 tools (+4), 429 tests (+147). Intégration sélective d’assets MIT de AgriciDaniel/claude-seo.

Sécurité : module SSRF unifié

  • src/gsc_mcp/url_safety.py : protection SSRF et DNS-rebinding centralisée. Bloque les IPs privées/loopback/réservées, l’IPv4 obfusqué (décimal, hexadécimal, octal, FQDN avec trailing dot), les endpoints de métadonnées cloud (AWS IMDS 169.254.169.254, Azure 169.254.169.254/metadata, GCP metadata.google.internal, Oracle, Alibaba). DNS-pinning via patch de socket.getaddrinfo sous lock pour sécuriser les redirections. API publique : validate_url(), validate_url_strict(), safe_httpx_get(), safe_fetch_html(). Remplace les guards ad-hoc présents dans technical.py et sitemaps.py. Adapté de claude-seo (agricidaniel, MIT).

Technical : génération JSON-LD

  • schema_generate(schema_type, ...) : génère un bloc JSON-LD Schema.org pour quatre types à fort impact SEO. reservation (FoodEstablishmentReservation avec provider, start_time, party_size, customer), order_action (OrderAction avec merchant, order_url, delivery methods), discussion (DiscussionForumPosting avec headline, author, url, date_published, comment_count optionnel), profile (ProfilePage avec mainEntity Person, sameAs, knowsAbout, worksFor). Aucun appel réseau, aucune auth requise. Adapté de claude-seo (agricidaniel, MIT).

Drift monitoring : 3 nouveaux tools

  • drift_baseline(url, with_cwv) : capture un snapshot SEO d’une URL (title, meta description, robots, canonical, H1-H3, JSON-LD, OpenGraph, status HTTP, SHA-256 du HTML et des schemas). Stocké en SQLite via platformdirs.user_data_dir("gsc-mcp")/drift/baselines.db (WAL mode). CWV optionnel si CRUX_API_KEY est configuré.
  • drift_compare(url, with_cwv) : fetch live puis applique 17 règles de diff (méthodologie Dan Colta, MIT) : 8 CRITICAL (changement de title, H1, canonical, meta robots, suppression de schema, perte de status 200, régression LCP/CLS), 6 WARNING (ajout/suppression H2-H3, modification meta description, ajout noindex, dégradation INP), 3 INFO (modification OpenGraph, ajout schema, variation de longueur HTML > 20%). Verdict global : no_drift | drift_detected.
  • drift_history(url, limit) : liste les comparaisons stockées pour une URL avec le résumé des findings par run.

Documentation SEO sourcée

  • docs/seo-knowledge/comparison-rules.md : 17 règles de drift avec seuils et sévérités. Source : Dan Colta (MIT).
  • docs/seo-knowledge/cwv-thresholds.md : seuils LCP/INP/CLS/FCP/TTFB avec méthodologie de scoring (INP remplace FID).
  • docs/seo-knowledge/local-seo-signals.md : signaux 2026 Whitespark/BrightLocal/Sterling Sky pour le SEO local et l’AI search.
  • docs/seo-knowledge/serp-overlap-methodology.md : algorithme de clustering par overlap top-10 (Lutfiya Miller).

Attribution

  • NOTICE : copyright MIT de claude-seo (Copyright (c) 2026 agricidaniel), contribution de Dan Colta, liste des assets portés, exclusions explicites (CC BY-SA word lists).
  • README.md : section Credits pointant vers github.com/AgriciDaniel/claude-seo.
  • technical.py : _reject_ssrf interne remplacé par url_safety.validate_url_strict. Trois annotations Optional[list] corrigées en Optional[list[str]] pour la compatibilité CLI.
  • sitemaps.py : origin-check des URLs enfant rebranché sur url_safety.validate_url_strict.
  • cli.py : _type_kind() étend la détection aux annotations typing.Optional[X] (= typing.Union[X, None]) et bool. _build_subparser() gère le cas bool avec store_true/store_false. Corrigeait un sys.exit(1) silencieux qui cassait la construction des 47 parsers.
  • registry.py + properties.py : 4 nouveaux tools enregistrés (schema_generate, drift_baseline, drift_compare, drift_history).
  • CLI : 56 tests en échec causés par _type_kind() qui ne gérait pas typing.Optional ni bool (annotations des tools drift et schema_generate).
  • Tests test_technical.py + test_sitemap_audit.py : cible de mock DNS corrigée (gsc_mcp.url_safety.socket.getaddrinfo au lieu de gsc_mcp.tools.technical.socket.getaddrinfo).
  • test_technical.py : assertion SSRF corrigée ("192.168.1.1" in result["error"] au lieu de "Blocked").
  • test_properties.py + test_registry.py : compteurs hardcodés 43 → 47.

43 → 47 tools. Test count : 282 → 429.

  • README: “What’s New” section summarizing v0.6.0 and v0.6.1 highlights for users landing on the repo
  • README: Troubleshooting section covering the 6 most common setup issues (auth, GA4, CrUX, Indexing API quota, uvx launch)

Refactor interne et corrections sans nouveau tool.

  • Entrypoint gsc-mcp-tools manquant dans pyproject.toml : uvx gsc-mcp-tools échouait avec “An executable named gsc-mcp-tools is not provided by package gsc-mcp-tools”. Les deux noms (gsc-mcp et gsc-mcp-tools) pointent maintenant sur le même gsc_mcp.server:main
  • README : badge tests corrigé (286 → 282), compteur dev section corrigé (222+ → 282), tableau détaillé complété avec les 4 tools manquants (sitemap_audit, crux_page_vitals, crux_history, schema_validate), descriptions Cross traduites de FR en EN
  • Ponytail refactor (complexité sans valeur supprimée) :
    • auth.py : helper _ga4_creds() extrait pour éliminer la duplication entre get_ga4_service et get_alpha_ga4_service
    • analytics.py : _SEARCH_TYPES déplacé en variable locale dans search_type_breakdown (pas d’autres usages)
    • seo.py : helper _two_periods(days) ajouté pour déduplication du calcul de dates dans traffic_drops et seo_lost_queries
    • sitemaps.py : DefusedXmlException ajouté au except interne de _fetch_xml, bloc try/except externe supprimé
    • indexing.py : _submit_batch_impl fusionné dans submit_batch, couche de délégation supprimée
    • cross.py : dicts parallèles remplacés par de l’arithmétique directe dans la renormalisation de page_health_score
  • Tests : test_seo_v2.py et test_sitemaps_v2.py fusionnés dans test_seo.py et test_sitemaps.py respectivement, puis supprimés. test_scaffold.py supprimé (couvert par conftest et imports)
  • Test count : 268 → 282

7 nouveaux tools, 43 tools au total, 268 tests.

Analytics GSC : nouvelles variations search type

  • discover_performance(site, days, limit) : performances Google Discover par page. Utilise "type": "discover" dans le corps de requête. La dimension query n’est pas supportée par Discover, seul page est retourné. Trié par impressions décroissantes.
  • news_performance(site, days, limit) : identique à discover_performance mais pour Google News ("type": "googleNews").
  • search_type_breakdown(site, url, days) : 5 appels séquentiels à _fetch_rows (web, discover, googleNews, image, video), agrège clicks et impressions par type. Paramètre url optionnel pour filtrer sur une page spécifique.
  • ai_overviews_impact(site, days, limit) : requête avec "dimensions": ["query", "searchAppearance"] et "dataState": "all". Retourne un dict {"error": "AI_OVERVIEWS_NOT_AVAILABLE"} sur HttpError 400/403 (propriétés sans données AI Overviews) sans lever d’exception. Les erreurs 500+ restent remontées.

Cross GSC+GA4 : outils composites

  • page_health_score(site, url, property_id, hostname, country) : score composite 0-100 combinant 4 sources. GSC (30 pts via inspect_url), GA4 (25 pts via ga4_page_performance), CrUX (25 pts via crux_page_vitals, LCP+INP+CLS), et schema (20 pts via schema_validate). Chaque composant est isolé dans un try/except RuntimeError : si une source est absente (credentials manquants), ses points sont 0 et le score est renormalisé sur les sources disponibles.
  • content_brief(site, page_url, days, property_id) : intelligence éditoriale par page. Filtre les requêtes GSC sur page_url (normalisation via _normalize_url), trie par clicks, extrait les requêtes “question” (who/what/when/where/why/how) depuis la liste complète filtrée. Enrichit avec ga4_page_performance (sessions, engagement_rate) en dégradation gracieuse si GA4 est absent.

GA4 : funnel via v1alpha

  • ga4_funnel(steps, start_date, end_date, property_id) : rapport de funnel multi-étapes via AlphaAnalyticsDataClient.run_funnel_report(). Validation stricte : moins de 2 étapes retourne {"error": "INVALID_STEPS"}. Chaque étape est un dict {"name": str, "event": str}. Taux de conversion par étape relatif à l’étape 1 (qui est toujours null). Nouveau getter get_alpha_ga4_service() dans auth.py, même scope et token que la beta.
  • Tool count : 36 -> 43
  • Test count : 222 -> 268
  • get_capabilities docstring : “36” -> “43”, _ALL_TOOLS étendu avec les 7 nouveaux tools

4 nouveaux tools, filtres hostname/country sur tous les tools GA4, et 55 nouveaux tests. 36 tools au total, 222 tests.

GA4 : filtres hostname et country (tools existants)

  • _build_dimension_filter(hostname, country, base_filter) dans ga4.py : construit un FilterExpression seul ou un AND group (via FilterExpressionList) selon le nombre de filtres actifs. Backward-compatible : avec None, None, le comportement est identique à avant
  • Paramètres hostname: str | None = None et country: str | None = None ajoutés à ga4_organic_landing_pages, ga4_traffic_sources, ga4_page_performance, ga4_user_behavior, ga4_conversion_funnel
  • Paramètre hostname: str | None = None ajouté à ga4_realtime (country non disponible sur runRealtimeReport)
  • Mêmes paramètres propagés à traffic_health_check et page_analysis (passés à ga4_organic_landing_pages en interne)
  • 20+ nouveaux tests dans test_ga4.py et correction de test_pa_meta dans test_cross.py

CrUX : Core Web Vitals réels (nouveaux tools)

  • crux_page_vitals(url, form_factor) : interroge le Chrome UX Report API (:queryRecord). Retourne LCP, INP, CLS, FCP, TTFB avec ratings good/needs_improvement/poor, percentiles p75, et un verdict global good/needs_improvement/poor/not_enough_data. Nécessite CRUX_API_KEY
  • crux_history(url, form_factor) : séries temporelles hebdomadaires via :queryHistoryRecord. Retourne jusqu’à 25 semaines de p75 par métrique pour tracker les régressions CWV
  • CRUX_API_KEY : variable d’env distincte de l’auth GSC (Google API Key simple, pas service account). La Chrome UX Report API doit être activée dans le projet GCP
  • Nouvelle dépendance : httpx>=0.27.0
  • 16 nouveaux tests dans tests/test_crux.py

Sitemaps : audit de couverture (nouveau tool)

  • sitemap_audit(site, sitemap_url) : fetch un sitemap (ou sitemap index) via httpx, parse les URLs avec defusedxml.ElementTree (prévient XXE et billion-laughs), cross-référence contre 90 jours de GSC via get_search_analytics. Gère les sitemap index avec une récursion d’un niveau. Verdicts : empty | fetch_error | partial (>20% URLs absentes de GSC) | healthy
  • Protection SSRF : les child sitemaps d’un sitemap index sont validés contre l’origin du sitemap parent (follow_redirects=False)
  • Nouvelle dépendance : defusedxml>=0.7.1
  • 7 nouveaux tests dans tests/test_sitemap_audit.py

Technical : validation JSON-LD (nouveau tool)

  • schema_validate(url) : fetch n’importe quelle URL publique via httpx, extrait tous les blocs <script type="application/ld+json"> via html.parser (stdlib, pas de dépendance externe), valide les champs requis par type (Article, LocalBusiness, FAQPage, Product, WebSite, BreadcrumbList, SoftwareApplication…), et suggère des schemas manquants selon les patterns d’URL (/faq → FAQPage, /blog/ → BlogPosting, etc.). Verdicts : healthy | missing_schemas | invalid_schemas | fetch_error. Ne nécessite pas d’auth
  • 15 nouveaux tests dans tests/test_technical.py
  • Tool count : 32 → 36
  • Test count : 167 → 222
  • get_capabilities docstring : “32” → “36”, _ALL_TOOLS étendu avec les 4 nouveaux tools
  • pyproject.toml description mise à jour pour refléter les 36 tools et les nouvelles catégories
  • Tous les tools GA4 (ga4_organic_landing_pages, ga4_traffic_sources, ga4_page_performance, ga4_realtime, ga4_user_behavior, ga4_conversion_funnel) et les tools cross (traffic_health_check, page_analysis) acceptent un paramètre optionnel property_id: str = None. Quand fourni, il override GA4_PROPERTY_ID sans modifier la config. Permet de requêter plusieurs properties GA4 depuis une seule instance MCP.
  • get_ga4_property_id(override=None) dans auth.py : accepte un override direct, court-circuite la lecture de l’env var. Sans override, comportement identique à avant.
  • 4 nouveaux tests : test_get_ga4_property_id_override_takes_precedence, test_get_ga4_property_id_override_no_env_needed, test_thc_property_id_propagated, test_pa_property_id_propagated

Corrections de cohérence et consolidation interne : pas de nouveaux tools.

  • get_capabilities retournait 18 tools sur les 32 réellement disponibles. Les 14 manquants (analytics_anomalies, seo_striking_distance, seo_cannibalization, seo_lost_queries, sitemaps_delete, sitemaps_get, 6 tools GA4, traffic_health_check, page_analysis) sont maintenant listés
  • inspect_url, batch_url_inspection, check_indexing_issues appelaient webmasters/v3, qui n’expose pas urlInspection. Ces trois tools levaient une AttributeError au runtime sur chaque appel. Corrigé en passant sur searchconsole/v1 (API qui expose la ressource urlInspection)
  • traffic_health_check et page_analysis incluent maintenant un champ note dans leur réponse JSON pour avertir que GSC a un décalage de 3 jours vs GA4, les ratios sont donc approximatifs
  • ga4_organic_landing_pages ajoute un champ note quand le nombre de résultats atteint la limite, signalant une troncature potentielle
  • Tous les tools GSC (analytics, SEO, sitemaps, properties) consolidés sur searchconsole/v1. Le client webmasters/v3 (get_gsc_service) est supprimé de auth.py. searchconsole/v1 expose les mêmes ressources sites, searchanalytics, sitemaps en plus de urlInspection
  • Couverture quick_wins améliorée : 4 nouveaux cas couvrant les pages à CTR zéro avec impressions suffisantes, l’exclusion sous le seuil d’impressions et l’exclusion des pages déjà au benchmark de CTR

Phase 3: 2 tools cross-platform GSC+GA4, nouveau module cross.py.

  • traffic_health_check(site, days): compare les clics GSC agrégés avec les sessions organiques GA4 pour détecter les écarts de tracking. Retourne un statut parmi no_gsc_data, tracking_gap (ratio < 0.6), filter_issue (ratio > 1.3) ou healthy. GA4 interrogé avec limit=10000 pour éviter les sous-comptages sur les gros sites
  • page_analysis(site, days, limit): jointure page par page entre GSC (dimensions=[“page”]) et GA4 (landing pages organiques). Les pages présentes dans une seule source sont incluses avec les champs manquants à None. Chaque page reçoit un opportunity_score = log10(impressions+1)*10 + engagement_rate*100 + log10(conversions+1)*20, trié décroissant, tronqué à limit
  • _normalize_url(url) helper interne: ramène URLs absolues (GSC) et paths GA4 au même chemin nu, sans scheme, host, query ni slash final, pour fiabiliser la jointure
  • engagement_rate dérivé dans cross.py comme engaged_sessions/sessions (formule GA4 native), sans modifier la sortie de ga4_organic_landing_pages
  • 24 nouveaux tests dans tests/test_cross.py, dont les boundaries 0.6 et 1.3 du ratio, les cas GSC-only, GA4-only, trailing slash et query string
  • Tool count: 30 vers 32

Phase 2: 6 new GA4 tools, new dependency, new environment variable.

  • ga4_organic_landing_pages(start_date, end_date, limit): sessions, engaged sessions, bounce rate, average session duration, conversions and revenue for organic landing pages. Uses the sessionMedium=organic filter on landingPagePlusQueryString
  • ga4_traffic_sources(start_date, end_date): sessions and conversions broken down by channel group, source and medium
  • ga4_page_performance(start_date, end_date, page_path): 7 metrics per page path (page views, active users, average session duration, engagement rate, bounce rate, conversions, revenue). Optional page_path parameter adds a CONTAINS filter
  • ga4_realtime(): active users right now, by screen name, country and device. No date range, uses run_realtime_report directly
  • ga4_user_behavior(start_date, end_date): single batch_run_reports call returning three breakdowns (by device, by country top 20, by user type new/returning)
  • ga4_conversion_funnel(start_date, end_date, event_name): two sequential run_report calls. First lists pages with conversions > 0; second lists events, optionally filtered by exact event name
  • New dependency: google-analytics-data>=0.18.0 (Google Analytics Data API v1beta, protobuf-based client)
  • New environment variable: GA4_PROPERTY_ID (numeric property ID, e.g. 123456789). Validated lazily on first GA4 tool call, never at startup. GSC-only users are not affected
  • get_ga4_service() and get_ga4_property_id() in auth.py, reusing the same _resolve_creds path as GSC and Indexing clients
  • Tool count: 24 → 30

Phase 1: 6 new tools, no new dependencies.

  • seo_striking_distance(site, days, min_impressions): queries in positions 8-15 sorted by impressions desc. Separate band from quick_wins (4-15), intended for queries one push away from page 1
  • seo_cannibalization(site, days, min_impressions): detects queries split across multiple pages using an HHI conflict score (1 - sum(share²)). Zero-click groups use uniform 1/n fallback to avoid division by zero. Filters on per-query total impressions, not per-page
  • seo_lost_queries(site, days): flags queries with a click drop ≥ 80% vs the previous period, requiring at least 5 previous clicks. Iterates over the previous period to catch fully-vanished queries. Same two-window no-lag pattern as traffic_drops
  • analytics_anomalies(site, days, threshold): Z-score anomaly detection on daily clicks via statistics.pstdev. Returns anomalies = [] when std is zero (constant or all-zero series) to handle low-traffic sites safely
  • sitemaps_delete(site, sitemap_url): deletes a sitemap with a safety check before any API call (URL must end with .xml or contain /sitemap, raises ValueError otherwise)
  • sitemaps_get(site, sitemap_url): fetches a single sitemap resource and normalises it to the same flat shape as list_sitemaps (warnings and errors coerced to int)
  • 62 new tests (35 SEO, 15 sitemaps, 9 analytics anomalies): all mocked, no API calls
  • Tool count: 18 → 24

Initial release.

  • 18 MCP tools across 6 categories: meta, properties, analytics, SEO, inspection, indexing, sitemaps
  • submit_batch using true HTTP multipart batch via new_batch_http_request(), chunked at 100 URLs per request. Avoids the late-binding closure bug with a _make_callback(url) factory pattern
  • Dual OAuth scope architecture: separate clients for GSC (auth/webmasters) and Indexing API (auth/indexing), because the Indexing API rejects webmasters tokens
  • OAuth flow with token stored as JSON (google.oauth2.credentials.Credentials.to_json()) instead of pickle
  • Service Account support as an alternative to OAuth (set GSC_SERVICE_ACCOUNT_PATH)
  • Exponential backoff retry on HTTP 429/500/502/503/504 via with_retry() decorator, no retry on 404
  • In-memory quota tracker (QuotaTracker) with configurable limit and warn threshold (warns at 180/200 by default)
  • with_meta() wrapper on all tool outputs: every response includes a _meta block with tool name and call parameters, so Claude has full context on what was fetched
  • quick_wins tool scoring CTR opportunity vs benchmark by SERP position
  • check_indexing_issues categorizing URLs into not_indexed, robots_blocked, fetch_error, canonical_issue, indexed
  • traffic_drops diagnosing drops as ranking_loss, ctr_collapse, or demand_decline
  • Full test suite: 52 tests, fully mocked, no Google API calls required