Questa è la chiamata che un client MCP fa subito dopo l'handshake, inviata con curl al server di osFoundry da un Mac a Tokyo il 2026-11-06. Non serve nessun token:
$ 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… ]}}
Una delle 59 voci:
{"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"]}]}
Codex CLI si collega a questo server con una tabella [mcp_servers.osfoundry] e un url. A meno che il client non ne rimandi alcune, ogni richiesta che Codex fa poi al modello si porta dietro queste 59 dichiarazioni in qualche forma. Le domande qui sotto sono quelle in cui ti imbatti quando decidi quante dovrebbe portarsene dietro.
Cosa scarica Codex prima del primo turno?
Tre POST allo stesso URL: initialize, la notifica notifications/initialized e tools/list. Il server in produzione risponde così:
| Richiesta | Stato | Body |
|---|---|---|
initialize |
200 | 308 bytes: protocollo 2025-06-18, osfoundry 0.1.0, tools.listChanged: false, instructions |
notifications/initialized |
202 | vuoto |
tools/list |
200 | 45,368 bytes, 59 tool |
tools/call senza token |
401 |
WWW-Authenticate con resource_metadata
|
listChanged: false vuol dire che il server non annuncerà modifiche alla lista a metà sessione. La lista che Codex riceve all'avvio è quella di tutta la sessione.
La stringa instructions è di 125 caratteri: "Use list_granted_resources first. Every Note edit must use the exact tab ID. Local-only data is unavailable to cloud clients." La documentazione di Codex di OpenAI dice che Codex legge questo campo come indicazione valida per tutto il server e che i primi 512 caratteri dovrebbero reggersi da soli. Questa ci sta con ampio margine.
Il 401 è il punto in cui parte OAuth. Quella parte, e il motivo per cui osFoundry controlla l'accesso a ogni chiamata, è trattata in un altro post sullo stesso endpoint dal lato di Claude. Qui conta solo il catalogo.
Quanti token sono 59 tool?
Al modello non arriva il JSON grezzo. Codex trasforma ogni tool MCP in una dichiarazione di funzione. Nome, descrizione e schema di input sono ciò che serve al modello. title, annotations e securitySchemes sono metadati di protocollo che usa il client stesso. OpenAI non pubblica come vengono contate le dichiarazioni dei tool dalla sua parte, quindi i numeri qui sotto contano il payload, non la fattura.
Misurato con tiktoken 0.14.0 e l'encoding o200k_base, su JSON compatto:
| Cosa | Token |
|---|---|
Risultato grezzo di tools/list, tutti i campi |
10,494 |
59 dichiarazioni (nome con prefisso mcp__osfoundry__, descrizione, schema) |
7,609 |
| di cui descrizioni | 2,194 |
| di cui schemi di input | 4,118 |
di cui i prefissi mcp__osfoundry__
|
413 |
| Metadati che restano nel client (title, annotations, securitySchemes) | 3,590 |
La dichiarazione mediana è di 116 token. La più grande è edit_note_tab con 336, poi send_channel_message (237) e upload_linked_chat_artifact (233). La più piccola è list_granted_resources con 57.
Per scope: i 30 tool osfoundry:read arrivano a 2,906 token, i 26 tool osfoundry:write a 4,378 e i 3 tool osfoundry:run a 329. Quelli di scrittura costano di più per tool perché si portano dietro expected_version, idempotency_key e il contenuto da scrivere.
Circa 7,600 token stanno davanti a ogni richiesta, che il modello chiami osFoundry o no. I prefissi di prompt in cache lo rendono più economico di quanto sembri. Occupa comunque contesto, e 59 nomi si contendono l'attenzione del modello quando decide quale tool è quello giusto.
Dove applica Codex enabled_tools?
Le chiavi sono documentate nella pagina MCP di Codex di OpenAI: enabled_tools è una allow list, disabled_tools una deny list "applied after enabled_tools". L'implementazione sta in codex-rs/codex-mcp/src/tools.rs nel repository 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()
}
Due conseguenze. Il filtro confronta tool.tool.name, il nome MCP grezzo, quindi in config.toml scrivi read_note_tab, non mcp__osfoundry__read_note_tab. Il prefisso viene aggiunto dopo, nello stesso file, quando si costruiscono i nomi visibili al modello. E una ricerca in un set non fallisce mai. Un nome che non esiste in tools/list non corrisponde a niente e non dice niente.
Il filtro gira nel client dopo che tools/list è arrivato. Accorcia il prompt. Non accorcia il download, e il server non ne sa nulla.
Ricostruire il lato client
Sul Mac dove sono state fatte queste misure (Apple M1) non c'è nessun binario codex installato. catalog.py fa le veci dell'avvio di Codex. Sono 92 righe e si lancia con uv run --python 3.12 --with tiktoken. Legge le tabelle [mcp_servers.*] da un file TOML, invia le tre richieste all'endpoint reale tramite http.client, applica ai nomi grezzi la stessa regola "prima consenti, poi nega", costruisce le dichiarazioni mcp__osfoundry__<tool> e le conta:
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)
Il file dei profili ha una tabella per configurazione. Un config.toml vero avrebbe una sola tabella [mcp_servers.osfoundry], globale o per repository:
[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"]
Output:
$ 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
read_only sono tutti i tool con readOnlyHint: true. coding_project sono i nove lettori Note/KB/Record più cinque lettori GitNest, edit_record_item e post_session_note. deny_list toglie i 19 tool che a un assistente di coding con un checkout locale servono a poco: i tre tool run, i dieci tool propose_… e sei tool di rete degli agenti e di profilo.
Passare da 59 tool ai nove lettori fa scendere le dichiarazioni da 7,609 a 881 token, l'88% in meno. La deny list arriva solo a 4,693, perché i tool di scrittura sono quelli costosi e lei ne tiene la maggior parte. Una allow list è la scelta più prevedibile. Resta anche corta quando il server aggiunge tool, ed è questo il motivo per preferirla.
La riga typo è l'errore che vedrai davvero. read_note non è un tool (read_note_tab sì), quindi il profilo finisce con tre tool, e nessuno si lamenta. Codex elencherà tranquillamente la Note e poi non avrà modo di aprirla. Il simulatore stampa i nomi sconosciuti. Codex no, per quanto mostrano la documentazione e il codice del filtro.
Quanto tempo costa l'handshake?
catalog.py --runs 10 ha cronometrato le tre richieste dall'inizio alla fine, due volte: una su un'unica connessione TLS e una con una connessione nuova per ogni richiesta:
| Batch (n=10 ciascuno) | mediana initialize
|
mediana tools/list
|
mediana avvio completo | min–max |
|---|---|---|---|---|
| una connessione, batch 1 | 482 ms | 826 ms | 1,515 ms | 1,034–2,899 ms |
| connessione nuova, batch 1 | 521 ms | 704 ms | 1,715 ms | 1,122–3,107 ms |
| una connessione, batch 2 | 454 ms | 828 ms | 1,441 ms | 741–1,915 ms |
| connessione nuova, batch 2 | 432 ms | 478 ms | 1,257 ms | 1,088–1,557 ms |
Riutilizzare la connessione non ha fatto una differenza costante. La variazione tra un run e l'altro era più grande dell'effetto. Filtrare non cambia per niente questo numero, perché Codex scarica comunque la lista completa.
Tre impostazioni documentate di Codex si scontrano con questo numero. startup_timeout_sec vale 10 secondi di default, e il run più lento qui ha impiegato 3.1 s. mcp_optional_startup_grace_ms vale 1,000 ms di default e, secondo la documentazione, controlla quanto Codex aspetta i server MCP opzionali quando costruisce il catalogo iniziale dei tool. Ogni avvio misurato qui ha impiegato di più. Se i tool di osFoundry devono esserci dal primo turno, le leve documentate sono required = true, che fa aspettare il server all'avvio e lo fa fallire se non è pronto, e startup_readiness = "catalog", che lascia che un catalogo in cache valido soddisfi l'avvio mentre la connessione reale si stabilisce. Le chiamate ai tool hanno comunque bisogno della connessione reale.
Cosa ti danno le annotazioni?
default_tools_approval_mode accetta auto, prompt, writes o approve, e "the writes mode prompts for tools that aren't marked read-only". osFoundry marca 30 dei suoi 59 tool con readOnlyHint: true. Con writes, Codex esegue quelli senza chiedere e chiede conferma per gli altri 29, che è proprio la divisione che faresti a mano. Per un singolo tool, tools.<tool>.approval_mode sovrascrive il default, e tools.<tool>.output_token_limit limita quanto il risultato di un tool può aggiungere al contesto.
Lo scope funziona allo stesso modo a livello OAuth. Codex usa scopes dalla configurazione se è impostato. Altrimenti prende gli scope dalla challenge del server o dai suoi metadati della risorsa protetta, e quelli di osFoundry elencano osfoundry:read, osfoundry:write, osfoundry:run e osfoundry:models. Con scopes = ["osfoundry:read"] il token viene emesso solo per la lettura, e ogni tool di scrittura dichiara osfoundry:write nei suoi securitySchemes, qualunque cosa dica enabled_tools.
Qual è il filtro che conta davvero?
Ce ne sono tre, e solo uno è un confine di sicurezza.
-
enabled_tools/disabled_toolsinconfig.tomldecidono cosa viene offerto al modello. Chiunque possa modificare il file può cambiarlo. - La checklist Tools nella scheda Connected di osFoundry è salvata sulla connessione come
allowed_tools.nullsignifica "tutti i tool supportati, attuali e futuri". La imposta il proprietario, e un file locale non può allargarla. - L'appartenenza ai progetti e il controllo di accesso a ogni chiamata decidono cosa può toccare qualsiasi tool. Una lista di tool completa su una connessione senza progetti non legge nulla.
Il filtro del client è igiene del prompt. Usalo per i token e per le scelte del modello, e usa gli altri due per i permessi.
Se lo configuri da solo
- Per un server con decine di tool, preferisci
enabled_toolsadisabled_tools. Costa meno token e resta stabile quando il server aggiunge tool. - Scrivi i nomi MCP grezzi, e confronta la tua lista con un
tools/listappena scaricato quando il server si aggiorna. I nomi sconosciuti falliscono in silenzio. - Imposta
scopesin modo esplicito, così il token non porta più di quanto prevede la configurazione. - Abbina
default_tools_approval_mode = "writes"a un server che impostareadOnlyHintonestamente. La divisione delle conferme vale quanto le annotazioni. - Se il server sta a un secondo o più di distanza, scegli tra
required = trueestartup_readiness = "catalog"invece di lasciare tutto al margine di default di 1,000 ms. - Tieni le
instructionsdel server sotto i 512 caratteri e comprensibili da sole. Vengono inviate a ogni sessione, come la lista dei tool.
Come si lega la lista dei tool al budget di contesto in generale è spiegato nella pagina del glossario sulla context window di osFoundry.
Top comments (0)