Aller au contenu principal

Runtime MCP (ToolHive Operator)

Version anglaise : en/mcp-runtime.md.

Le Runtime MCP exécute dans le cluster les serveurs MCP (Model Context Protocol) des agents et des utilisateurs. Il repose sur le ToolHive Kubernetes Operator de Stacklok (Apache-2.0, auto-hébergé, aucun service externe), version 0.59.0. Chaque instance MCP est éphémère et isolée par tenant, projet et utilisateur : le contrôleur de cycle de vie du Control Plane la crée à la demande et la détruit dès qu'elle n'est plus utilisée.

1. Architecture : Gateway et Runtime​

BriqueRôleOù
MCP GatewayPoint d'entrée unique des appels MCP des agents et des utilisateurs : authentification (Keycloak), autorisation par utilisateur et par agent (OpenFGA, OPA), journal d'audit du Control Plane, routage vers l'instance.Control Plane, namespace enterprise-ai
MCP RuntimeExécution des serveurs MCP : un namespace par projet, une instance par utilisateur et modèle de serveur, proxy ToolHive authentifiant devant chaque serveur.ToolHive Operator (toolhive-system) et namespaces mcp-<12 hex>

Flux d'un appel :

  1. L'agent ou l'utilisateur appelle la Gateway du Control Plane.
  2. Le contrôleur de cycle de vie crée au besoin le namespace du projet puis l'instance (MCPServer et objets associés).
  3. L'opérateur ToolHive réconcilie le MCPServer : un proxy (eai.io/mcp-role: proxy, port 8080) et le serveur MCP (eai.io/mcp-role: server).
  4. La Gateway appelle le proxy avec un JWT court signé par le Control Plane. Le proxy vérifie la signature avec le JWKS public du Control Plane (route /v1/mcp/runtime/jwks, EAI_MCP_RUNTIME_JWKS_URL), l'émetteur (EAI_MCP_RUNTIME_ISSUER) et l'audience, puis applique sa politique Cedar.
  5. À la fin de l'usage, le contrôleur supprime l'instance et révoque ses identifiants temporaires. Si un MCPServer disparaît côté cluster, le contrôleur clôt l'instance de la même façon.

Objets créés par le contrôleur (contrat partagé avec services/control-plane) :

PortéeObjets
Namespace de projetNamespace mcp-<12 hex de sha256(tenant_id/project_id)> avec les étiquettes eai.io/mcp-runtime: "true", eai.io/tenant, eai.io/project, app.kubernetes.io/managed-by: eai-mcp-runtime et pod-security.kubernetes.io/enforce, audit, warn à restricted ; NetworkPolicy default-deny (entrée et sortie), ResourceQuota mcp-quota, LimitRange mcp-limits, MCPOIDCConfig eai-mcp-runtime
InstanceMCPServer inst-<10 hex>, MCPToolConfig du même nom, Secret inst-<10 hex>-env, CiliumNetworkPolicy inst-<10 hex> ; étiquettes eai.io/mcp-instance (UUID), eai.io/mcp-template, eai.io/owner (16 hex de sha256 de l'identifiant utilisateur, jamais l'identité), eai.io/tenant, eai.io/project, app.kubernetes.io/managed-by: eai-mcp-runtime
Podseai.io/mcp-instance et eai.io/mcp-role (proxy ou server)

2. Installation​

Les fichiers :

FichierContenu
deploy/toolhive/crds-values.yamlValeurs du chart toolhive-operator-crds 0.59.0 (crds.keep: true)
deploy/toolhive/operator-values.yamlValeurs du chart toolhive-operator 0.59.0 : 2 réplicas avec élection de leader, operator.rbac.scope: cluster, ressources, contextes de sécurité, images épinglées par digest
deploy/toolhive/k8s/namespace.yamlNamespace toolhive-system (Pod Security restricted, étiquettes tenant: platform, owner: platform-team)
deploy/toolhive/k8s/operator.yamlPodDisruptionBudget, Service de métriques et ServiceMonitor de l'opérateur (le chart ne les fournit pas)
deploy/argocd/repository-toolhive.yamlDéclaration du registre OCI ghcr.io/stacklok/toolhive dans Argo CD (enableOCI)
deploy/policies/kyverno/mcp-runtime.yamlClusterPolicy eai-mcp-runtime (Enforce)
deploy/cilium/mcp-runtime.yamlPolitiques réseau de l'opérateur, des proxies et du Control Plane
deploy/helm/enterprise-ai/templates/control-plane/mcp-runtime-rbac.yamlClusterRole et ClusterRoleBinding eai-mcp-runtime-controller

Composants Argo CD (deploy/argocd/applicationset.yaml) :

ComposantCoucheSource
toolhive-operator-crdsinfra, avant kyverno-policies (la politique cible le type MCPServer)chart OCI ghcr.io/stacklok/toolhive/toolhive-operator-crds 0.59.0
toolhive-operatorplatformchart OCI ghcr.io/stacklok/toolhive/toolhive-operator 0.59.0
toolhive-runtimeplatformdeploy/toolhive/k8s

Argo CD ne tire un chart Helm d'un registre OCI que si ce registre est déclaré avec enableOCI, et l'adresse s'écrit sans le préfixe oci:// : appliquez deploy/argocd/repository-toolhive.yaml avec l'AppProject et l'ApplicationSet (installation.md). L'AppProject autorise ghcr.io/stacklok/toolhive. Le namespace toolhive-system est créé par le module deploy/terraform/modules/platform-bootstrap, comme les autres namespaces de la plateforme.

Images (miroir Harbor, digest de l'index OCI lu sur ghcr.io pour le tag v0.59.0 ; copiez avec skopeo copy --all pour garder le même digest, puis signez avec la clé interne comme les autres images du miroir) :

Image amontDigest
ghcr.io/stacklok/toolhive/operator:v0.59.0sha256:b094d9220a687ef975a62f9c073fa5faf433e88c3f355f8ad29f50cd082e430c
ghcr.io/stacklok/toolhive/proxyrunner:v0.59.0sha256:f0e970f749ef10004d150721311fa8512ba4c349768ca7b1915ff77452750250
ghcr.io/stacklok/toolhive/vmcp:v0.59.0sha256:e9077bc25803740423a04333d8400a02b6f5f44d5ba7c6b5edf65437ce1fe0dd
ghcr.io/stacklok/thv-registry-api:v1.5.3sha256:323652f7bd195a0a6a84815e749f52a809633a07d911580abb2d58945abc78f3

Artefacts des charts : toolhive-operator 0.59.0 sha256:0920e45a4d371a6a9096c0019afea688e3aecbd1167182f5dacfe86b37605449, toolhive-operator-crds 0.59.0 sha256:b7694eb741a04043818978c34b0136cf8cef30329a95ae16de518bd02daaf620.

Activation côté Control Plane, dans les valeurs du chart deploy/helm/enterprise-ai :

mcpRuntime:
enabled: true
issuer: "" # vide : http://<release>-control-plane.<namespace>.svc.cluster.local:8080/v1/mcp/runtime
jwksUrl: "" # vide : <issuer>/jwks
gatewayNamespace: "" # vide : namespace de la release

La valeur ajoute la ClusterRole eai-mcp-runtime-controller (namespaces : get, list, create, patch, delete ; resourcequotas, limitranges, secrets, networkpolicies, ciliumnetworkpolicies, mcpservers, mcpoidcconfigs, mcptoolconfigs : get, list, watch, create, patch, update, delete), sa liaison au compte de service du Control Plane et les variables EAI_MCP_RUNTIME_ENABLED, EAI_MCP_RUNTIME_ISSUER, EAI_MCP_RUNTIME_JWKS_URL, EAI_MCP_RUNTIME_GATEWAY_NAMESPACE du Deployment du Control Plane. Par défaut elle vaut false : rien n'est rendu.

3. Durcissement​

Authentification obligatoire. ToolHive n'active pas l'authentification par défaut : un MCPServer sans configuration OIDC expose un proxy anonyme. Chaque MCPServer référence donc la MCPOIDCConfig eai-mcp-runtime du namespace (spec.oidcConfigRef avec nom et audience propre à l'instance). Le proxy n'accepte que les JWT émis par le Control Plane et vérifiés avec son JWKS public. La règle Kyverno mcpserver-oidc refuse tout MCPServer sans oidcConfigRef.

Autorisation Cedar. Chaque instance porte sa politique Cedar inline (spec.authzConfig) ou une référence (spec.authzConfigRef) : seul le propriétaire de l'instance (empreinte eai.io/owner) appelle ses outils, et le MCPToolConfig restreint la liste des outils exposés. Règle Kyverno mcpserver-authz.

Audit. spec.audit.enabled: true sur chaque MCPServer (règle mcpserver-audit) : le proxy écrit un événement d'audit par appel, envoyé dans l'index séparé décrit plus bas.

Isolation réseau.

  • Namespace de projet : NetworkPolicy default-deny créée par le contrôleur et maintenue par la règle Kyverno generate-default-deny (synchronize: true, recréée si elle est modifiée ou supprimée).
  • Proxies (deploy/cilium/mcp-runtime.yaml, eai-mcp-runtime-proxy) : entrée depuis le Control Plane seul, sur 8080 ; sortie vers le Control Plane pour le seul chemin JWKS (règle HTTP L7), vers l'API Kubernetes (attache au serveur stdio) et le DNS du cluster.
  • Serveurs MCP : aucune sortie hors de la CiliumNetworkPolicy inst-<10 hex> de l'instance (domaines exacts du modèle de serveur).
  • Opérateur : default-deny dans toolhive-system, sortie vers l'API Kubernetes et le DNS seulement, entrée des métriques depuis Prometheus (observability).
  • Control Plane : eai-mcp-runtime-control-plane ouvre la sortie vers les proxies et l'entrée JWKS depuis les proxies.

Pod Security. Les namespaces de projet sont en restricted (enforce, audit, warn), exigé par la règle namespace-name-and-labels ; toolhive-system aussi. Le conteneur mcp du podTemplateSpec : readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true ; au niveau du pod : seccompProfile: RuntimeDefault, runAsNonRoot: true, automountServiceAccountToken: false (règles mcpserver-mcp-container-*). Les politiques générales de deploy/policies/kyverno/pod-network-security.yaml et cluster-policies.yaml s'appliquent en plus à tous les pods créés.

Quotas. ResourceQuota mcp-quota et LimitRange mcp-limits dans chaque namespace de projet, créés par le contrôleur : un projet ne peut pas épuiser le cluster.

Images épinglées. Opérateur, proxy et images annexes par digest (tableau ci-dessus) ; l'image de chaque MCPServer doit porter @sha256: (règle mcpserver-image-digest).

Kyverno, politique eai-mcp-runtime (Enforce).

RègleContrôle
namespace-writersSeul system:serviceaccount:enterprise-ai:enterprise-ai-control-plane crée, modifie ou supprime un namespace eai.io/mcp-runtime=true
runtime-object-writersMême restriction pour les MCPServer, MCPToolConfig, MCPOIDCConfig, Secrets et CiliumNetworkPolicy de ces namespaces ; l'opérateur ne peut que mettre à jour ses propres ressources (finaliseurs), le contrôleur de namespaces et le ramasse-miettes que supprimer
namespace-name-and-labels, namespace-name-format, mcp-name-requires-labelNom mcp-<12 hex>, étiquettes Pod Security, tenant, projet ; un namespace nommé ainsi porte forcément l'étiquette
mcpserver-oidc, mcpserver-authz, mcpserver-audit, mcpserver-image-digestOIDC, Cedar, audit, digest
mcpserver-mcp-container-present, mcpserver-mcp-container-hardened, mcpserver-mcp-container-drops-allConteneur mcp présent et durci, pas de jeton de compte de service
generate-default-denyNetworkPolicy default-deny synchronisée

Cas de test : deploy/policies/kyverno/tests/mcp-runtime (ressources conformes et non conformes, requêtes du Control Plane et d'un administrateur du cluster).

Périmètre RBAC. L'opérateur tourne en operator.rbac.scope: cluster car les namespaces de projet sont créés dynamiquement ; les garde-fous sont Kyverno et Cilium. La ClusterRole du Control Plane donne l'accès aux Secrets de tout le cluster en lecture : Kyverno borne les écritures, pas les lectures. Gardez le journal d'audit de l'API Kubernetes actif sur les Secrets et surveillez les lectures du compte enterprise-ai-control-plane hors des namespaces mcp-*.

4. Journaux d'audit séparés​

  • Alloy (deploy/observability/alloy/values.yaml) retire les pods des namespaces mcp-<12 hex> du flux des journaux de la plateforme et les lit dans un flux à part (pods étiquetés eai.io/mcp-instance), étiqueté eai_stream=mcp-runtime-audit avec l'instance, le rôle, le tenant et le projet. Kyverno réserve ce nom aux namespaces étiquetés eai.io/mcp-runtime=true, la sélection par nom est donc équivalente à la sélection par étiquette.
  • Le collecteur OpenTelemetry (deploy/observability/otel-collector) écarte ces enregistrements du pipeline logs (index eai-logs-* et ClickHouse) et les écrit par le pipeline logs/mcp-runtime-audit dans l'index quotidien eai-mcp-runtime-audit-yyyy.MM.dd, après les mêmes filtres de données personnelles.
  • OpenSearch (deploy/observability/opensearch) : modèle d'index eai-mcp-runtime-audit, politique ISM eai-mcp-runtime-audit-retention (lecture seule après 2 jours, suppression après 400 jours), installés par bootstrap.py. Lecture réservée au rôle eai_mcp_runtime_audit_reader (rôle d'arrière-plan OpenSearch eai-security-officer, comme pour Security Analytics) ; OpenSearch Dashboards et les autres lecteurs de journaux n'y ont pas accès.
  • Le journal d'audit du Control Plane (chaîne de hachage PostgreSQL, exports WORM) reste la preuve de référence ; cet index sert aux investigations sur les instances.

5. Exploitation​

  • Pilotage : ToolHive UI est une application de bureau locale, destinée au poste d'un développeur ; il n'y a pas d'interface web ToolHive côté cluster et l'ancienne Cloud UI n'est plus proposée. Le pilotage se fait dans le Navin Manager (état de l'opérateur, instances par tenant et projet, destruction) et dans le portail (instances de l'utilisateur).
  • État de l'opérateur : 2 réplicas, un seul actif (bail toolhive-operator-leader-election du namespace toolhive-system), sondes /healthz et /readyz sur 8081, métriques sur 8080 (ServiceMonitor toolhive-operator).
  • Instances : MCPServer étiquetés app.kubernetes.io/managed-by=eai-mcp-runtime dans tous les namespaces, phase Pending, Ready, Failed, Terminating ou Stopped.
  • Destruction : depuis le Navin Manager ou le portail. Supprimer un MCPServer est sûr : le contrôleur révoque les identifiants temporaires et met l'instance en veille ; l'appel suivant de son propriétaire la recrée. Le Navin Manager redémarre aussi une instance (annotation mcpserver.toolhive.stacklok.dev/restarted-at) ; ses identités sont listées dans la ConfigMap kyverno/eai-mcp-runtime-operators (deploy/policies/kyverno/mcp-runtime-operators.yaml), seules autorisées à supprimer et à redémarrer, jamais à modifier la spécification.
  • Intervention d'urgence : Kyverno refuse les écritures d'un administrateur dans les namespaces mcp-*. Passez par le Control Plane ; sinon, une modification revue de la politique dans Git (Argo CD), jamais une désactivation à la main.
  • Mise à niveau : CRD d'abord (toolhive-operator-crds), puis l'opérateur, avec les nouveaux digests ; les CRD sont conservées à la désinstallation (crds.keep).
  • Panne de l'opérateur : les instances en cours continuent de servir ; aucune nouvelle instance n'est réconciliée tant qu'aucun réplica ne détient le bail.

6. Aucun MCP en ligne, catalogue de l'entreprise dans SeaweedFS​

  • Les utilisateurs n'utilisent jamais un serveur MCP en ligne. La passerelle n'atteint que des adresses internes : serveurs hébergés par le Navin Manager (EAI_EGRESS_PRIVATE_ALLOWLIST), instances du Runtime MCP, hôtes internes où l'entreprise héberge les serveurs qu'elle développe (EAI_MCP_INTERNAL_HOSTS : hôte exact, hôte:port, *.suffixe ou CIDR). Un hôte public est refusé à l'enregistrement et à chaque appel (403 mcp_online_forbidden, audit security.egress.denied, métrique eai_mcp_online_refused_total) ; un nom déclaré qui se résout vers une adresse publique est refusé à la connexion. Un préréglage en ligne de la Bibliothèque Navin (hosting: online) ne s'installe qu'avec l'adresse interne où l'entreprise l'héberge. Le registre MCP du Navin Manager applique la même règle et le service mcp-host lance ses serveurs hors ligne (npm, uv et pip sans téléchargement, sans proxy).
  • Les modèles du Runtime MCP viennent du catalogue de l'entreprise dans SeaweedFS : objet EAI_MCP_CATALOG_KEY (défaut mcp-runtime/templates.yaml) du profil de stockage plateforme EAI_MCP_CATALOG_STORAGE_PROFILE, relu toutes les EAI_MCP_CATALOG_REFRESH_SECONDS (300). Une image doit venir d'un registre interne (EAI_MCP_RUNTIME_IMAGE_REGISTRIES, défaut <harbor-host>) et être épinglée par digest. Une version illisible ou invalide ne retire jamais les modèles en service : la dernière version valide continue de servir, marquée périmée (eai_mcp_catalog_stale, alerte EaiMcpCatalogStale). En production, sans profil, la création répond 503 mcp_catalog_not_configured. Publication par l'équipe plateforme :
cd services/control-plane
.venv/bin/python -m eai.domains.mcp_runtime.catalog_cli validate templates.yaml
.venv/bin/python -m eai.domains.mcp_runtime.catalog_cli publish templates.yaml

7. Cycle de vie : destruction dès la panne, réveil à l'appel​

La boucle du contrôleur tourne dans les pods de l'API du Control Plane, seuls autorisés à appeler l'API Kubernetes, une réplique à la fois (verrou consultatif PostgreSQL), toutes les EAI_MCP_RUNTIME_REAPER_SECONDS (60).

SituationEffet
Inactivité au-delà de la durée de la politique (900 s par défaut), fin de la durée de vie d'une génération, arrêt par le propriétaire (POST .../stop)Veille : identifiants révoqués, ressources ToolHive supprimées, entrée de passerelle gardée
Appel d'une instance en veille par son propriétaire ou un de ses agentsRecréation : modèle relu dans le catalogue, politique vérifiée de nouveau, nouvelle génération et nouveaux identifiants ; l'appel attend jusqu'à EAI_MCP_RUNTIME_WAKE_TIMEOUT_SECONDS (90)
MCPServer Failed, condition *Validated à False, pods en CrashLoopBackOff, image introuvable, conteneur qui ne démarre pas, MCPServer jamais pris en charge par l'opérateur, délai de démarrage dépassé, deux sondes /health du proxy en échec, proxy qui ne répond pas à un appelDestruction immédiate puis veille ; l'appel suivant la recrée. Un appel dont la requête n'a pas atteint le serveur, ou sans effet de bord, est rejoué une fois sur l'instance recréée
EAI_MCP_RUNTIME_FAILURE_LIMIT pannes (3) en EAI_MCP_RUNTIME_FAILURE_WINDOW_SECONDS (900), configuration refusée par ToolHive, modèle retiré du catalogue, politique du projetÉchec : appels refusés (mcp_instance_failed) jusqu'au redémarrage explicite par le propriétaire (POST .../start)

8. Traçabilité par défaut​

  • Chaque espace de noms de projet reçoit le MCPTelemetryConfig eai-mcp-telemetry : traces et métriques des proxys ToolHive en OTLP/HTTP avec TLS vers le collecteur de la plateforme (EAI_MCP_RUNTIME_OTEL_ENDPOINT, échantillonnage EAI_MCP_RUNTIME_OTEL_SAMPLING_RATE, 1.0 par défaut), la CA interne copiée depuis EAI_MCP_RUNTIME_OTEL_CA_FILE (Secret eai-internal-ca du namespace de la release, monté par le chart). Kyverno refuse un MCPServer sans telemetryConfigRef.
  • La passerelle propage le contexte W3C (traceparent) au proxy : la trace d'un appel continue dans ToolHive. Chaque appel est compté (eai_mcp_calls_total par cible, méthode et résultat) et porté par le span eai.mcp.proxy.
  • Le journal de cycle de vie eai.mcp_runtime_events porte le trace_id de la requête et la génération de l'instance ; les métriques eai_mcp_runtime_instances, eai_mcp_runtime_lifecycle_total, eai_mcp_runtime_failures_total et eai_mcp_runtime_wake_seconds suivent les instances ; les journaux des pods vont dans l'index séparé eai-mcp-runtime-audit.
  • Alertes (groupe eai-mcp-runtime) : EaiMcpRuntimeFailures, EaiMcpRuntimeInstancesFailed, EaiMcpCatalogStale, EaiMcpOnlineServerRefused, EaiMcpCallErrorRatio.

9. Ce qui est repris du guide ToolHive​

Section du guide run-mcp-k8sDans la plateforme
Créer un serveur MCP, transports stdio et Streamable HTTPMCPServer écrit par le contrôleur depuis le modèle du catalogue (transport, mcpPort)
Gestion automatique du RBAC, ServiceAccount existantCompte de service propre à chaque proxy, jamais de jeton monté dans le conteneur MCP
Personnaliser le pod, serveur avec secrets, volumepodTemplateSpec durci (non root, racine en lecture seule, aucune capacité, seccomp), Secret propre à l'instance, /tmp borné
État d'un serveur, dépannage, référence des conditionsPhase, conditions *Validated, pods et sondes lus par le contrôleur ; raison et message affichés dans le Manager et le portail
Redémarrer un serveurAction Redémarrer du Navin Manager (annotation restarted-at)
Groupes et agrégation, mise à l'échelle, affinité et routage de session, vidangeNon repris : une instance par utilisateur et par projet, une réplique, sans agrégation entre utilisateurs (isolation)

10. Vérifications hors ligne​

helm lint deploy/helm/enterprise-ai --set mcpRuntime.enabled=true
/tmp/toolchains/kyverno test deploy/policies/kyverno/tests
cd services/control-plane && .venv/bin/python -m pytest -q -p no:cacheprovider tests/test_platform_mcp_runtime.py

tests/test_platform_mcp_runtime.py lit les fichiers, rend les deux charts ToolHive (helm template, réseau vers ghcr.io) et le chart de la plateforme avec et sans mcpRuntime.enabled, puis lance kyverno test et kyverno apply ; il échoue si helm ou kyverno manque.