You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Platform: Pi. The ctx_* tool surface costs 6,219 tokens (27,074 bytes) in tools/list.
On Pi specifically, the adapter registers each MCP tool individually via pi.registerTool() (build/adapters/pi/extension.js:127), so all 11 tools sit in the tool registry for every model call in the session — regardless of whether any ctx_* tool is invoked. I have only verified this registration path for Pi; other hosts may handle the payload differently, so the per-call framing below is scoped to Pi.
Relevant precedent from the same adapter: the Pi routing anchor at extension.js:574 carries the comment "7KB routing block is too heavy for Pi's context budget", and was deliberately compressed for that reason. The static tool payload is comparable in size and is the remaining always-on cost on that budget.
Measured against context-mode@1.0.169 (current latest) by driving the stdio server directly over JSON-RPC and counting with a real tokenizer (gpt-tokenizer, o200k_base) rather than a bytes/4 estimate — token counts per field:
3.1% of a 200K window — small as a share, but it is resident: it occupies that share of every request in the session, and it is the one component a user cannot reduce.
Two details worth noting:
inputSchema is 37% of the total and near-parity with prose on some entries (ctx_batch_execute: 498 desc + 497 schema), so trimming descriptions alone would leave a large share in place.
The descriptions are mostly not descriptions. Splitting each description into its leading "what this tool does" sentence versus the trailing steering sections (WHEN / WHEN NOT / RETURNS / EXAMPLE / Think-in-Code narrative):
tokens
share
leading "what it does" sentence
194
5%
steering / teaching prose
3,059
80%
remainder (params, notes)
553
15%
ctx_execute is the extreme case: 9 tokens describe the tool ("Run code in a sandboxed subprocess."), 711 tokens argue for using it.
The MCP spec describes description as "a human-readable description of the tool… like a 'hint' to the model" and marks it optional, while inputSchema is required. Persuasion and worked examples are a different genre from a hint, and they are the genre that scales badly: it is billed on every call, forever, whereas the routing decision it informs happens at most once per session. Skills, the PreToolUse hook, and the routing anchor are all better-suited carriers — they are fetched when relevant.
Repro script (no auth or API key needed)
#!/usr/bin/env node
// Measure the static tools/list payload of the context-mode MCP server.// node probe.mjs [path-to-context-mode-package]// Default resolves context-mode from NODE_PATH / node_modules.import{spawn}from"node:child_process";import{createRequire}from"node:module";importfsfrom"node:fs";importpathfrom"node:path";letroot=process.argv[2];if(!root){try{constreq=createRequire(`${process.cwd()}/`);root=path.dirname(req.resolve("context-mode/package.json"));}catch{console.error("pass the package path: node probe.mjs <path-to-context-mode>",);process.exit(1);}}constentry=path.join(root,"build/server.js");if(!fs.existsSync(entry)){console.error(`no build/server.js under ${root}`);process.exit(1);}letversion="unknown";try{version=JSON.parse(fs.readFileSync(path.join(root,"package.json"),"utf8"),).version;}catch{/* cosmetic only */}constp=spawn(process.execPath,[entry],{stdio: ["pipe","pipe","pipe"]});letout="";p.stdout.on("data",(d)=>(out+=d));constsend=(o)=>p.stdin.write(`${JSON.stringify(o)}\n`);send({jsonrpc: "2.0",id: 1,method: "initialize",params: {protocolVersion: "2024-11-05",capabilities: {},clientInfo: {name: "probe",version: "1"},},});letasked=false;p.stdout.on("data",()=>{if(asked||!/"id":\s*1\b/.test(out))return;asked=true;send({jsonrpc: "2.0",method: "notifications/initialized"});send({jsonrpc: "2.0",id: 2,method: "tools/list",params: {}});});setTimeout(()=>{p.kill();constframe=out.split("\n").filter(Boolean).map((l)=>{try{returnJSON.parse(l);}catch{returnnull;}}).find((j)=>j?.id===2);if(!frame){console.error("no tools/list response");process.exit(1);}constrows=frame.result.tools.map((t)=>{constd=Buffer.byteLength(t.description||"");consts=Buffer.byteLength(JSON.stringify(t.inputSchema||{}));return{name: t.name, d, s,total: Buffer.byteLength(t.name)+d+s};}).sort((a,b)=>b.total-a.total);consttotal=rows.reduce((a,r)=>a+r.total,0);console.log(`context-mode v${version} — ${rows.length} tools\n`);console.log("tool desc schema total");for(constrofrows)console.log(r.name.padEnd(20),String(r.d).padStart(6),String(r.s).padStart(7),String(r.total).padStart(7),);console.log(`\nTOTAL ${total} bytes ≈ ${Math.round(total/4)} tokens`);console.log(`% of a 200K window: ${((total/4/200000)*100).toFixed(2)}`);},6000);
The script reports bytes and a bytes/4 approximation. The token figures in the table above come from encoding the same strings with gpt-tokenizer (o200k_base); for this corpus bytes/4 overstates by ~9% (actual ratio 4.35 bytes/token), so the byte output is a safe upper bound if you don't want the tokenizer dependency.
Use case
In Pi, doc-heavy, orchestration-heavy, and infra/ops sessions frequently invoke zeroctx_* tools, yet still carry ~6.2K tokens of tool definitions in every call.
The share of a 200K window (~3%) understates this, for two reasons:
It is non-reclaimable. Conversation history can be compacted; a resident tool payload cannot. It is the floor of every request.
It competes with the extension's own value. context-mode exists to keep bytes out of context; a session that never triggers a ctx_* call has paid ~6.2K tokens for that guarantee and received none of it.
It does not land in isolation
context-mode is rarely the only extension loaded. A realistic Pi session in my setup carries 48 tools: 11 from context-mode, 9 core Pi tools, and the rest from four other extensions (knowledge base, code intelligence, subagent orchestration) plus an MCP gateway.
Rather than extrapolate, I measured what Pi actually sends. Pi records totalTokens per turn in its
session transcripts, so the first substantive turn of a fresh session is a direct reading of resident
overhead — system prompt, skills, and the full tool payload, before any real work.
Across 71 fresh sessions on this machine:
first substantive prompt
min
15,725
p25
24,076
median
27,810
p75
41,652
So a fresh session here starts at roughly 25–28K tokens before the first instruction — about 14% of a 200K window, and well over a fifth in the heavier cases. Tool definitions are not the
only contributor (system prompt and skill preambles are in there too), but at 11 tools and 6,219
tokens context-mode is a consistent, measurable share of it.
For per-tool cost as a sanity check, two stdio servers I can measure precisely:
server
tools
tokens
per tool
github/github-mcp-server (default toolset)
44
10,023
228
context-mode
11
6,219
565
That is the part I'd flag: at 565 tokens/tool context-mode costs ~2.5× per tool what GitHub's
server does, and GitHub's covers a far larger API surface with four times the tools. The difference
is not capability, it is prose. When the baseline is already 25K+, a 6.2K component that is 80%
teaching material is a meaningful and easily reclaimable piece of it.
This is the same class of concern as #641 (routing-block cost, shipped as #680) and #964 (SessionStart injection knob), but for the static tool payload, which neither addresses. build/server.js has 11 unconditional server.registerTool("ctx_…") calls (lines 1455, 1810, 1988, 2279, 3100, 3331, 3559, 3772, 3901, 4068, 4394) with no gating; the only related env var, CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS, governs crash handlers. So today the only way to reduce this is to disable the extension entirely.
Proposed solution
These compose; (3) is the largest single win and the one I'd argue for most.
CONTEXT_MODE_TOOLS allow-list. GitHub's own MCP server ships GITHUB_TOOLSETS + GITHUB_TOOLS for exactly this reason (measured on their Docker image: all = 85 tools vs a hand-picked 4 via GITHUB_TOOLS — a ~15x difference in payload).
CONTEXT_MODE_TOOLS=ctx_execute,ctx_search # register only these
Skipping registration — rather than filtering the response — is what actually removes the bytes.
Fold the 5 admin tools into one ctx_admin({action}). Reclaims ~865 tokens with no capability loss; doctor/upgrade/purge/stats/insight are rare, deliberate operations.
Move the steering prose out of description entirely. Not a toggle — a separation of concerns. description states what the tool does and what it returns; the WHEN / WHEN NOT / EXAMPLE material moves to the context-mode skill and the PreToolUse hook, which are already in place and are consulted when relevant rather than billed every call. On the measured split that is the ~80% of description tokens, i.e. ~3.0K tokens — the single largest reclaimable item, and it needs no new env var.
If the concern is that routing quality depends on the prose being in-band, that is testable: ship terse descriptions behind CONTEXT_MODE_TERSE_DESCRIPTIONS=1 first and compare tool-choice behaviour. Shorten tool and skill descriptions for command palette readability #177 proposed one-line descriptions for palette readability and was closed unmerged; the tokens argument is a stronger case for the same change, and the descriptions have grown substantially since.
Happy to send a PR for (1) or (2) if either is acceptable in principle.
Alternatives considered
Disabling the extension entirely — works, but loses the tools in sessions where they would occasionally help.
The session-level figure above is reproducible on any Pi install:
# first substantive prompt size per session, across all recorded sessionsforfin~/.pi/agent/sessions/*/*.jsonl;do
grep -o '"totalTokens":[0-9]*'"$f"| cut -d: -f2 \
| awk '$1>5000{print; exit}'done| sort -n | awk '{a[NR]=$1} END{print "n="NR, "median="a[int(NR/2)]}'
Platform: Pi. The
ctx_*tool surface costs 6,219 tokens (27,074 bytes) intools/list.On Pi specifically, the adapter registers each MCP tool individually via
pi.registerTool()(build/adapters/pi/extension.js:127), so all 11 tools sit in the tool registry for every model call in the session — regardless of whether anyctx_*tool is invoked. I have only verified this registration path for Pi; other hosts may handle the payload differently, so the per-call framing below is scoped to Pi.Relevant precedent from the same adapter: the Pi routing anchor at
extension.js:574carries the comment "7KB routing block is too heavy for Pi's context budget", and was deliberately compressed for that reason. The static tool payload is comparable in size and is the remaining always-on cost on that budget.Measured against
context-mode@1.0.169(currentlatest) by driving the stdio server directly over JSON-RPC and counting with a real tokenizer (gpt-tokenizer, o200k_base) rather than a bytes/4 estimate — token counts per field:ctx_executectx_batch_executectx_searchctx_fetch_and_indexctx_indexctx_execute_filectx_purgectx_insightctx_upgradectx_doctorctx_statsstats,doctor,upgrade,purge,insight): 865 tokensTwo details worth noting:
inputSchemais 37% of the total and near-parity with prose on some entries (ctx_batch_execute: 498 desc + 497 schema), so trimming descriptions alone would leave a large share in place.The descriptions are mostly not descriptions. Splitting each
descriptioninto its leading "what this tool does" sentence versus the trailing steering sections (WHEN/WHEN NOT/RETURNS/EXAMPLE/ Think-in-Code narrative):ctx_executeis the extreme case: 9 tokens describe the tool ("Run code in a sandboxed subprocess."), 711 tokens argue for using it.The MCP spec describes
descriptionas "a human-readable description of the tool… like a 'hint' to the model" and marks it optional, whileinputSchemais required. Persuasion and worked examples are a different genre from a hint, and they are the genre that scales badly: it is billed on every call, forever, whereas the routing decision it informs happens at most once per session. Skills, the PreToolUse hook, and the routing anchor are all better-suited carriers — they are fetched when relevant.Repro script (no auth or API key needed)
Run:
node probe.mjs /path/to/node_modules/context-modeThe script reports bytes and a
bytes/4approximation. The token figures in the table above come from encoding the same strings withgpt-tokenizer(o200k_base); for this corpusbytes/4overstates by ~9% (actual ratio 4.35 bytes/token), so the byte output is a safe upper bound if you don't want the tokenizer dependency.Use case
In Pi, doc-heavy, orchestration-heavy, and infra/ops sessions frequently invoke zero
ctx_*tools, yet still carry ~6.2K tokens of tool definitions in every call.The share of a 200K window (~3%) understates this, for two reasons:
context-modeexists to keep bytes out of context; a session that never triggers actx_*call has paid ~6.2K tokens for that guarantee and received none of it.It does not land in isolation
context-modeis rarely the only extension loaded. A realistic Pi session in my setup carries 48 tools: 11 fromcontext-mode, 9 core Pi tools, and the rest from four other extensions (knowledge base, code intelligence, subagent orchestration) plus an MCP gateway.Rather than extrapolate, I measured what Pi actually sends. Pi records
totalTokensper turn in itssession transcripts, so the first substantive turn of a fresh session is a direct reading of resident
overhead — system prompt, skills, and the full tool payload, before any real work.
Across 71 fresh sessions on this machine:
So a fresh session here starts at roughly 25–28K tokens before the first instruction — about
14% of a 200K window, and well over a fifth in the heavier cases. Tool definitions are not the
only contributor (system prompt and skill preambles are in there too), but at 11 tools and 6,219
tokens
context-modeis a consistent, measurable share of it.For per-tool cost as a sanity check, two stdio servers I can measure precisely:
github/github-mcp-server(default toolset)context-modeThat is the part I'd flag: at 565 tokens/tool
context-modecosts ~2.5× per tool what GitHub'sserver does, and GitHub's covers a far larger API surface with four times the tools. The difference
is not capability, it is prose. When the baseline is already 25K+, a 6.2K component that is 80%
teaching material is a meaningful and easily reclaimable piece of it.
This is the same class of concern as #641 (routing-block cost, shipped as #680) and #964 (SessionStart injection knob), but for the static tool payload, which neither addresses.
build/server.jshas 11 unconditionalserver.registerTool("ctx_…")calls (lines 1455, 1810, 1988, 2279, 3100, 3331, 3559, 3772, 3901, 4068, 4394) with no gating; the only related env var,CONTEXT_MODE_EMBEDDED_PLUGIN_TOOLS, governs crash handlers. So today the only way to reduce this is to disable the extension entirely.Proposed solution
These compose; (3) is the largest single win and the one I'd argue for most.
CONTEXT_MODE_TOOLSallow-list. GitHub's own MCP server shipsGITHUB_TOOLSETS+GITHUB_TOOLSfor exactly this reason (measured on their Docker image:all= 85 tools vs a hand-picked 4 viaGITHUB_TOOLS— a ~15x difference in payload).CONTEXT_MODE_TOOLS=ctx_execute,ctx_search # register only theseSkipping registration — rather than filtering the response — is what actually removes the bytes.
Fold the 5 admin tools into one
ctx_admin({action}). Reclaims ~865 tokens with no capability loss;doctor/upgrade/purge/stats/insightare rare, deliberate operations.Move the steering prose out of
descriptionentirely. Not a toggle — a separation of concerns.descriptionstates what the tool does and what it returns; the WHEN / WHEN NOT / EXAMPLE material moves to thecontext-modeskill and the PreToolUse hook, which are already in place and are consulted when relevant rather than billed every call. On the measured split that is the ~80% of description tokens, i.e. ~3.0K tokens — the single largest reclaimable item, and it needs no new env var.If the concern is that routing quality depends on the prose being in-band, that is testable: ship terse descriptions behind
CONTEXT_MODE_TERSE_DESCRIPTIONS=1first and compare tool-choice behaviour. Shorten tool and skill descriptions for command palette readability #177 proposed one-line descriptions for palette readability and was closed unmerged; the tokens argument is a stronger case for the same change, and the descriptions have grown substantially since.Happy to send a PR for (1) or (2) if either is acceptable in principle.
Alternatives considered
The session-level figure above is reproducible on any Pi install:
build/server.jslocally — wiped on every upgrade (same durability argument made in proposal: MINIMAL_ROUTING env flag + per-agent tools_available filter to reduce SessionStart routing-block context cost #641).