Guide de développement MCP
English version: ../../en/guides/mcp-development.md
Ce guide vous mène d'un dossier vide à un serveur MCP (Model Context Protocol) que les agents d'un projet appellent
par la passerelle MCP du Control Plane : modèle, outils, manifeste, politique, tests, enregistrement, découverte,
appel, versions et publication. Il s'adresse aux développeurs ; enregistrer un serveur demande un rôle d'écriture
(par exemple developer ou tenant_admin), approuver ses outils demande tenant_admin ou security_officer.
Comptez une heure pour un premier serveur.
L'endroit où le serveur s'exécute est décrit dans Héberger des serveurs MCP, les instances par utilisateur dans Runtime MCP par utilisateur et par projet et les contrôles de la passerelle dans Sécurité MCP.
Avant de commencer
- La CLI
enterprise-aidu SDK Python est installée et connectée. Vérification :enterprise-ai doctor. - OPA est installé pour les tests de politique. Vérification :
opa version. - Vous connaissez le projet (identifiant ou slug) auquel le serveur appartient, par exemple
alpha. - Le serveur s'exécutera uniquement à une adresse interne. La passerelle n'atteint jamais un serveur MCP en ligne :
l'hôte de l'URL doit être nommé dans
EAI_MCP_INTERNAL_HOSTS(hôte exact, hôte:port,*.suffixeou CIDR) par l'opérateur de la plateforme, sauf si le serveur est hébergé par le Navin Manager ou s'exécute dans le Runtime MCP. - Les secrets sont uniquement des références (
openbao://...), jamais des valeurs dans le code, les manifestes ou les requêtes.
Créer un serveur à partir du modèle
Le modèle sdks/python/src/enterprise_ai/templates/mcp-server fournit un serveur MCP qui répond en JSON-RPC 2.0
(initialize, tools/list, tools/call), son manifeste Tool, sa politique OPA et ses tests. secure-mcp est un
alias de mcp-server.
-
Créer le projet. La commande copie le modèle et remplace son nom partout.
enterprise-ai create mcp files-server --template secure-mcpcd files-serverRésultat attendu : un dossier
files-serveravecserver.py,tool.yaml,policies/,tests/,README.md,README.fr.md,AGENTS.mdetRULES.md.En cas d'échec : un dossier existant n'est jamais écrasé ; choisissez un autre nom ou ajoutez
--force. Lancezenterprise-ai doctorquand la CLI elle-même ne démarre pas. -
Lire les règles du modèle.
AGENTS.mdetRULES.mdlistent ce qu'une modification doit préserver : aucun secret, aucun appel direct à un modèle, des tests pour chaque modification, des textes en anglais et en français, le moindre privilège.Résultat attendu : vous connaissez les contrôles que lancent
enterprise-ai validate,enterprise-ai test authzet les tests unitaires.En cas d'échec : les fichiers manquent quand le dossier a été créé à la main ; recréez-le avec l'étape 1.
Contenu du modèle
| Fichier | Rôle |
|---|---|
server.py | Gestionnaire JSON-RPC 2.0 indépendant du transport : initialize (version du protocole, serverInfo), tools/list, tools/call, erreurs -32600, -32601, -32602 |
tool.yaml | Manifeste Tool (kind: Tool, contrats v1) : schémas, endpoint.type: mcp, endpoint.mcp_server, portées, classification, risque |
policies/mcp.rego, policies/mcp_test.rego | Politique de refus par défaut et ses tests |
tests/test_server.py, tests/test_manifests.py | Tests unitaires du gestionnaire et des manifestes |
template.yaml | Description du modèle : manifestes, politiques, types de tests (pytest, opa) |
Écrire les outils, le manifeste et les politiques
-
Déclarer chaque outil dans
server.py. Un outil a unname, unedescriptionbilingue et uninputSchemaqui refuse les propriétés inconnues. La passerelle calcule l'empreinte de la description, du schéma d'entrée et du schéma de sortie de chaque outil : ces trois champs définissent sa version dans le catalogue.TOOLS = [{"name": "lookup","description": "Look up a product by reference / Rechercher un produit par référence","inputSchema": {"type": "object", "required": ["reference"],"properties": {"reference": {"type": "string", "pattern": "^[A-Z0-9-]{3,32}$"}},"additionalProperties": False},}]Résultat attendu : tools/list renvoie l'outil, tools/call vérifie ses arguments et répond
isError: trueavec un message lisible quand ils sont invalides.En cas d'échec : une méthode inconnue doit répondre -32601 et un outil inconnu -32602, comme dans le modèle.
-
Décrire l'outil dans
tool.yaml. Le manifeste est ce que la Tool Gateway enregistre : les mêmes schémas que le serveur,endpoint.mcp_serverégal au nom que le serveur aura dans le registre, les portées dont l'appelant a besoin (scopes_required),data_classification,side_effects,requires_approvaletrisk_level.Résultat attendu : le manifeste décrit le même outil que
server.py, avec la classification la plus basse et le plus petit ensemble de portées nécessaires.En cas d'échec : un champ que le contrat ne définit pas est refusé par
enterprise-ai validate; retirez-le. -
Écrire la politique. La politique refuse par défaut. La passerelle évalue l'action
mcp.callsur la ressourcemcp_serveravec les attributsmethodettarget(le nom de l'outil pour tools/call) ; adaptez les règles depolicies/mcp.regoà cette entrée et couvrez chaque règle par un cas autorisé et un cas refusé danspolicies/mcp_test.rego. La publication d'une politique sur la plateforme est décrite dans Développement de politiques.Résultat attendu : chaque règle a un test qui l'autorise et un test qui la refuse.
En cas d'échec :
opa testnomme le cas en échec ; corrigez la règle ou le cas, ne supprimez jamais le test.
Tester et valider
-
Lancer les tests unitaires. Ils appellent le gestionnaire avec chaque méthode et chaque erreur.
python -m pytest -q testsRésultat attendu : tous les tests passent.
En cas d'échec : lisez l'assertion ; un code d'erreur du protocole qui change casse les clients.
-
Lancer les tests de politique.
enterprise-ai test authzlance les tests OPA depolicies(et les tests du modèle OpenFGA quand il y en a).enterprise-ai test authzopa test policies -vRésultat attendu :
PASSpour chaque cas.En cas d'échec : vérifiez le nom du paquet et les champs d'entrée utilisés par les règles.
-
Valider le manifeste. La CLI vérifie le manifeste selon les contrats v1.
enterprise-ai validate -f tool.yamlRésultat attendu : le document est valide, code de sortie 0.
En cas d'échec : le code de sortie 1 liste chaque champ invalide avec son chemin dans le document.
Enregistrer le serveur
-
Exécuter le serveur à une adresse interne. Exposez le gestionnaire par le transport HTTP streamable (ou SSE) à l'adresse d'un hôte nommé dans
EAI_MCP_INTERNAL_HOSTS, ou laissez la plateforme l'héberger (Héberger des serveurs MCP).Résultat attendu : le serveur répond à
initializeà son URL depuis le réseau du Control Plane.En cas d'échec : la passerelle refuse un hôte public avec 403
mcp_online_forbidden, à l'enregistrement et à chaque appel ; demandez à l'opérateur d'ajouter l'hôte interne. -
Écrire le corps de l'enregistrement.
nameest unique dans le tenant (minuscules, chiffres,.,_,-) ;transportvautstreamable_http(par défaut) ousse;auth.typevautnone,bearer,api_key,query_paramouconnector, avec unecredential_refopenbao://ouenv://pour les trois types à secret ;allowed_toolsetallowed_resourcessont des motifs glob (par défaut["*"]) ;projectrattache le serveur à un projet.{"name": "files-server","url": "https://<mcp-files-host>/mcp","transport": "streamable_http","auth": {"type": "bearer", "credential_ref": "openbao://mcp/files-server/token"},"allowed_tools": ["lookup"],"allowed_resources": [],"project": "alpha"}Résultat attendu : un corps qui ne contient aucune valeur secrète.
En cas d'échec : 400
invalid_requestnomme le champ (auth.credential_ref,auth.param,auth.connector). -
Enregistrer le serveur. POST /v1/mcp/servers l'enregistre, écrit son propriétaire dans OpenFGA et audite
mcp.server.create. Dans le portail, le même corps se saisit dans Studio MCP (pagestudio/mcp), qui le vérifie selon les champs documentés, puis Enregistrer.curl -sS -X POST <control-plane-url>/v1/mcp/servers \-H "Authorization: Bearer <token>" -H "X-Project: alpha" \-H "Content-Type: application/json" -d @files-server.mcp-server.jsonserver = client.mcp.register_server("files-server", "https://<mcp-files-host>/mcp",auth={"type": "bearer", "credential_ref": "openbao://mcp/files-server/token"},allowed_tools=["lookup"], project="alpha")<token>: une clé d'API ou un jeton d'accès d'un utilisateur qui a un rôle d'écriture.Résultat attendu : 201 avec le serveur, son
statuset sonauthsans aucun secret.En cas d'échec : 409
conflictquand le nom existe, 403mcp_project_forbiddenquand le projet de la requête diffère deproject, 403mcp_online_forbiddenpour un hôte public. -
Restreindre qui peut l'appeler (facultatif). PUT /v1/mcp/servers/{server_id}/access liste les utilisateurs et les agents autorisés à appeler le serveur ; une liste vide laisse chaque membre de son projet l'appeler. Dans le portail : Bibliothèque Navin > MCP, menu du serveur.
Résultat attendu : la réponse montre
allowed_principals; tout autre appelant reçoitmcp_principal_not_allowed.En cas d'échec : 409
mcp_instance_access_managedpour l'entrée d'une instance du Runtime MCP, dont la liste d'accès est son propriétaire et ses agents.
Découvrir et approuver les outils
-
Lancer la découverte. POST /v1/mcp/servers/{server_id}/discover appelle
initializepuis tools/list par la passerelle. Chaque nouvel outil, et chaque outil dont la description ou les schémas ont changé, devient une nouvelle version du catalogue avec une demande d'approbation (actionmcp_tool_version). Dans Studio MCP : Découvrir les outils.client.mcp.discover("files-server")Résultat attendu :
new_versionsliste chaque outil avec saversionet sonapproval_id;unchangedliste les outils déjà connus ;server_versionest leserverInfo.versiondu serveur.En cas d'échec : 502
upstream_errorquand le serveur ne répond pas àinitializeou à tools/list. -
Approuver les versions. Un
tenant_adminou unsecurity_officerlit la demande avec GET /v1/approvals et décide avec POST /v1/approvals/{approval_id}/decide ({"decision": "approve"}).Résultat attendu : GET /v1/mcp/catalog montre la version avec
status: approved(sinonpending_approvalourejected), avec ses schémas d'entrée et de sortie et ses portées.En cas d'échec : seuls
tenant_adminetsecurity_officerpeuvent décider une approbationmcp_tool_version. -
Lire les outils. GET /v1/mcp/servers/{server_id}/tools liste les outils que le serveur expose, filtrés par
allowed_tools.Résultat attendu : seuls les outils autorisés apparaissent.
En cas d'échec : 403
mcp_server_project_mismatchdepuis un autre projet ; envoyez l'en-têteX-Projectdu projet du serveur.
Le catalogue garde la trace de la revue de chaque schéma. La passerelle applique elle-même allowed_tools : gardez
un outil hors de allowed_tools tant que sa version n'est pas approuvée.
Appeler par la passerelle
Les agents et les utilisateurs n'appellent jamais un serveur MCP en direct : POST /v1/mcp/gateway/{server_id} relaie le JSON-RPC 2.0 (un message ou un lot de 50 au plus) après les contrôles de Sécurité MCP.
-
Ouvrir une session et appeler un outil. La session du SDK envoie
initialize, puis chaque méthode avec son propre identifiant.session = client.mcp.session("files-server")session.initialize()tools = session.list_tools()result = session.call_tool("lookup", {"reference": "KB-100"})Résultat attendu :
resultcontientstructuredContentetcontent; les valeurs sensibles trouvées par la DLP sont masquées.En cas d'échec : le SDK lève
McpErroravec le code JSON-RPC ; voir le tableau de dépannage. -
Envoyer une requête brute (facultatif). La page Serveurs MCP du portail (
capabilities/mcp) a un testeur Passerelle MCP qui envoie un corps JSON-RPC ; le SDK fait de même :client.mcp.rpc("files-server", "tools/call", {"name": "lookup", "arguments": {"reference": "KB-100"}})Résultat attendu : une réponse JSON-RPC avec
resultouerror.En cas d'échec : -32601 signifie que la méthode n'est pas l'une de
initialize, tools/list, tools/call,resources/list,resources/read,prompts/list,ping. -
Appeler par la Tool Gateway (facultatif). Enregistrez le manifeste avec
enterprise-ai apply -f tool.yaml, puis appelez POST /v1/tools/{name}/invoke : la Tool Gateway vérifie les portées, la politique, l'approbation et le coût, puis appelle la passerelle MCP.Résultat attendu : l'invocation apparaît dans GET /v1/tools/invocations.
En cas d'échec : 422
invalid_manifestquandendpoint.mcp_servermanque.
Versionner un serveur
-
Monter les versions. Montez
serverInfo.versiondansserver.pyetmetadata.versiondanstool.yaml.Résultat attendu : le nouveau build annonce sa version dans
initialize.En cas d'échec : une version qui ne change pas masque la modification dans l'historique du catalogue.
-
Déployer le nouveau build à la même URL, puis relancer la découverte. Chaque outil dont la description ou les schémas ont changé reçoit la version N+1 et une nouvelle demande d'approbation ; les autres restent
unchanged.Résultat attendu : GET /v1/mcp/catalog montre les deux versions d'un outil modifié.
En cas d'échec : aucune nouvelle version signifie que les trois champs pris dans l'empreinte n'ont pas changé.
-
Revenir en arrière si nécessaire. Déployez le build précédent : ses schémas correspondent à une version déjà enregistrée, la découverte liste donc les outils comme
unchangedet ne demande aucune nouvelle approbation.Résultat attendu : le catalogue garde son historique ; rien n'est supprimé.
En cas d'échec : désactivez le serveur tout de suite avec POST /v1/mcp/servers/{server_id}/status (
{"status": "disabled", "reason": "..."}) ; le réactiver (active) passe par une demande de promotion (202).
Le registre n'a pas de route pour changer l'URL ou allowed_tools d'un serveur enregistré. Pour les changer, un
tenant_admin supprime le serveur (DELETE /v1/mcp/servers/{server_id}) et l'enregistre de nouveau ; les outils du
nouveau serveur repassent par la découverte et l'approbation.
Publier
Sur la marketplace
-
Mettre le serveur dans un pack. Un pack contient
mcp/servers(un YAML avecnameettools),mcp/tools(manifestes Tool) etmcp/schemas. Voir Développement de packs.enterprise-ai plugin validate <pack-folder>enterprise-ai plugin build <pack-folder> --out <dist-folder>Résultat attendu : le pack est valide et construit.
En cas d'échec : "an MCP server needs name and tools" nomme le fichier de
mcp/serversà corriger. -
Signer et publier le pack. Suivez Marketplace : la signature, le SBOM, la provenance et les autres contrôles sont vérifiés à la publication.
Résultat attendu : le paquet est listé par GET /v1/marketplace/packages.
En cas d'échec : la réponse nomme le contrôle en échec.
-
Donner l'adresse à l'installation. L'installateur enregistre le serveur MCP dans la passerelle quand
config.mcp_servers.<name>.urlest fourni (une adresse interne), sinon l'objet restepending_endpoint; le test de fumée lanceinitializepuis tools/list.Résultat attendu : l'installation est
installedet son serveur MCP est actif.En cas d'échec : un test de fumée en échec annule la modification ; l'installation garde son état précédent.
Dans le catalogue MCP de l'entreprise
-
Construire une image de conteneur du serveur. Poussez-la dans un registre interne de
EAI_MCP_RUNTIME_IMAGE_REGISTRIESet notez son empreinte.Résultat attendu : une référence d'image de la forme
<registry>/<path>:<tag>@sha256:<digest>.En cas d'échec : une image sans empreinte ou d'un autre registre est refusée par le catalogue.
-
Ajouter un modèle et publier le catalogue. L'équipe plateforme ajoute le modèle au fichier des modèles (partez de
catalog/mcp-runtime/templates.yaml) et le publie dans SeaweedFS (EAI_MCP_CATALOG_STORAGE_PROFILE,EAI_MCP_CATALOG_KEY) :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.yamlRésultat attendu :
published <profile>/<key> version <n>; les utilisateurs des projets dont la politique autorise le modèle le démarrent depuis Bibliothèque Navin > Instances MCP.En cas d'échec : code de sortie 1 pour un fichier invalide, 2 quand la publication est refusée (stockage, configuration).
Dans la bibliothèque Navin
Les préréglages MCP de la bibliothèque Navin viennent du paquet Navin, copiés dans catalog/navin/mcp-presets.json
par python tools/navin_library/sync.py. Un préréglage s'ajoute dans le paquet Navin, puis la copie est
synchronisée ; un projet l'installe avec POST /v1/projects/{project_id}/navin-library/mcp-presets/{name}/install.
Voir Bibliothèque Navin.
-
Vérifier la copie après une synchronisation.
python tools/navin_library/sync.py --checkRésultat attendu : code de sortie 0, la copie correspond à sa source et
catalog/navin/index.jsoncontient l'empreinte de chaque élément.En cas d'échec : le code de sortie 1 signale une copie qui ne correspond plus à sa source ; relancez la synchronisation.
Vérifier
- GET /v1/mcp/servers liste
files-serveravecstatus: active. - GET /v1/mcp/catalog montre chaque outil avec
status: approved. - Un tools/call par la passerelle répond un
result, et GET /v1/audit/events contient l'événementmcp.tools.callavec l'outil et les détections de la DLP.
Étapes suivantes
Dépannage
| Symptôme | Cause | Correction |
|---|---|---|
403 mcp_online_forbidden à l'enregistrement ou à l'appel | Hôte de l'URL public ou non déclaré interne | Hébergez le serveur en interne ; l'opérateur ajoute l'hôte à EAI_MCP_INTERNAL_HOSTS |
409 conflict à l'enregistrement | Nom déjà utilisé dans le tenant | Choisissez un autre nom ou supprimez l'ancien serveur |
404 mcp_server_not_found | Identifiant ou nom erroné | Listez les serveurs avec GET /v1/mcp/servers |
403 mcp_server_project_mismatch ou JSON-RPC -32003 "MCP server of another project" | Serveur d'un autre projet, ou aucun contexte de projet | Envoyez X-Project avec le projet du serveur |
JSON-RPC -32003 mcp_principal_not_allowed | Appelant absent de la liste d'accès | Ajoutez l'appelant avec PUT /v1/mcp/servers/{server_id}/access |
| JSON-RPC -32003 "MCP server disabled" | Serveur disabled | Demandez active avec POST /v1/mcp/servers/{server_id}/status et approuvez la promotion |
| JSON-RPC -32003 "tool ... is not allowed on this server" | Outil hors de allowed_tools | Enregistrez de nouveau le serveur avec l'outil dans allowed_tools |
| JSON-RPC -32003 "scope tools:invoke required" | Jeton sans la portée | Utilisez une clé ou un jeton avec tools:invoke |
JSON-RPC -32003 dlp_blocked | Une règle DLP du tenant bloque les arguments ou le résultat | Retirez la donnée sensible ou revoyez la règle avec le responsable sécurité |
429 quota_exceeded | Quota d'appels d'outils du tenant atteint | Consultez les quotas avec enterprise-ai quotas usage |
502 upstream_error ou JSON-RPC -32004 | Serveur injoignable, erreur HTTP ou absence de réponse | Vérifiez les journaux du serveur et son URL |
new_versions vide après une modification | Description et schémas inchangés | Seuls ces trois champs créent une version |