Aller au contenu principal

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-ai du 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, *.suffixe ou 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.

  1. Créer le projet. La commande copie le modèle et remplace son nom partout.

    enterprise-ai create mcp files-server --template secure-mcp
    cd files-server

    Résultat attendu : un dossier files-server avec server.py, tool.yaml, policies/, tests/, README.md, README.fr.md, AGENTS.md et RULES.md.

    En cas d'échec : un dossier existant n'est jamais écrasé ; choisissez un autre nom ou ajoutez --force. Lancez enterprise-ai doctor quand la CLI elle-même ne démarre pas.

  2. Lire les règles du modèle. AGENTS.md et RULES.md listent 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 authz et 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​

FichierRôle
server.pyGestionnaire JSON-RPC 2.0 indépendant du transport : initialize (version du protocole, serverInfo), tools/list, tools/call, erreurs -32600, -32601, -32602
tool.yamlManifeste Tool (kind: Tool, contrats v1) : schémas, endpoint.type: mcp, endpoint.mcp_server, portées, classification, risque
policies/mcp.rego, policies/mcp_test.regoPolitique de refus par défaut et ses tests
tests/test_server.py, tests/test_manifests.pyTests unitaires du gestionnaire et des manifestes
template.yamlDescription du modèle : manifestes, politiques, types de tests (pytest, opa)

Écrire les outils, le manifeste et les politiques​

  1. Déclarer chaque outil dans server.py. Un outil a un name, une description bilingue et un inputSchema qui 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: true avec 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.

  2. 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_approval et risk_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.

  3. Écrire la politique. La politique refuse par défaut. La passerelle évalue l'action mcp.call sur la ressource mcp_server avec les attributs method et target (le nom de l'outil pour tools/call) ; adaptez les règles de policies/mcp.rego à cette entrée et couvrez chaque règle par un cas autorisé et un cas refusé dans policies/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 test nomme le cas en échec ; corrigez la règle ou le cas, ne supprimez jamais le test.

Tester et valider​

  1. Lancer les tests unitaires. Ils appellent le gestionnaire avec chaque méthode et chaque erreur.

    python -m pytest -q tests

    Résultat attendu : tous les tests passent.

    En cas d'échec : lisez l'assertion ; un code d'erreur du protocole qui change casse les clients.

  2. Lancer les tests de politique. enterprise-ai test authz lance les tests OPA de policies (et les tests du modèle OpenFGA quand il y en a).

    enterprise-ai test authz
    opa test policies -v

    Résultat attendu : PASS pour chaque cas.

    En cas d'échec : vérifiez le nom du paquet et les champs d'entrée utilisés par les règles.

  3. Valider le manifeste. La CLI vérifie le manifeste selon les contrats v1.

    enterprise-ai validate -f tool.yaml

    Ré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​

  1. 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.

  2. Écrire le corps de l'enregistrement. name est unique dans le tenant (minuscules, chiffres, ., _, -) ; transport vaut streamable_http (par défaut) ou sse ; auth.type vaut none, bearer, api_key, query_param ou connector, avec une credential_ref openbao:// ou env:// pour les trois types à secret ; allowed_tools et allowed_resources sont des motifs glob (par défaut ["*"]) ; project rattache 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_request nomme le champ (auth.credential_ref, auth.param, auth.connector).

  3. 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 (page studio/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.json
    server = 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 status et son auth sans aucun secret.

    En cas d'échec : 409 conflict quand le nom existe, 403 mcp_project_forbidden quand le projet de la requête diffère de project, 403 mcp_online_forbidden pour un hôte public.

  4. 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çoit mcp_principal_not_allowed.

    En cas d'échec : 409 mcp_instance_access_managed pour 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​

  1. Lancer la découverte. POST /v1/mcp/servers/{server_id}/discover appelle initialize puis 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 (action mcp_tool_version). Dans Studio MCP : Découvrir les outils.

    client.mcp.discover("files-server")

    Résultat attendu : new_versions liste chaque outil avec sa version et son approval_id ; unchanged liste les outils déjà connus ; server_version est le serverInfo.version du serveur.

    En cas d'échec : 502 upstream_error quand le serveur ne répond pas à initialize ou à tools/list.

  2. Approuver les versions. Un tenant_admin ou un security_officer lit 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 (sinon pending_approval ou rejected), avec ses schémas d'entrée et de sortie et ses portées.

    En cas d'échec : seuls tenant_admin et security_officer peuvent décider une approbation mcp_tool_version.

  3. 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_mismatch depuis un autre projet ; envoyez l'en-tête X-Project du projet du serveur.

remarque

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.

  1. 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 : result contient structuredContent et content ; les valeurs sensibles trouvées par la DLP sont masquées.

    En cas d'échec : le SDK lève McpError avec le code JSON-RPC ; voir le tableau de dépannage.

  2. 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 result ou error.

    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.

  3. 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_manifest quand endpoint.mcp_server manque.

Versionner un serveur​

  1. Monter les versions. Montez serverInfo.version dans server.py et metadata.version dans tool.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.

  2. 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é.

  3. 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 unchanged et 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).

attention

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​

  1. Mettre le serveur dans un pack. Un pack contient mcp/servers (un YAML avec name et tools), mcp/tools (manifestes Tool) et mcp/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.

  2. 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.

  3. Donner l'adresse à l'installation. L'installateur enregistre le serveur MCP dans la passerelle quand config.mcp_servers.<name>.url est fourni (une adresse interne), sinon l'objet reste pending_endpoint ; le test de fumée lance initialize puis tools/list.

    Résultat attendu : l'installation est installed et 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​

  1. Construire une image de conteneur du serveur. Poussez-la dans un registre interne de EAI_MCP_RUNTIME_IMAGE_REGISTRIES et 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.

  2. 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.yaml

    Ré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.

  1. Vérifier la copie après une synchronisation.

    python tools/navin_library/sync.py --check

    Résultat attendu : code de sortie 0, la copie correspond à sa source et catalog/navin/index.json contient 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-server avec status: 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énement mcp.tools.call avec l'outil et les détections de la DLP.

Étapes suivantes​

Dépannage​

SymptômeCauseCorrection
403 mcp_online_forbidden à l'enregistrement ou à l'appelHôte de l'URL public ou non déclaré interneHébergez le serveur en interne ; l'opérateur ajoute l'hôte à EAI_MCP_INTERNAL_HOSTS
409 conflict à l'enregistrementNom déjà utilisé dans le tenantChoisissez un autre nom ou supprimez l'ancien serveur
404 mcp_server_not_foundIdentifiant 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 projetEnvoyez X-Project avec le projet du serveur
JSON-RPC -32003 mcp_principal_not_allowedAppelant absent de la liste d'accèsAjoutez l'appelant avec PUT /v1/mcp/servers/{server_id}/access
JSON-RPC -32003 "MCP server disabled"Serveur disabledDemandez 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_toolsEnregistrez de nouveau le serveur avec l'outil dans allowed_tools
JSON-RPC -32003 "scope tools:invoke required"Jeton sans la portéeUtilisez une clé ou un jeton avec tools:invoke
JSON-RPC -32003 dlp_blockedUne règle DLP du tenant bloque les arguments ou le résultatRetirez la donnée sensible ou revoyez la règle avec le responsable sécurité
429 quota_exceededQuota d'appels d'outils du tenant atteintConsultez les quotas avec enterprise-ai quotas usage
502 upstream_error ou JSON-RPC -32004Serveur injoignable, erreur HTTP ou absence de réponseVérifiez les journaux du serveur et son URL
new_versions vide après une modificationDescription et schémas inchangésSeuls ces trois champs créent une version