DEV Community

Cover image for Combien de tokens la liste d'outils MCP d'osFoundry coûte-t-elle à Codex et comment la réduire par projet ?
BBT for BBT Français

Posted on

Combien de tokens la liste d'outils MCP d'osFoundry coûte-t-elle à Codex et comment la réduire par projet ?

Voici l'appel qu'un client MCP envoie juste après le handshake, fait avec curl vers le serveur d'osFoundry depuis un Mac à Tokyo le 2026-11-06. Aucun token n'est nécessaire :

$ curl -s -D - -X POST https://api.osfoundry.io/api/v1/external-mcp \
    -H 'content-type: application/json' \
    -H 'accept: application/json, text/event-stream' \
    -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

HTTP/2 200
content-type: application/json
content-length: 45368

{"jsonrpc":"2.0","id":2,"result":{"tools":[ …59 tools… ]}}
Enter fullscreen mode Exit fullscreen mode

Une des 59 entrées :

{"name": "read_note_tab",
 "title": "Read one Note tab",
 "description": "Read a selected Note, Markdown, Slides, Sheet, Mermaid, Board, Whiteboard, Design, or Artifacts tab by instance ID. Use list_note_attachments for Files and Sources.",
 "inputSchema": {"type": "object",
                 "properties": {"note_id": {"type": "string"}, "tab_id": {"type": "string"}},
                 "required": ["note_id", "tab_id"], "additionalProperties": false},
 "annotations": {"title": "Read one Note tab", "readOnlyHint": true,
                 "openWorldHint": false, "destructiveHint": false},
 "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:read"]}]}
Enter fullscreen mode Exit fullscreen mode

Codex CLI se connecte à ce serveur avec une table [mcp_servers.osfoundry] et une url. Sauf si le client en diffère certaines, chaque requête que Codex envoie ensuite au modèle transporte ces 59 déclarations sous une forme ou une autre. Les questions ci-dessous sont celles qui se posent quand vous décidez combien il doit en transporter.

Que récupère Codex avant le premier tour ?

Trois POST vers la même URL : initialize, la notification notifications/initialized et tools/list. Le serveur en production répond ainsi :

Requête Statut Corps
initialize 200 308 bytes : protocole 2025-06-18, osfoundry 0.1.0, tools.listChanged: false, instructions
notifications/initialized 202 vide
tools/list 200 45,368 bytes, 59 outils
tools/call sans token 401 WWW-Authenticate avec resource_metadata

listChanged: false signifie que le serveur n'annoncera pas de changements de la liste en cours de session. La liste que Codex reçoit au démarrage est celle de toute la session.

La chaîne instructions fait 125 caractères : "Use list_granted_resources first. Every Note edit must use the exact tab ID. Local-only data is unavailable to cloud clients." La documentation Codex d'OpenAI indique que Codex lit ce champ comme une consigne valable pour tout le serveur et que les 512 premiers caractères doivent se suffire à eux-mêmes. Celle-ci tient largement.

Le 401, c'est là que commence OAuth. Cette partie, et la raison pour laquelle osFoundry vérifie l'accès à chaque appel, est traitée dans un autre article sur le même endpoint, côté Claude. Ici, seul le catalogue compte.

Combien de tokens pour 59 outils ?

Ce qui arrive au modèle n'est pas le JSON brut. Codex transforme chaque outil MCP en déclaration de fonction. Le nom, la description et le schéma d'entrée sont ce dont le modèle a besoin. title, annotations et securitySchemes sont des métadonnées de protocole que le client utilise lui-même. OpenAI ne publie pas la façon dont les déclarations d'outils sont comptées de son côté, donc les chiffres ci-dessous mesurent le payload, pas la facture.

Mesuré avec tiktoken 0.14.0 et l'encodage o200k_base, sur du JSON compact :

Quoi Tokens
Résultat brut de tools/list, tous les champs 10,494
59 déclarations (nom avec le préfixe mcp__osfoundry__, description, schéma) 7,609
dont descriptions 2,194
dont schémas d'entrée 4,118
dont préfixes mcp__osfoundry__ 413
Métadonnées qui restent dans le client (title, annotations, securitySchemes) 3,590

La déclaration médiane fait 116 tokens. La plus grosse est edit_note_tab avec 336, suivie de send_channel_message (237) et upload_linked_chat_artifact (233). La plus petite est list_granted_resources avec 57.

Par scope : les 30 outils osfoundry:read totalisent 2,906 tokens, les 26 outils osfoundry:write 4,378 et les 3 outils osfoundry:run 329. Les outils d'écriture coûtent plus cher par outil parce qu'ils portent expected_version, idempotency_key et le contenu à écrire.

Environ 7,600 tokens se trouvent devant chaque requête, que le modèle appelle osFoundry ou non. Les préfixes de prompt mis en cache rendent ça moins cher qu'il n'y paraît. Ça occupe quand même du contexte, et 59 noms se disputent l'attention du modèle quand il choisit l'outil qui convient.

Où Codex applique-t-il enabled_tools ?

Les clés sont documentées sur la page MCP de Codex chez OpenAI : enabled_tools est une liste d'autorisation, disabled_tools une liste de refus "applied after enabled_tools". L'implémentation se trouve dans codex-rs/codex-mcp/src/tools.rs du dépôt openai/codex :

pub(crate) fn allows(&self, tool_name: &str) -> bool {
    if let Some(enabled) = &self.enabled
        && !enabled.contains(tool_name)
    {
        return false;
    }

    !self.disabled.contains(tool_name)
}

pub(crate) fn filter_tools(tools: Vec<ToolInfo>, filter: &ToolFilter) -> Vec<ToolInfo> {
    tools
        .into_iter()
        .filter(|tool| filter.allows(&tool.tool.name))
        .collect()
}
Enter fullscreen mode Exit fullscreen mode

Deux conséquences. Le filtre compare tool.tool.name, le nom MCP brut, donc vous écrivez read_note_tab dans config.toml, pas mcp__osfoundry__read_note_tab. Le préfixe est ajouté plus tard, dans le même fichier, quand les noms visibles par le modèle sont construits. Et une recherche dans un set n'échoue jamais. Un nom qui n'existe pas dans tools/list ne correspond à rien et ne signale rien.

Le filtre s'exécute dans le client une fois tools/list arrivé. Il raccourcit le prompt. Il ne raccourcit pas le téléchargement, et le serveur n'en sait rien.

Reconstruire le côté client

Aucun binaire codex n'est installé sur le Mac où ces mesures ont été faites (Apple M1). catalog.py joue le rôle du démarrage de Codex. Il fait 92 lignes et se lance avec uv run --python 3.12 --with tiktoken. Il lit les tables [mcp_servers.*] d'un fichier TOML, envoie les trois requêtes à l'endpoint réel via http.client, applique la même règle « autoriser puis refuser » aux noms bruts, construit les déclarations mcp__osfoundry__<tool> et les compte :

kept = [t for t in tools if (allow is None or t["name"] in allow) and t["name"] not in deny]
decls = [model_decl("osfoundry", t) for t in kept]
unknown = sorted(((allow or set()) | deny) - names)
Enter fullscreen mode Exit fullscreen mode

Le fichier de profils contient une table par configuration. Un vrai config.toml aurait une seule table [mcp_servers.osfoundry], globale ou par dépôt :

[mcp_servers.notes_kb_records]
url = "https://api.osfoundry.io/api/v1/external-mcp"
enabled_tools = [
  "list_granted_resources", "list_project_resources", "list_note_tabs",
  "read_note_tab", "read_sheet_range", "search_knowledge_base",
  "read_knowledge_article", "list_record_items", "read_record_item",
]

[mcp_servers.deny_list]
url = "https://api.osfoundry.io/api/v1/external-mcp"
disabled_tools = ["request_session_run", "run_graph_session", "cancel_graph_run",
                  "propose_gitnest_work", "…the other propose_ tools…",
                  "join_agent_network", "send_agent_message", "wait_for_messages",
                  "list_agents", "set_status", "claim_connected_profile"]

[mcp_servers.typo]
url = "https://api.osfoundry.io/api/v1/external-mcp"
enabled_tools = ["list_granted_resources", "list_project_resources",
                 "read_note", "search_knowledge_base"]
Enter fullscreen mode Exit fullscreen mode

Sortie :

$ uv run --python 3.12 --with tiktoken python catalog.py profiles.toml --runs 10
status (200, 202, 200)  initialize 308 B  tools/list 45368 B  59 tools  instructions 125 chars
raw tools/list result: 10494 tokens (o200k_base)

profile                  tools   bytes  tokens  unknown names
all_59                      59   33559    7609  -
read_only                   30   13181    2906  -
notes_kb_records             9    3937     881  -
coding_project              16    7245    1604  -
deny_list                   40   20943    4693  -
typo                         3    1449     309  read_note
Enter fullscreen mode Exit fullscreen mode

read_only, ce sont tous les outils avec readOnlyHint: true. coding_project, ce sont les neuf lecteurs Note/KB/Record plus cinq lecteurs GitNest, edit_record_item et post_session_note. deny_list retire les 19 outils dont un assistant de code avec un checkout local n'a pas vraiment l'usage : les trois outils run, les dix outils propose_… et six outils de réseau d'agents et de profils.

Passer de 59 outils aux neuf lecteurs fait tomber les déclarations de 7,609 à 881 tokens, soit 88% de moins. La liste de refus n'arrive qu'à 4,693, parce que les outils d'écriture sont les plus chers et qu'elle en garde la plupart. Une liste d'autorisation est le choix le plus prévisible. Elle reste aussi courte quand le serveur ajoute des outils, et c'est la raison de la préférer.

La ligne typo est l'erreur que vous verrez vraiment. read_note n'est pas un outil (read_note_tab en est un), donc le profil se retrouve avec trois outils, et rien ne proteste. Codex listera la Note sans problème, puis n'aura aucun moyen de l'ouvrir. Le simulateur affiche les noms inconnus. Codex non, d'après ce que montrent la documentation et le code du filtre.

Combien de temps coûte le handshake ?

catalog.py --runs 10 a chronométré les trois requêtes de bout en bout, deux fois : une fois sur une seule connexion TLS, une fois avec une nouvelle connexion par requête :

Lot (n=10 chacun) médiane initialize médiane tools/list médiane du démarrage complet min–max
une connexion, lot 1 482 ms 826 ms 1,515 ms 1,034–2,899 ms
nouvelle connexion, lot 1 521 ms 704 ms 1,715 ms 1,122–3,107 ms
une connexion, lot 2 454 ms 828 ms 1,441 ms 741–1,915 ms
nouvelle connexion, lot 2 432 ms 478 ms 1,257 ms 1,088–1,557 ms

Réutiliser la connexion n'a pas fait de différence constante. La variation d'un run à l'autre était plus grande que l'effet. Filtrer ne change rien à ce chiffre, puisque Codex télécharge la liste complète dans tous les cas.

Trois réglages documentés de Codex se heurtent à ce chiffre. startup_timeout_sec vaut 10 secondes par défaut, et le run le plus lent ici a pris 3.1 s. mcp_optional_startup_grace_ms vaut 1,000 ms par défaut et, selon la documentation, contrôle combien de temps Codex attend les serveurs MCP optionnels lorsqu'il construit le catalogue d'outils initial. Chaque démarrage mesuré ici a pris plus longtemps. Si les outils d'osFoundry doivent être là dès le premier tour, les leviers documentés sont required = true, qui fait attendre le démarrage sur le serveur et le fait échouer s'il n'est pas prêt, et startup_readiness = "catalog", qui permet à un catalogue en cache valide de satisfaire le démarrage pendant que la connexion réelle s'établit. Les appels d'outils ont toujours besoin de la connexion réelle.

Qu'apportent les annotations ?

default_tools_approval_mode accepte auto, prompt, writes ou approve, et "the writes mode prompts for tools that aren't marked read-only". osFoundry marque 30 de ses 59 outils avec readOnlyHint: true. Avec writes, Codex les exécute sans demander et demande confirmation pour les 29 autres, soit exactement le partage que vous feriez à la main. Pour un outil précis, tools.<tool>.approval_mode remplace la valeur par défaut, et tools.<tool>.output_token_limit plafonne ce que le résultat d'un outil peut ajouter au contexte.

Le scope fonctionne de la même façon au niveau OAuth. Codex utilise scopes de la configuration s'il est défini. Sinon, il prend les scopes du challenge du serveur ou de ses métadonnées de ressource protégée, et celles d'osFoundry listent osfoundry:read, osfoundry:write, osfoundry:run et osfoundry:models. Avec scopes = ["osfoundry:read"], le token est émis pour la lecture seule, et chaque outil d'écriture déclare osfoundry:write dans ses securitySchemes, quoi que dise enabled_tools.

Quel filtre est le vrai ?

Il y en a trois, et un seul est une frontière de sécurité.

  • enabled_tools / disabled_tools dans config.toml décident de ce qui est proposé au modèle. Quiconque peut modifier le fichier peut le changer.
  • La liste de cases Tools dans l'onglet Connected d'osFoundry est stockée sur la connexion sous allowed_tools. null signifie « tous les outils pris en charge, actuels et futurs ». C'est le propriétaire qui la définit, et un fichier local ne peut pas l'élargir.
  • L'appartenance aux projets et la vérification d'accès à chaque appel décident de ce que n'importe quel outil peut toucher. Une liste d'outils complète sur une connexion sans projet ne lit rien.

Le filtre client relève de l'hygiène du prompt. Utilisez-le pour les tokens et pour les choix du modèle, et les deux autres pour les permissions.

Si vous le configurez vous-même

  • Pour un serveur avec des dizaines d'outils, préférez enabled_tools à disabled_tools. C'est plus court en tokens et stable quand le serveur ajoute des outils.
  • Écrivez les noms MCP bruts, et comparez votre liste avec un tools/list récent quand le serveur est mis à jour. Les noms inconnus échouent en silence.
  • Définissez scopes explicitement, pour que le token n'ait pas plus de droits que ce que prévoit la configuration.
  • Associez default_tools_approval_mode = "writes" à un serveur qui renseigne readOnlyHint honnêtement. Le partage des confirmations ne vaut que ce que valent les annotations.
  • Si le serveur est à une seconde ou plus, choisissez entre required = true et startup_readiness = "catalog" plutôt que de vous en remettre au délai de grâce par défaut de 1,000 ms.
  • Gardez les instructions du serveur sous 512 caractères et compréhensibles seules. Elles sont envoyées à chaque session, comme la liste d'outils.

Le lien entre la liste d'outils et le budget de contexte en général est expliqué sur la page du glossaire consacrée à la fenêtre de contexte d'osFoundry.

Top comments (0)