Esta es la llamada que hace un cliente MCP justo después del handshake, enviada con curl al servidor de osFoundry desde un Mac en Tokio el 2026-11-06. No hace falta ningún 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 de las 59 entradas:
{"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 se conecta a este servidor con una tabla [mcp_servers.osfoundry] y una url. A menos que el cliente aplace algunas, cada petición que Codex hace al modelo lleva estas 59 declaraciones de una forma u otra. Las preguntas de abajo son las que te encuentras cuando decides cuántas debería llevar.
¿Qué descarga Codex antes del primer turno?
Tres POST a la misma URL: initialize, la notificación notifications/initialized y tools/list. El servidor en producción responde así:
| Petición | Estado | Cuerpo |
|---|---|---|
initialize |
200 | 308 bytes: protocolo 2025-06-18, osfoundry 0.1.0, tools.listChanged: false, instrucciones |
notifications/initialized |
202 | vacío |
tools/list |
200 | 45,368 bytes, 59 herramientas |
tools/call sin token |
401 |
WWW-Authenticate con resource_metadata
|
listChanged: false significa que el servidor no avisará de cambios en la lista a mitad de sesión. La lista que Codex recibe al arrancar es la lista de toda la sesión.
La cadena instructions tiene 125 caracteres: "Use list_granted_resources first. Every Note edit must use the exact tab ID. Local-only data is unavailable to cloud clients." La documentación de Codex de OpenAI dice que Codex lee este campo como guía para todo el servidor y que los primeros 512 caracteres deberían entenderse por sí solos. Esta cabe de sobra.
El 401 es donde empieza OAuth. Esa parte, y por qué osFoundry comprueba el acceso en cada llamada, se trata en otro post sobre el mismo endpoint desde el lado de Claude. Aquí solo importa el catálogo.
¿Cuántos tokens son 59 herramientas?
Lo que llega al modelo no es el JSON en bruto. Codex convierte cada herramienta MCP en una declaración de función. El nombre, la descripción y el esquema de entrada son lo que el modelo necesita. title, annotations y securitySchemes son metadatos del protocolo que usa el propio cliente. OpenAI no publica cómo cuenta las declaraciones de herramientas por su lado, así que los números de abajo miden el payload, no la factura.
Medido con tiktoken 0.14.0 y la codificación o200k_base, sobre JSON compacto:
| Qué | Tokens |
|---|---|
Resultado de tools/list en bruto, todos los campos |
10,494 |
59 declaraciones (nombre con prefijo mcp__osfoundry__, descripción, esquema) |
7,609 |
| de ello, descripciones | 2,194 |
| de ello, esquemas de entrada | 4,118 |
de ello, prefijos mcp__osfoundry__
|
413 |
| Metadatos que se quedan en el cliente (title, annotations, securitySchemes) | 3,590 |
La declaración mediana ocupa 116 tokens. La más grande es edit_note_tab con 336, seguida de send_channel_message (237) y upload_linked_chat_artifact (233). La más pequeña es list_granted_resources con 57.
Por scope: las 30 herramientas osfoundry:read suman 2,906 tokens, las 26 osfoundry:write 4,378 y las 3 osfoundry:run 329. Las de escritura cuestan más por herramienta porque llevan expected_version, idempotency_key y el contenido que se escribe.
Unos 7,600 tokens van delante de cada petición, llame el modelo a osFoundry o no. Los prefijos de prompt en caché lo abaratan más de lo que parece. Aun así ocupa contexto, y 59 nombres compiten por la atención del modelo cuando decide qué herramienta encaja.
¿Dónde aplica Codex enabled_tools?
Las claves están documentadas en la página de MCP de Codex de OpenAI: enabled_tools es una lista de permitidos y disabled_tools una lista de denegados "applied after enabled_tools". La implementación está en codex-rs/codex-mcp/src/tools.rs en el repositorio 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()
}
Dos consecuencias. El filtro compara tool.tool.name, el nombre MCP en bruto, así que en config.toml escribes read_note_tab, no mcp__osfoundry__read_note_tab. El prefijo se añade más tarde, en el mismo archivo, cuando se construyen los nombres que ve el modelo. Y una búsqueda en un set nunca falla. Un nombre que no existe en tools/list no coincide con nada y no dice nada.
El filtro se ejecuta en el cliente después de que llegue tools/list. Acorta el prompt. No acorta la descarga, y el servidor no sabe nada de él.
Reconstruir el lado del cliente
En el Mac donde se midió esto (Apple M1) no hay ningún binario codex instalado. catalog.py hace de arranque de Codex. Tiene 92 líneas y se ejecuta con uv run --python 3.12 --with tiktoken. Lee las tablas [mcp_servers.*] de un archivo TOML, envía las tres peticiones al endpoint real con http.client, aplica la misma regla de primero permitir y luego denegar sobre los nombres en bruto, construye las declaraciones mcp__osfoundry__<tool> y las cuenta:
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)
El archivo de perfiles tiene una tabla por configuración. Un config.toml real tendría una sola tabla [mcp_servers.osfoundry], global o por repositorio:
[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"]
Salida:
$ 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 son todas las herramientas con readOnlyHint: true. coding_project son los nueve lectores de Note/KB/Record más cinco lectores de GitNest, edit_record_item y post_session_note. deny_list quita las 19 herramientas que poco le sirven a un asistente de código con un checkout local: las tres de ejecución, las diez propose_… y seis de la red de agentes y de perfiles.
Pasar de 59 herramientas a los nueve lectores baja las declaraciones de 7,609 a 881 tokens, un 88% menos. La lista de denegados solo llega a 4,693, porque las escrituras son las herramientas caras y conserva la mayoría. Una lista de permitidos es la opción más predecible. Además sigue siendo corta cuando el servidor añade herramientas, y por eso es la que conviene.
La fila typo es el fallo que de verdad vas a ver. read_note no es una herramienta (read_note_tab sí), así que el perfil se queda con tres herramientas y nada se queja. Codex listará la Note sin problema y luego no tendrá forma de abrirla. El simulador imprime los nombres desconocidos. Codex no, por lo que muestran la documentación y el código del filtro.
¿Cuánto tiempo cuesta el handshake?
catalog.py --runs 10 cronometró las tres peticiones de punta a punta, dos veces: una sobre una sola conexión TLS y otra con una conexión nueva por petición:
| Lote (n=10 cada uno) | mediana initialize
|
mediana tools/list
|
mediana del arranque completo | mín–máx |
|---|---|---|---|---|
| una conexión, lote 1 | 482 ms | 826 ms | 1,515 ms | 1,034–2,899 ms |
| conexión nueva, lote 1 | 521 ms | 704 ms | 1,715 ms | 1,122–3,107 ms |
| una conexión, lote 2 | 454 ms | 828 ms | 1,441 ms | 741–1,915 ms |
| conexión nueva, lote 2 | 432 ms | 478 ms | 1,257 ms | 1,088–1,557 ms |
Reutilizar la conexión no marcó una diferencia consistente. La variación entre ejecuciones fue mayor que el efecto. Filtrar no cambia este número en absoluto, porque Codex descarga la lista completa de todas formas.
Hay tres ajustes documentados de Codex que chocan con este número. startup_timeout_sec vale 10 segundos por defecto, y la ejecución más lenta aquí tardó 3.1 s. mcp_optional_startup_grace_ms vale 1,000 ms por defecto y, según la documentación, controla cuánto espera Codex a los servidores MCP opcionales al construir el catálogo inicial de herramientas. Todos los arranques medidos aquí tardaron más que eso. Si las herramientas de osFoundry tienen que estar desde el primer turno, las palancas documentadas son required = true, que hace que el arranque espere al servidor y falle si no está listo, y startup_readiness = "catalog", que deja que un catálogo en caché válido satisfaga el arranque mientras se levanta la conexión real. Las llamadas a herramientas siguen necesitando la conexión real.
¿Qué te aportan las anotaciones?
default_tools_approval_mode acepta auto, prompt, writes o approve, y "the writes mode prompts for tools that aren't marked read-only". osFoundry marca 30 de sus 59 herramientas con readOnlyHint: true. Con writes, Codex ejecuta esas sin preguntar y pide confirmación para las otras 29, que es justo la división que harías a mano. Para una sola herramienta, tools.<tool>.approval_mode sobrescribe el valor por defecto, y tools.<tool>.output_token_limit limita lo que el resultado de una herramienta puede añadir al contexto.
El scope funciona igual en la capa OAuth. Codex usa scopes de la configuración si está definido. Si no, toma los scopes del challenge del servidor o de sus metadatos de recurso protegido, y los de osFoundry listan osfoundry:read, osfoundry:write, osfoundry:run y osfoundry:models. Con scopes = ["osfoundry:read"] el token se emite solo para lectura, y cada herramienta de escritura declara osfoundry:write en sus securitySchemes, diga lo que diga enabled_tools.
¿Qué filtro es el que cuenta de verdad?
Hay tres, y solo uno de ellos es una frontera de seguridad.
-
enabled_tools/disabled_toolsenconfig.tomldeciden qué se le ofrece al modelo. Cualquiera que pueda editar el archivo puede cambiarlo. - La lista de Tools en la pestaña Connected de osFoundry se guarda en la conexión como
allowed_tools.nullsignifica "todas las herramientas soportadas, actuales y futuras". La define el propietario, y un archivo local no puede ampliarla. - La pertenencia a proyectos y la comprobación de acceso en cada llamada deciden qué puede tocar cualquier herramienta. Una lista completa de herramientas en una conexión sin proyectos no lee nada.
El filtro del cliente es higiene del prompt. Úsalo para los tokens y para las decisiones del modelo, y usa los otros dos para los permisos.
Si lo montas tú
- Con un servidor de decenas de herramientas, prefiere
enabled_toolsadisabled_tools. Ocupa menos tokens y se mantiene estable cuando el servidor añade herramientas. - Escribe los nombres MCP en bruto y compara tu lista con un
tools/listrecién descargado cuando el servidor se actualice. Los nombres desconocidos fallan en silencio. - Define
scopesexplícitamente, para que el token no lleve más de lo que pretende la configuración. - Combina
default_tools_approval_mode = "writes"con un servidor que pongareadOnlyHintcon honestidad. La división de confirmaciones es tan buena como las anotaciones. - Si el servidor está a un segundo o más, decide entre
required = trueystartup_readiness = "catalog"en lugar de dejarlo en el margen de 1,000 ms por defecto. - Mantén las
instructionsdel servidor por debajo de 512 caracteres y que se entiendan solas. Se envían en cada sesión, igual que la lista de herramientas.
Cómo se relaciona la lista de herramientas con el presupuesto de contexto en general está en la página del glosario sobre la ventana de contexto de osFoundry.
Top comments (0)