Dev.to · 10 min read

How many tokens does osFoundry's MCP tool list cost Codex, and how do you trim it per project?

How many tokens does osFoundry's MCP tool list cost Codex, and how do you trim it per project?

This is the call an MCP client makes right after the handshake, sent with curl to osFoundry's server from a Mac in Tokyo on 2026-11-06. No token is needed for it: $ 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… ]}} One entry out of the 59: {"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 connects to this server with a [mcp_servers.osfoundry] table and a url. Unless the client defers some of them, every request Codex makes to the model then carries these 59 declarations in some form. The questions below are the ones you hit when you decide how many of them it should carry. What does Codex fetch before the first turn? Three POSTs to the same URL: initialize, the notifications/initialized notification, and tools/list. The live server answers them like this: Request Status Body initialize 200 308 bytes: protocol 2025-06-18, osfoundry 0.1.0, tools.listChanged: false, instructions notifications/initialized 202 empty tools/list 200 45,368 bytes, 59 tools tools/call without a token 401 WWW-Authenticate with resource_metadata listChanged: false means the server won't announce changes to the list mid-session. The list Codex gets at startup is the list for the session. The instructions string is 125 characters: "Use list_granted_resources first. Every Note edit must use the exact tab ID. Local-only data is unavailable to cloud clients." OpenAI's Codex docs say Codex reads this field as server-wide guidance and that the first 512 characters should stand on their own. This one fits with room to spare. The 401 is where OAuth starts. That part, and why osFoundry checks access on every call, is covered in a separate post on the same endpoint from Claude's side. Here only the catalogue matters. How many tokens are 59 tools? What reaches the model isn't the raw JSON. Codex turns each MCP tool into a function declaration. The name, the description and the input schema are what the model needs. title, annotations and securitySchemes are protocol metadata the client uses itself. OpenAI doesn't publish how tool declarations are counted on its side, so the numbers below count the payload, not the bill. Measured with tiktoken 0.14.0 and the o200k_base encoding, on compact JSON: What Tokens Raw tools/list result, all fields 10,494 59 declarations (name with mcp__osfoundry__ prefix, description, schema) 7,609 of which descriptions 2,194 of which input schemas 4,118 of which the mcp__osfoundry__ prefixes 413 Metadata that stays in the client (title, annotations, securitySchemes) 3,590 The median declaration is 116 tokens. The largest is edit_note_tab at 336, then send_channel_message (237) and upload_linked_chat_artifact (233). The smallest is list_granted_resources at 57. By scope: the 30 osfoundry:read tools come to 2,906 tokens, the 26 osfoundry:write tools to 4,378, and the 3 osfoundry:run tools to 329. Writes cost more per tool because they carry expected_version, idempotency_key and the content being written. About 7,600 tokens sit in front of every request whether the model calls osFoundry or not. Cached prompt prefixes make that cheaper than it sounds. It still takes up context, and 59 names compete for the model's attention when it decides which tool fits. Where does Codex apply enabled_tools? The keys are documented on OpenAI's Codex MCP page: enabled_tools is an allow list, disabled_tools a deny list "applied after enabled_tools". The implementation is in codex-rs/codex-mcp/src/tools.rs in the openai/codex repository: 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, filter: &ToolFilter) -> Vec { tools .into_iter() .filter(|tool| filter.allows(&tool.tool.name)) .collect() } Two consequences. The filter compares tool.tool.name, the raw MCP name, so you write read_note_tab in config.toml, not mcp__osfoundry__read_note_tab. The prefix is added later, in the same file, when the model-visible names are built. And a set lookup never fails. A name that doesn't exist in tools/list matches nothing and says nothing. The filter runs in the client after tools/list has arrived. It shortens the prompt. It doesn't shorten the download, and the server knows nothing about it. Rebuilding the client side No codex binary is installed on the Mac this was measured on (Apple M1). catalog.py stands in for Codex's startup. It's 92 lines and runs with uv run --python 3.12 --with tiktoken. It reads [mcp_servers.*] tables from a TOML file, sends the three requests to the live endpoint over http.client, applies the same allow-then-deny rule to the raw names, builds mcp__osfoundry__ declarations and counts them: 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) The profile file has one table per configuration. A real config.toml would have one [mcp_servers.osfoundry] table, global or 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 is every tool with readOnlyHint: true. coding_project is the nine Note/KB/Record readers plus five GitNest readers, edit_record_item and post_session_note. deny_list removes the 19 tools a coding assistant with a local checkout has little use for: the three run tools, the ten propose_… tools and six agent-network and profile tools. Going from 59 tools to the nine readers cuts the declarations from 7,609 to 881 tokens, 88% less. The deny list only gets to 4,693, because writes are the expensive tools and it keeps most of them. An allow list is the more predictable choice. It also stays short when the server adds tools, which is the reason to prefer it. The typo row is the failure you'll actually see. read_note isn't a tool (read_note_tab is), so the profile ends up with three tools, and nothing complains. Codex will happily list the Note and then have no way to open it. The simulator prints unknown names. Codex doesn't, as far as the documentation and the filter code show. What does the handshake cost in time? catalog.py --runs 10 timed the three requests end to end, twice, once on one TLS connection and once with a new connection per request: Batch (n=10 each) initialize median tools/list median whole startup median min–max one connection, batch 1 482 ms 826 ms 1,515 ms 1,034–2,899 ms new connection, batch 1 521 ms 704 ms 1,715 ms 1,122–3,107 ms one connection, batch 2 454 ms 828 ms 1,441 ms 741–1,915 ms new connection, batch 2 432 ms 478 ms 1,257 ms 1,088–1,557 ms Connection reuse made no consistent difference. Variation between runs was larger than the effect. Filtering doesn't change this number at all, since Codex downloads the full list either way. Three documented Codex settings meet this number. startup_timeout_sec defaults to 10 seconds, and the slowest run here took 3.1 s. mcp_optional_startup_grace_ms defaults to 1,000 ms and, per the docs, "controls how long Codex waits for optional MCP servers when building the initial tool catalog". Every startup measured here took longer than that. If osFoundry's tools must be there from the first turn, the documented levers are required = true, which makes startup wait for the server and fail if it isn't ready, and startup_readiness = "catalog", which lets a valid cached catalogue satisfy startup while the live connection comes up. Tool calls still need the live connection. What do the annotations buy you? default_tools_approval_mode takes auto, prompt, writes or approve, and "the writes mode prompts for tools that aren't marked read-only". osFoundry marks 30 of its 59 tools readOnlyHint: true. With writes, Codex runs those without asking and prompts for the other 29, which is the split you'd draw by hand. For a single tool, tools..approval_mode overrides the default, and tools..output_token_limit caps what one tool's result may add to the context. The scope works the same way at the OAuth layer. Codex uses scopes from the config if it's set. Otherwise it takes the scopes from the server's challenge or its protected-resource metadata, and osFoundry's lists osfoundry:read, osfoundry:write, osfoundry:run and osfoundry:models. With scopes = ["osfoundry:read"] the token is issued for reading only, and every write tool declares osfoundry:write in its securitySchemes, whatever enabled_tools says. Which filter is the real one? There are three, and only one of them is a security boundary. enabled_tools / disabled_tools in config.toml decide what the model is offered. Anyone who can edit the file can change it. The Tools checklist in osFoundry's Connected tab is stored on the connection as allowed_tools. null means "every current and future supported tool". The owner sets it, and a local file can't widen it. Project membership and the per-call access check decide what any tool can touch. A full tool list on a connection with no projects reads nothing. Example: The Tools checklist with only the read tools ticked, then Save tools The client filter is prompt hygiene. Use it for tokens and for the model's choices, and use the other two for permissions. If you wire this up yourself Prefer enabled_tools over disabled_tools for a server with dozens of tools. It's shorter in tokens and stable when the server adds tools. Write raw MCP names, and diff your list against a fresh tools/list when the server updates. Unknown names fail silently. Set scopes explicitly, so the token carries no more than the config intends. Pair default_tools_approval_mode = "writes" with a server that sets readOnlyHint honestly. The prompt split is only as good as the annotations. If the server sits a second or more away, decide between required = true and startup_readiness = "catalog" rather than leaving it to the 1,000 ms grace default. Keep the server's instructions under 512 characters and self-contained. It's sent with every session, like the tool list. How the tool list relates to the context budget in general is on osFoundry's context window glossary page.

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News