درس 1 — MCP Google Sheets avec OAuth par utilisateur

MCP Google Sheets pour Gemini Enterprise (OAuth, Cloud Run, Terraform)

12 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

Un serveur MCP qui donne à Gemini Enterprise une interaction native et complète avec Google Sheets : rechercher, lire, écrire, ajouter des lignes, créer des classeurs, gérer les onglets, mettre en forme, et toute l'API Sheets via batch_update. Chaque action est faite au nom de l'utilisateur connecté, avec son propre compte Google (OAuth).

Exemples dans Gemini Enterprise :

  • « Trouve le classeur Ventes 2026 et résume l'onglet Ventes »
  • « Ajoute une ligne Sac à dos, 39 € dans Ventes »
  • « Crée un classeur Inventaire Lille avec les onglets Rayon et Réserve, puis mets la ligne d'en-tête en gras »
  • « Trie l'onglet Ventes par prix décroissant »

Contexte : ce sous-projet complète le projet 04 (MCP sur Cloud Run + Agent Registry) et s'intègre au socle de sécurité du projet 06.


Sommaire

  1. Architecture : comment la connexion fonctionne
  2. Sécurité : les contrôles, et pourquoi
  3. Les 17 outils
  4. Mise en place pas à pas
  5. Tester
  6. Exploiter
  7. Dépannage
  8. Fichiers

1. Architecture : comment la connexion fonctionne

 Utilisateur (alice@acme.example)
     │  1ère utilisation : « Autoriser Google Sheets » → écran de consentement Google (une fois)
     ▼
 Gemini Enterprise ── stocke le jeton OAuth d'Alice (et son refresh token), le renouvelle seul
     │
     │  POST https://sheets-mcp-xxxx.europe-west1.run.app/mcp
     │    X-Serverless-Authorization: <jeton du service Gemini Enterprise>   → vérifié par Cloud Run (IAM)
     │    Authorization: Bearer <jeton OAuth d'Alice>                       → vérifié par le serveur MCP
     ▼
 Cloud Run « sheets-mcp » (PRIVÉ, aucun droit propre)
     │  vérifie le jeton d'Alice : émis pour NOTRE client OAuth, scope Sheets, e-mail @acme.example
     │
     │  appelle l'API avec le jeton d'Alice  →  Alice ne voit et ne modifie que SES fichiers
     ▼
 Google Sheets API / Drive API

Deux jetons, deux rôles :

En-têteContientVérifié parGarantit
X-Serverless-AuthorizationJeton signé par Google du service agent Gemini EnterpriseCloud Run (IAM roles/run.invoker)Seul Gemini Enterprise peut atteindre le serveur
Authorization: Bearer …Jeton OAuth de l'utilisateurLe serveur (CompanyGoogleTokenVerifier)L'utilisateur est authentifié, appartient au domaine, et a consenti au scope Sheets

Gemini Enterprise ajoute le premier en-tête automatiquement pour les URL *.run.app, et transmet le second intact (documentation Google). Le serveur reste donc privé, sans clé API ni secret côté Cloud Run.


2. Sécurité : les contrôles, et pourquoi

#ContrôlePourquoiOù
1Cloud Run privé, run.invoker réservé au service agent Gemini EnterpriseLe serveur n'est pas joignable depuis Internet, même avec un jeton Google valideterraform/main.tf (§5)
2Délégation stricte : toutes les API sont appelées avec le jeton de l'utilisateurLe serveur n'a aucun droit propre (compte de service sans rôle). Impossible d'accéder à un fichier que l'utilisateur ne peut pas ouvrirserver/main.py (google())
3Vérification de l'audience du jeton (aud = notre client OAuth)Empêche le « confused deputy » : un jeton émis pour une autre application ne peut pas être rejoué iciGoogleTokenVerifier(audience=…)
4Scope Sheets obligatoire, e-mail vérifié et domaine autorisé (acme.example)Les comptes Gmail personnels et les autres organisations sont refusés, même s'ils obtiennent un jetonCompanyGoogleTokenVerifier
5Écran de consentement OAuth « Interne » + paramètre hd=acme.exampleSeuls les comptes de l'organisation peuvent se connecter ; le sélecteur de compte propose directement le compte professionnelConsole, étape 1
6Annotations MCP : readOnlyHint (lecture), destructiveHint (écrasement, suppression)Gemini Enterprise exécute les lectures sans friction et demande confirmation avant toute écriture destructiveChaque @mcp.tool
7Journal d'audit structuré : utilisateur, outil, classeur, plage. Jamais le contenu des cellulesTraçabilité sans exposer les donnéesaudit() → Cloud Logging
8Garde-fou de volume : 20 000 cellules max par lecture (truncated: true au-delà)Évite d'envoyer un classeur entier au modèleMAX_CELLS_PER_READ
9Démarrage refusé sans audience configuréeUne erreur de configuration ne peut pas produire un serveur qui accepte n'importe quel jetonmain.py, début
10Scopes minimaux : spreadsheets + drive.metadata.readonly (recherche par nom uniquement)Pas d'accès au contenu des autres fichiers DriveFormulaire Gemini Enterprise

Pourquoi pas un compte de service avec délégation domaine (DWD) ? Ce serait plus simple à configurer, mais le serveur pourrait alors lire n'importe quel classeur de l'entreprise. Avec OAuth par utilisateur, le serveur n'a aucun pouvoir propre : les droits Google Drive de chacun font foi. C'est le bon modèle pour un assistant ouvert à tous.


3. Les 17 outils

OutilRôleType (confirmation)
whoamiUtilisateur connecté (test de la connexion)Lecture
search_spreadsheetsTrouver des classeurs par nom (Mon Drive + Drive partagés)Lecture
get_spreadsheet_infoTitre, lien, onglets (taille, lignes figées)Lecture
read_rangeLire une plage (valeurs affichées, brutes ou formules)Lecture
read_rangesLire jusqu'à 20 plages en un appelLecture
find_in_sheetTrouver les cellules contenant un texteLecture
write_rangeÉcrire en remplaçant une plage (formules acceptées)Destructif
append_rowsAjouter des lignes à la fin d'un tableauÉcriture
clear_rangeEffacer les valeurs d'une plageDestructif
create_spreadsheetCréer un classeur (avec onglets)Écriture
add_sheet / rename_sheetAjouter / renommer un ongletÉcriture
delete_sheetSupprimer un onglet et son contenuDestructif
insert_dimensionInsérer des lignes ou colonnesÉcriture
delete_dimensionSupprimer des lignes ou colonnesDestructif
format_rangeGras, italique, couleurs, format de nombre, alignementÉcriture
batch_updateToute l'API Sheets : tri, filtres, fusion, mise en forme conditionnelle, graphiques, validation, protection…Destructif

Les plages s'écrivent en notation A1 : Ventes!A1:D20, 'Mon onglet'!A:A, B2.

Recommandation au lancement : dans Gemini Enterprise, activez d'abord les 6 outils de lecture, puis append_rows, create_spreadsheet et format_range. Activez les outils destructifs une fois les usages observés.


4. Mise en place pas à pas

Étape 1 : créer le client OAuth (console, une fois)

Dans le projet GCP de Gemini Enterprise : APIs et services → Google Auth Platform (ou Écran de consentement OAuth).

  1. Image de marque (Branding) : nom de l'application Gemini Enterprise - Google Sheets, e-mail d'assistance, domaine autorisé acme.example.
  2. Audience : Interne. Seuls les comptes de l'organisation peuvent se connecter, et aucune validation Google n'est nécessaire.
  3. Accès aux données (scopes) : ajoutez openid, .../auth/userinfo.email, .../auth/spreadsheets, .../auth/drive.metadata.readonly.
  4. Clients → Créer un client : type Application Web, nom gemini-enterprise-sheets-mcp, URI de redirection autorisé : https://vertexaisearch.cloud.google.com/oauth-redirect.
  5. Notez le Client ID (pour Terraform et Gemini Enterprise) et le Client secret (pour Gemini Enterprise uniquement : ne le mettez ni dans Terraform ni dans Git).

Le client OAuth ne peut pas être créé par Terraform pour des comptes Google (pas de ressource dans le provider) : c'est une étape console documentée.

Étape 2 : déployer le serveur (Terraform)

cd sheets-mcp/terraform
cp terraform.tfvars.example terraform.tfvars   # project_id, oauth_client_ids, allowed_domains
terraform init
terraform plan
terraform apply
terraform output mcp_server_url
terraform output gemini_enterprise_oauth_settings
terraform output egress_fqdns_to_allow

Ce que Terraform crée : les API (Cloud Run, Sheets, Drive), l'image, un compte de service sans rôle, le service Cloud Run privé, le droit d'appel pour le service agent Gemini Enterprise, et l'entrée Agent Registry avec la liste des 17 outils (générée depuis le code par scripts/gen_toolspec.py).

Étape 3 : politiques d'organisation (admin de l'organisation)

Gemini Enterprise bloque par défaut les serveurs MCP personnalisés. Dans IAM et administration → Règles d'administration, sur le projet :

  1. Autoriser la création de data stores « Custom MCP Server » (règle qui bloque les MCP personnalisés).
  2. Sources de données autorisées pour les connecteurs : ajouter custom_mcp.
  3. Domaines de sortie autorisés pour les connecteurs : ajouter les 3 domaines de terraform output egress_fqdns_to_allow (le domaine *.run.app du service, accounts.google.com, oauth2.googleapis.com).

Étape 4 : ajouter le serveur dans Gemini Enterprise

Console : Gemini Enterprise → Data stores → Créer un data store → Custom MCP Server → Add MCP server.

ChampValeur
AuthentificationOAuth 2.0
MCP server URLterraform output mcp_server_url (se termine par /mcp)
Authorization URLhttps://accounts.google.com/o/oauth2/v2/auth
Authorization URL parametersaccess_type=offline&prompt=consent&hd=acme.example
Token URLhttps://oauth2.googleapis.com/token
Client ID / Client secretceux de l'étape 1
Enable PKCE✅ activé
Scopesopenid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.metadata.readonly
Localisationcelle de l'app (eu)
Nom du data connectorGoogle Sheets

Pourquoi ces paramètres :

  • access_type=offline + prompt=consent : Google délivre un refresh token. Gemini Enterprise renouvelle le jeton seul, et l'utilisateur ne se reconnecte pas toutes les heures.
  • hd=acme.example : le sélecteur de compte propose directement le compte professionnel. Ce n'est qu'un confort : la vraie barrière est le contrôle de domaine côté serveur (contrôle n° 4).
  • PKCE : protège l'échange du code d'autorisation contre l'interception.

Puis :

  1. Actions : les outils apparaissent comme actions, désactivées par défaut. Activez-les (voir la recommandation au §3 ; 100 actions maximum).
  2. App → Connected data stores : ajoutez le data store Google Sheets à l'app.

Étape 5 : première utilisation (chaque utilisateur, une fois)

L'utilisateur demande par exemple « Liste mes classeurs Ventes ». Gemini Enterprise affiche Autoriser : écran de consentement Google (compte @acme.example, scopes listés), puis retour dans l'assistant. C'est terminé : les appels suivants sont directs.


5. Tester

5.1 Test de bout en bout, sans compte Google (déjà exécuté)

cd sheets-mcp
pip install -r server/requirements.txt
python tests/test_server.py

Le serveur tourne en HTTP comme sur Cloud Run. Un client MCP l'appelle avec Authorization: Bearer, et Google est simulé. Le test vérifie :

  • 4 refus : jeton invalide, jeton d'une autre application, compte hors domaine, scope absent ;
  • les 17 outils, et leurs annotations lecture / destructif ;
  • que chaque appel Google porte le jeton de l'utilisateur ;
  • la traduction des erreurs Google en messages clairs.

Résultat actuel : OK : tous les tests passent (23 appels Google simulés, tous avec le jeton de l'utilisateur).

5.2 Test réel en local, avec votre compte

# Un jeton avec les bons scopes (son audience = le client OAuth de gcloud)
gcloud auth application-default login \
  --scopes=openid,https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/spreadsheets,https://www.googleapis.com/auth/drive.metadata.readonly,https://www.googleapis.com/auth/cloud-platform
TOKEN=$(gcloud auth application-default print-access-token)
AUD=$(curl -s "https://oauth2.googleapis.com/tokeninfo?access_token=$TOKEN" | python3 -c 'import json,sys; print(json.load(sys.stdin)["aud"])')

# Serveur local qui accepte CETTE audience (uniquement en local, jamais en production)
OAUTH_CLIENT_IDS=$AUD ALLOWED_DOMAINS=acme.example python server/main.py &

# Appel MCP
curl -s -X POST localhost:8080/mcp -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"whoami","arguments":{}}}'

Vous pouvez aussi connecter MCP Inspector (npx @modelcontextprotocol/inspector) à http://localhost:8080/mcp en ajoutant l'en-tête Authorization: Bearer $TOKEN.

5.3 Recette dans Gemini Enterprise

TestAttendu
« Qui suis-je pour Google Sheets ? »Votre e-mail @acme.example
Lire un classeur partagé avec vousContenu résumé
Lire un classeur non partagé avec vous (ID connu)« accès refusé » : les droits Drive font foi
« Ajoute une ligne … »Ligne ajoutée
« Efface la colonne C »Demande de confirmation, puis effacement
Compte d'un autre domaineConnexion impossible (consentement interne) ; et refus serveur en défense
curl sans jeton sur l'URL Cloud Run403 (Cloud Run)

6. Exploiter

Journaux (Cloud Logging, ressource Cloud Run sheets-mcp) :

jsonPayload.event="tool_call"                      -- qui a utilisé quel outil, sur quel classeur
jsonPayload.event="auth_rejected"                  -- tentatives refusées (domaine, e-mail non vérifié)

Le contenu des cellules n'est jamais journalisé.

Révoquer un accès :

  • un utilisateur : myaccount.google.com → Sécurité → Applications tierces ;
  • un administrateur Workspace : Admin console → Sécurité → Contrôle des API → Accès des applications (bloquer le client OAuth) ;
  • tout le monde, d'urgence : désactiver le client OAuth (étape 1), ou retirer run.invoker au service agent.

Mettre à jour le serveur : modifier server/main.py, lancer python tests/test_server.py puis python scripts/gen_toolspec.py (liste des outils du registre), et terraform apply. L'image est reconstruite seulement si le code change.

Quotas : l'API Sheets impose des quotas par minute, par utilisateur et par projet. Le serveur traduit le 429 en message clair (« réessayez dans une minute »). Augmentez les quotas dans la console si l'usage le justifie.

Coût : min_instances = 1 garde une instance chaude (réponses immédiates, coût fixe modéré). 0 réduit le coût, au prix d'un démarrage à froid de quelques secondes.


7. Dépannage

SymptômeCause → solution
Gemini Enterprise : « impossible de se connecter au serveur »run.invoker manquant pour le service agent (terraform apply) ; ou URL sans /mcp ; ou domaine *.run.app non autorisé dans la politique de sortie
L'écran de consentement affiche « erreur redirect_uri_mismatch »L'URI https://vertexaisearch.cloud.google.com/oauth-redirect n'est pas dans le client OAuth
Tous les appels renvoient 401oauth_client_ids ne contient pas l'ID du client configuré dans Gemini Enterprise
Refus pour un utilisateur légitimeDomaine absent de allowed_domains, ou compte sans e-mail vérifié (journal auth_rejected)
search_spreadsheets : accès refuséScope drive.metadata.readonly absent : l'ajouter au client OAuth et dans Gemini Enterprise, puis se reconnecter
Reconnexion demandée toutes les heuresaccess_type=offline&prompt=consent absent des paramètres d'autorisation
Data store « Custom MCP Server » introuvable ou bloquéPolitiques d'organisation de l'étape 3
Réponse tronquée (truncated: true)Plage trop grande : lire une plage plus petite, ou ajuster MAX_CELLS_PER_READ

8. Fichiers

sheets-mcp/
├── server/
│   ├── main.py            ← le serveur : authentification, 17 outils, journal d'audit
│   ├── requirements.txt   ← fastmcp 4.0.10, uvicorn, httpx
│   └── Dockerfile
├── tests/test_server.py   ← test de bout en bout (HTTP réel, Google simulé)
├── scripts/gen_toolspec.py← génère terraform/toolspec.json depuis le code
└── terraform/
    ├── main.tf            ← APIs, image, compte sans rôle, Cloud Run privé, invoker, Agent Registry
    ├── variables.tf · outputs.tf · versions.tf · terraform.tfvars.example
    └── toolspec.json      ← les 17 outils publiés dans Agent Registry

Sources :