DEV Community

Cover image for ¿Cuántos tokens le cuesta a Codex la lista de herramientas MCP de osFoundry y cómo recortarla por proyecto?
BBT for BBT Español

Posted on

¿Cuántos tokens le cuesta a Codex la lista de herramientas MCP de osFoundry y cómo recortarla por proyecto?

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… ]}}
Enter fullscreen mode Exit fullscreen mode

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"]}]}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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_tools en config.toml deciden 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. null significa "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_tools a disabled_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/list recién descargado cuando el servidor se actualice. Los nombres desconocidos fallan en silencio.
  • Define scopes explí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 ponga readOnlyHint con 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 = true y startup_readiness = "catalog" en lugar de dejarlo en el margen de 1,000 ms por defecto.
  • Mantén las instructions del 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)