Aller au contenu

Cluster Operator MCP

Pour l'exploitation ou le dépannage sans client MCP ni IA, voir le Manuel opérateur — Gateway MCP et Task Runtime.

Élément Valeur
Namespace cluster-observer
Image ghcr.io/containers/kubernetes-mcp-server:v0.0.67@sha256:e47f6ccc5347e1ea6be495d8d9ab55ac8a995fe87800fb984fa770cebe14ad44
Réplicas 1
Transport Streamable HTTP sur le Service ClusterIP cluster-observer:8080
Exposition Cloudflare Tunnel + Access Service Auth par agent ; Secure MCP Tunnel OpenAI pour ChatGPT ; porte Bearer dédiée mcp-mammouth.djayzone.com pour Mammouth AI
Autorisations Observation en liste blanche ; patch uniquement sur workloads existants et objets Flux, sans création/suppression

Usage actuel

Le workload fournit un accès MCP de diagnostic et d'exploitation ciblée au cluster sans distribuer de kubeconfig administrateur. Le Service reste ClusterIP : les accès distants passent par les tunnels authentifiés prévus par les manifests actifs.

Les barrières restent cumulatives : outils de suppression, scale et exécution désactivés, ressources sensibles refusées, RBAC sans Secret/ConfigMap/RBAC et NetworkPolicy limitée à CoreDNS et à l'API Kubernetes. La lecture de logs est explicitement bornée à la sous-ressource pods/log avec le seul verbe Kubernetes get.

Lecture des logs

Le serveur amont v0.0.67 expose l'outil natif pods_log avec les paramètres name, namespace, container, tail et previous. Le besoin fonctionnel pods_logs(namespace, pod, container, tailLines, sinceSeconds, previous) est couvert par correspondance pod -> name et tailLines -> tail.

sinceSeconds et le renommage exact de l'outil en pods_logs ne sont pas supportés par cette version du serveur ; ils nécessitent une évolution amont ou une couche MCP dédiée. Le connecteur ne simule pas ces paramètres.

Actions opérateur ciblées

L'outil amont resources_create_or_update peut mettre à jour une ressource existante. Le ServiceAccount ne possède aucun verbe create ou delete : les mutations sont limitées à patch sur :

  • Deployment, StatefulSet et DaemonSet, pour actualiser spec.template.metadata.annotations["kubectl.kubernetes.io/restartedAt"] ;
  • Kustomization, GitRepository, HelmRepository et HelmRelease, pour actualiser metadata.annotations["reconcile.fluxcd.io/requestedAt"] ;
  • HelmRelease, pour reconcile.fluxcd.io/forceAt uniquement lorsqu'un force reconcile est explicitement demandé.

L'agent lit d'abord le manifeste complet, obtient une confirmation explicite, ne modifie que l'annotation prévue, réapplique le manifeste complet puis vérifie les conditions et le rollout. Cette capacité ne doit jamais servir à contourner GitOps ou à modifier une spécification applicative.

Le RBAC restreint les types de ressources, mais pas les champs d'un patch. La restriction aux annotations constitue donc le contrat d'utilisation de cette première version. Une future couche MCP spécialisée ou une politique d'admission dédiée pourra rendre cette contrainte techniquement incontournable.

Contrôles opérateur

Après une réconciliation Flux :

kubectl -n cluster-observer get deployment,service,pods
kubectl auth can-i --as=system:serviceaccount:cluster-observer:cluster-observer get pods
kubectl auth can-i --as=system:serviceaccount:cluster-observer:cluster-observer get pods/log
kubectl auth can-i --as=system:serviceaccount:cluster-observer:cluster-observer list pods/log
kubectl auth can-i --as=system:serviceaccount:cluster-observer:cluster-observer get secrets
kubectl auth can-i --as=system:serviceaccount:cluster-observer:cluster-observer create deployments
kubectl auth can-i --as=system:serviceaccount:cluster-observer:cluster-observer patch deployments
kubectl auth can-i --as=system:serviceaccount:cluster-observer:cluster-observer patch kustomizations.kustomize.toolkit.fluxcd.io -n flux-system
kubectl auth can-i --as=system:serviceaccount:cluster-observer:cluster-observer delete deployments

Les résultats attendus sont respectivement yes, yes, no, no, no, yes, yes, no. Un test MCP ciblé peut ensuite appeler pods_log sur un pod non sensible avec une petite valeur de tail et vérifier qu'aucune capacité de mutation n'est apparue.

Exposition Bearer dédiée à Mammouth AI

Mammouth AI accepte un MCP personnalisé en Streamable HTTP avec une clé d'API, mais ne peut pas émettre les deux en-têtes Cloudflare Access Service Auth. Un hostname distinct lui est donc réservé, sans modifier mcp.djayzone.com :

Élément Valeur
Endpoint Mammouth https://mcp-mammouth.djayzone.com/mcp
Passerelle mammouth-mcp-proxy (2 réplicas, ClusterIP :8080)
Authentification Authorization: Bearer …, clé dédiée de 256 bits
Secret cluster-observer/mammouth-mcp-api-key, créé hors Git
Backend http://mcp-gateway.cluster-observer.svc.cluster.local:8080/mcp

La passerelle ne dispose d'aucun ServiceAccount, d'aucun droit Kubernetes et d'aucun accès sortant hors CoreDNS et Service MCP. Elle compare la clé en temps constant, répond 401 sans indiquer quelle partie a échoué, ne transmet jamais l'en-tête Authorization au serveur MCP et relaie les réponses sans bufferisation pour préserver les flux SSE. Le corps des requêtes est borné à 1 MiB et le débit à 2 req/s avec une rafale de 4, comme la section [http] du serveur MCP.

Le contrôle Gatus « MCP Mammouth public » interroge l'endpoint sans clé et attend 401 : la fermeture de la porte est donc surveillée en continu, sans qu'aucun secret n'existe dans Git.

La création de la clé, sa rotation, sa révocation, la route Cloudflare Tunnel et le retour arrière sont décrits dans apps/ovh-primary/cluster-observer/README.md. Retirer le Secret coupe Mammouth seul, sans effet sur les autres agents.

Connecteur MCP unique (Gateway multi-domaines)

Depuis le Lot 0 de #434, le namespace héberge aussi mcp-gateway : un seul endpoint MCP derrière lequel plusieurs domaines d'outils sont exposés. Le Cluster Operator MCP y devient le domaine cluster.* au lieu de rester un connecteur indépendant.

Élément Valeur
Workload mcp-gateway (2 réplicas, ClusterIP :8080, chemin /mcp)
Image python:3.14-alpine@sha256:f6a589d43c42b9e7f7dc67a12d37132491f362859a5d750607710cc56da3bc72
Protocole MCP 2025-06-18, Streamable HTTP
Domaines actifs 11, aucun domaine planifié — ai.*, github.*, cluster.*, n8n.*, notrecouple.*, plane.*, slack.*, dagger.*, task.*, vehicle.* et gateway.*. L'inventaire exact des outils est découvert dynamiquement avec gateway.describe afin d'éviter de figer un compteur rapidement obsolète.
Authentification inchangée : Cloudflare Access Service Auth par agent, porte Bearer dédiée pour Mammouth AI
Contrôles de transport Origin validé contre une allowlist versionnée (403 sinon) ; seule la révision 2025-06-18 est annoncée, toute autre MCP-Protocol-Version est rejetée en 400
Privilèges Kubernetes aucun — ni ServiceAccount ni token Kubernetes
Écriture plane.* PLANE_WRITE_MODE=api : lectures et mutations directes par l’API Plane, sans commit Git
Déclenchement n8n.* par le webhook de production du workflow — l’API publique de n8n n’exécute pas. Un workflow n’est déclenchable que s’il porte le tag N8N_EXPOSED_TAG (mcp), possède un nœud Webhook actif et est lui-même activé
Credentials mcp-gateway-github pour github.* ; mcp-gateway-plane pour plane.* ; mcp-gateway-n8n pour n8n.* — tous provisionnés hors Git et montés en fichiers, jamais en variables d’environnement. slack.* n’en porte aucune : le jeton reste dans le composant slack-mcp
Administration n8n.* le domaine peut créer, modifier, supprimer, activer/désactiver un workflow et gérer explicitement son exposition avec n8n.set_workflow_exposure. La création ou l'activation n'expose jamais implicitement un workflow.

Un agent appelle d'abord gateway.describe pour connaître les domaines réellement actifs et leur surface exacte. Au 7 octobre 2026, planned_domains est vide : tous les domaines actuellement déclarés par le contrat sont actifs. La découverte dynamique reste la source de vérité plutôt qu'une liste statique dans la documentation.

Les outils sont publiés sous la forme <domaine>.<outil>. Un client qui normalise le nom en cluster_pods_list est routé correctement : le routeur résout le nom exact, puis un nom replié, et refuse de deviner en cas d'ambiguïté.

Un backend momentanément injoignable ne retire que son propre namespace de tools/list ; les autres domaines continuent de répondre. Une exception dans un domaine devient un résultat en erreur, jamais une panne du Gateway.

Domaine n8n.*

n8n reste l'orchestrateur — événements, workflows, retries et dispatch. Le Gateway lit ses workflows, les administre et peut les déclencher ; l'état durable, les checkpoints et les leases des tâches appartiennent à Task Runtime via task.*. Le domaine n8n.* ne transforme pas n8n en proxy synchrone.

Le déclenchement passe par le webhook de production du workflow, parce que l'API publique de n8n n'a aucun endpoint d'exécution. Cette contrainte sert de contrôle d'accès : un workflow n'est déclenchable que s'il porte le tag N8N_EXPOSED_TAG (mcp), possède un nœud Webhook actif, et est lui-même activé — sinon n8n n'a pas enregistré son chemin de production.

Le domaine administre aussi les workflows : create_workflow, update_workflow, delete_workflow, activate_workflow, deactivate_workflow et set_workflow_exposure. L'exposition reste une opération séparée et explicite : créer ou activer un workflow ne lui ajoute jamais automatiquement le tag mcp. set_workflow_exposure ajoute ou retire uniquement ce tag et préserve les autres tags. Un workflow n'est déclenchable que si les trois conditions restent réunies : tag mcp, webhook actif et workflow activé.

trigger_workflow rend la main sans attendre la fin. Si le workflow dépasse le délai, la réponse est status: "sent" et non une erreur : il a démarré, seule sa réponse manque, et signaler un échec inviterait à un doublon. Le résultat se lit avec list_executions puis get_execution.

Domaine slack.*

Slack est le bus de communication humain ↔ agents, jamais une source de vérité durable pour le code, le backlog ou l'état runtime.

Le Gateway ne parle pas directement à l'API Slack : il passe par le composant slack-mcp, un pont HTTP interne déployé dans son propre namespace, qui détient le jeton xoxb- et traduit vers https://slack.com/api. Cinq outils : channels_list, messages_list, messages_send, thread_reply et ops_event_publish. Ce dernier publie des événements opérationnels dédupliqués pour les intégrations prévues ; les agents respectent toujours les règles de routage de AGENTS.md et ne simulent pas manuellement les intégrations des canaux protégés.

Le jeton slack-mcp-secrets est injecté en variable d'environnement dans slack-mcp — contrairement aux credentials du Gateway, qui sont montées en fichiers et dont le contrat de rendu refuse toute origine secretKeyRef. Cette différence est volontaire au niveau du composant, mais elle mérite d'être sue : le jeton Slack est visible dans l'environnement du processus slack-mcp.

Domaines task.*, ai.* et vehicle.*

  • task.* porte l'état durable, les checkpoints, le fallback provider et les leases glissants de 10 minutes (get, acquire, renew, checkpoint, handoff, release, complete, fail). La présence de l'API ne suffit pas à déclarer un lot fonctionnel terminé : une preuve de tâche réelle reste exigée par Plane.
  • ai.* expose les routes providers autorisées du runtime agentique, notamment ChatGPT par abonnement OAuth et Mammouth lorsque sa route est disponible.
  • vehicle.* expose les lectures et commandes gardées du bridge Hyundai Bluelink sans donner d'accès direct au backend interne.

Domaine notrecouple.*

Le domaine notrecouple.* est volontairement limité à la recette DEV. Il expose les actions de test nécessaires à notrecouple-dev sans fournir de mutation générique de la production. Une panne de ce domaine n'empêche pas l'exploitation manuelle de DEV ni de PROD.

Domaine plane.*

Plane est lu et modifié directement par son API. PLANE_WRITE_MODE=api est actif : plane.set_state, plane.update_work_item et plane.create_work_item ne produisent plus de commit sur la branche historique plane. plane.create_module crée directement un Module/Epic avec une clé durable et un comportement idempotent.

Le Secret mcp-gateway-plane, provisionné hors Git avec la clé api-token, alimente les deux sens. Si la credential ou l'API manque, le domaine échoue explicitement sans basculer silencieusement vers Git.

Le workflow GitHub ne réconcilie plus .plane/** vers Plane. Il conserve temporairement la lecture de références historiques pour corréler les Pull Requests ; Plane reste la source de vérité du pilotage.

Les mutations protégées exigent le contexte _task.task_id et _task.lease_id obtenu via task.acquire. tools/list et gateway.describe(include_schemas=true) publient les mêmes schémas effectifs, y compris ce verrou et le champ facultatif _gateway_context de corrélation. Ce dernier ne remplace pas le verrou. Les deux contextes sont retirés avant le backend métier ; aucun lease brut n’est journalisé. Après déploiement d’une évolution des schémas, rafraîchir les signatures du connecteur et vérifier une recette réelle avant de conclure que les écritures fonctionnent depuis le client.

Point d’entrée MCP unique

mcp-gateway est le point d’entrée MCP unique et stable pour tous les consommateurs externes. Les services MCP spécialisés sont des backends internes et ne doivent pas être exposés ou configurés directement auprès de ChatGPT, Claude, Codex, Mammouth ou d’un futur client.

Les trois portes suivent le même invariant :

  • le Secure MCP Tunnel OpenAI cible mcp-gateway via MCP_SERVER_URL ;
  • le proxy Bearer Mammouth cible mcp-gateway via MCP_UPSTREAM_URL ;
  • le hostname Cloudflare mcp.djayzone.com, piloté à distance, cible le Service mcp-gateway:8080 sans modification de la politique Access.

La découverte dynamique tools/list rend tout nouveau domaine disponible aux consommateurs existants sans modifier leur destination.

Retour arrière

Repasser read_only = true, retirer les règles RBAC patch, puis fusionner la PR de rollback. Le hash du ConfigMap provoquera le rollout du serveur MCP via Flux.

Pour le Gateway seul : repointer mcp.djayzone.com vers cluster-observer:8080, remettre l'URL amont des portes déjà basculées, puis retirer gateway.yaml de apps/ovh-primary/cluster-observer/kustomization.yaml avec son entrée configMapGenerator et les ajouts d'allowlist réseau (networkpolicy.yaml, cloudflared.yaml, openai-tunnel.yaml, mammouth-proxy.yaml, ci-dispatcher/manifests.yaml). Le serveur MCP, son RBAC et les autres agents restent inchangés.

Modification des modules Plane

Le domaine Plane expose plane.update_module en mode API pour changer le nom ou la description d’un module existant, sans recréer son UUID ni modifier ses relations. La résolution refuse les noms ambigus et les collisions ; un PATCH minimal puis une relecture confirment les champs. Les répétitions identiques sont sans écriture. La description vide explicite permet d’effacer le texte. Les autres champs et les modes Git/dual sont refusés. Voir apps/ovh-primary/cluster-observer/plane-module-status.md pour la procédure et le smoke après déploiement.