<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andrea Troisi</title>
    <description>The latest articles on DEV Community by Andrea Troisi (@szabo_75).</description>
    <link>https://dev.to/szabo_75</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4080613%2F09361e73-cb02-4926-bc56-2d247c956f47.jpg</url>
      <title>DEV Community: Andrea Troisi</title>
      <link>https://dev.to/szabo_75</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/szabo_75"/>
    <language>en</language>
    <item>
      <title>Your MCP agent re-sends 7,000 tokens of tool schemas on every turn</title>
      <dc:creator>Andrea Troisi</dc:creator>
      <pubDate>Sun, 16 Aug 2026 21:58:01 +0000</pubDate>
      <link>https://dev.to/szabo_75/your-mcp-agent-re-sends-7000-tokens-of-tool-schemas-on-every-turn-2ep2</link>
      <guid>https://dev.to/szabo_75/your-mcp-agent-re-sends-7000-tokens-of-tool-schemas-on-every-turn-2ep2</guid>
      <description>&lt;p&gt;I wired three MCP servers into my agent, then did something I should have done first: I counted what they cost.&lt;/p&gt;

&lt;p&gt;One filesystem server, 28 tools: &lt;strong&gt;about 7,000 tokens&lt;/strong&gt;. Just the tool definitions. Before a system prompt, before any conversation, before the user has typed anything.&lt;/p&gt;

&lt;p&gt;Then I noticed the part that actually matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  It is not a setup cost
&lt;/h2&gt;

&lt;p&gt;An LLM API call is stateless. There is no server-side memory of a conversation — you re-send everything, every time. That includes the tool definitions.&lt;/p&gt;

&lt;p&gt;So the 7,000 tokens are not paid once when the agent connects. They are paid on turn one, and turn two, and turn twenty. A 40-turn session with three MCP servers connected pays for tool schemas &lt;em&gt;forty times&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Meanwhile, on any given turn, the agent calls one tool. Maybe two.&lt;/p&gt;

&lt;p&gt;You are paying full price for 26 tools that were never in play.&lt;/p&gt;

&lt;h2&gt;
  
  
  Count your own
&lt;/h2&gt;

&lt;p&gt;Do not take my number. Capture your own &lt;code&gt;tools/list&lt;/code&gt; response and measure it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;

&lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tiktoken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_encoding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cl100k_base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tokens per turn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most people I have shown this to guess low by a factor of three. The schemas are bigger than they feel, because nobody reads them — the client fetches them and hands them straight to the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is actually in there
&lt;/h2&gt;

&lt;p&gt;Here is one tool from my fixture, verbatim, as a real MCP server would send it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fs_write_file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Create a new file or completely overwrite an existing file with new content. Use with caution as it will overwrite existing files without warning. Handles text content with proper encoding."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Destination path of the file to write."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Full textual content to write into the file."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"encoding"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"utf-16"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"latin-1"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"createDirectories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"boolean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"POSIX file mode in octal."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"additionalProperties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;605 bytes. And that is a &lt;em&gt;small&lt;/em&gt; one — the GitHub and Slack tools in my set run past 900.&lt;/p&gt;

&lt;p&gt;Now split that into two piles.&lt;/p&gt;

&lt;p&gt;The model needs the &lt;strong&gt;name&lt;/strong&gt; and a rough sense of what the tool does in order to &lt;em&gt;decide&lt;/em&gt; to call it. It needs the &lt;strong&gt;full parameter schema&lt;/strong&gt; only once it has already decided — to actually format the call.&lt;/p&gt;

&lt;p&gt;Almost every tool in your list is in the first situation on any given turn. You are shipping pile two for all of them anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: only pay full price for what is about to be used
&lt;/h2&gt;

&lt;p&gt;That is the whole thing. Keep the tools the agent is plausibly about to call at full fidelity. Compress the rest down to what is needed to &lt;em&gt;name&lt;/em&gt; them.&lt;/p&gt;

&lt;p&gt;The hard part is the word "plausibly".&lt;/p&gt;

&lt;h2&gt;
  
  
  Predicting the next tool without a model
&lt;/h2&gt;

&lt;p&gt;The obvious reach is embeddings: index the tool descriptions, embed the conversation, retrieve top-K. I think that is the wrong instrument here, for two reasons.&lt;/p&gt;

&lt;p&gt;First, tool names are not prose. They are &lt;code&gt;fs_read_file&lt;/code&gt;, &lt;code&gt;git_commit&lt;/code&gt;, &lt;code&gt;github_create_pull_request&lt;/code&gt; — dense identifiers with maybe a sentence of docs. Embeddings smooth exactly the signal you want sharp.&lt;/p&gt;

&lt;p&gt;Second, you are on the hot path of &lt;code&gt;tools/list&lt;/code&gt;. Every millisecond you add is a millisecond in front of the user, on every turn.&lt;/p&gt;

&lt;p&gt;There is a much cheaper signal sitting in plain sight: &lt;strong&gt;agent workflows are extremely repetitive&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git_status&lt;/code&gt; → &lt;code&gt;git_diff&lt;/code&gt; → &lt;code&gt;git_commit&lt;/code&gt;. &lt;code&gt;fs_read_file&lt;/code&gt; → &lt;code&gt;fs_edit_file&lt;/code&gt; → &lt;code&gt;fs_read_file&lt;/code&gt;. Over and over.&lt;/p&gt;

&lt;p&gt;So build a graph. Every time a tool executes, draw an edge from the previously executed tool to this one and increment its weight. After a handful of turns you can read off a probability directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P(next = B | last = A) = weight(A → B) / total_out_weight(A)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a first-order Markov chain over tool calls, and it is embarrassingly cheap — a map lookup and a division. In my implementation it accounts for the largest single weight in the ranking.&lt;/p&gt;

&lt;p&gt;Notice the graph must &lt;strong&gt;not&lt;/strong&gt; be acyclic. Real workflows loop; &lt;code&gt;read → edit → read&lt;/code&gt; is signal, not noise. Every "DAG" framing of this problem I have seen throws away information.&lt;/p&gt;

&lt;p&gt;Four more signals fill the gaps, each normalised to &lt;code&gt;[0,1]&lt;/code&gt; and summed with a weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Second-order transition&lt;/strong&gt; — same probability conditioned on the tool &lt;em&gt;before&lt;/em&gt; last, which catches &lt;code&gt;A → B → A&lt;/code&gt; alternation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recency&lt;/strong&gt; — &lt;code&gt;2^(-steps_since_last_use / halflife)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frequency&lt;/strong&gt; — the tool's share of all executions this session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lexical overlap&lt;/strong&gt; — a decaying bag of terms harvested from live traffic (tool arguments, tool results, prompt requests) matched against each tool's name and description.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one needs one detail to work at all. Tool names are &lt;code&gt;snake_case&lt;/code&gt;, so you have to split on &lt;code&gt;_&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; on camelCase boundaries before matching. &lt;code&gt;fs_read_file&lt;/code&gt; becomes &lt;code&gt;read&lt;/code&gt; + &lt;code&gt;file&lt;/code&gt;, which then matches an agent that has been talking about reading a file. Skip that split and the lexical signal is dead on arrival — I know because mine was, for an afternoon.&lt;/p&gt;

&lt;p&gt;Total cost of ranking 28 tools: &lt;strong&gt;76 microseconds&lt;/strong&gt;. For 500 tools, 1.3 ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compression that stays valid
&lt;/h2&gt;

&lt;p&gt;For the tools that lose, the compressed form is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Create a new file or completely overwrite an existing file with new content. Use with…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fs_write_file"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;605 bytes → 152.&lt;/p&gt;

&lt;p&gt;The one decision worth explaining is &lt;code&gt;inputSchema&lt;/code&gt;. The tempting move is to delete it. Don't — an absent &lt;code&gt;inputSchema&lt;/code&gt; is invalid per the MCP schema, and strict clients will reject the tool outright. &lt;code&gt;{"type":"object"}&lt;/code&gt; is a legal, maximally permissive object schema that satisfies every validator for 19 bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when the prediction is wrong
&lt;/h2&gt;

&lt;p&gt;This is the part that decides whether the whole idea is usable or a footgun.&lt;/p&gt;

&lt;p&gt;A compressed tool &lt;strong&gt;keeps its name&lt;/strong&gt;. The model can still see that it exists and can still decide to call it — it just does not have the parameter schema in front of it. So when the prediction misses, the model reaches for the tool anyway.&lt;/p&gt;

&lt;p&gt;At that moment the proxy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;marks the tool as revealed and forwards the call untouched;&lt;/li&gt;
&lt;li&gt;emits &lt;code&gt;notifications/tools/list_changed&lt;/code&gt; to the client;&lt;/li&gt;
&lt;li&gt;the client re-requests &lt;code&gt;tools/list&lt;/code&gt;, and now that tool comes back at full fidelity.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Worst case is one extra round-trip. Nothing is ever unrecoverable.&lt;/p&gt;

&lt;p&gt;One protocol detail: that notification is only sent if the upstream server declared &lt;code&gt;capabilities.tools.listChanged&lt;/code&gt; during &lt;code&gt;initialize&lt;/code&gt;. Inventing a capability the server never advertised is how you get a client that ignores you, or worse, errors. If the capability is missing, compression still applies and the reveal lands on the client's next natural &lt;code&gt;tools/list&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Actually removing tools from the list is possible in my implementation, but it is &lt;strong&gt;off by default&lt;/strong&gt;. A compressed tool is recoverable; a deleted one is not. That asymmetry should decide the default, and in most tools of this kind it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap that eats your savings
&lt;/h2&gt;

&lt;p&gt;Here is the part I did not anticipate, and the reason I think most naive implementations of this idea &lt;em&gt;lose&lt;/em&gt; money.&lt;/p&gt;

&lt;p&gt;Both Anthropic and OpenAI cache prompt prefixes. The cache key is an &lt;strong&gt;exact byte prefix&lt;/strong&gt;. Tool definitions sit near the front of the prompt, which makes them prime cached content.&lt;/p&gt;

&lt;p&gt;Now imagine a pruner that re-ranks and re-orders the tool list on every turn, putting the most relevant tool first. It feels right. It is a disaster: every turn produces a different prefix, so every turn is a cache miss. You save 50% of the tool tokens and lose the discount on &lt;em&gt;everything&lt;/em&gt; that follows them.&lt;/p&gt;

&lt;p&gt;So the output has to be deterministic, and it has to be positionally stable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Preserve upstream ordering.&lt;/strong&gt; Only tool &lt;em&gt;contents&lt;/em&gt; change, never their positions. The prefix stays identical up to the first tool whose tier actually changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forward kept tools as the exact upstream bytes.&lt;/strong&gt; Never decode and re-encode a tool you are not modifying — you will normalise something and not notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build compressed objects with fixed key order.&lt;/strong&gt; Hash-map iteration order is a determinism bug waiting to happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Break ranking ties on original position&lt;/strong&gt;, not on whatever the sort happened to produce.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have a test that replays the same event sequence 50 times from a cold state and asserts byte equality of the output. It is the single most valuable test in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bug I did not see coming
&lt;/h2&gt;

&lt;p&gt;While verifying that pass-through was truly byte-identical, a test failed on a diff I could not explain at first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;got&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;email&amp;gt;.&lt;/span&gt;&lt;span class="s2"&gt;"}},"&lt;/span&gt;&lt;span class="err"&gt;required&lt;/span&gt;&lt;span class="s2"&gt;":["&lt;/span&gt;&lt;span class="err"&gt;repo&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="err"&gt;message&lt;/span&gt;&lt;span class="s2"&gt;"]
want &amp;lt;email&amp;gt;."&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="s2"&gt;"repo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go's &lt;code&gt;encoding/json&lt;/code&gt; &lt;strong&gt;HTML-escapes by default&lt;/strong&gt;. &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;amp;&lt;/code&gt; become six-byte &lt;code&gt;\uXXXX&lt;/code&gt; sequences. Every schema containing something like &lt;code&gt;Name &amp;lt;email&amp;gt;&lt;/code&gt; was being silently inflated — by the tool whose entire job is to shrink schemas.&lt;/p&gt;

&lt;p&gt;The fix is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetEscapeHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth checking in any Go proxy that re-encodes JSON it did not author. You are not just wasting bytes, you are modifying a payload you promised to forward transparently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;p&gt;Apple M5, Go 1.26, 28-tool fixture of realistic MCP schemas:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tokens per &lt;code&gt;tools/list&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;7,079 → 3,074 (&lt;strong&gt;-56.6%&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bytes per &lt;code&gt;tools/list&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;26,283 → 10,943 (&lt;strong&gt;-58.4%&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500-tool list&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-78%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ranking latency, 28 tools&lt;/td&gt;
&lt;td&gt;76 µs mean, 0.50 ms worst&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full frame rewrite (decode + prune + re-encode)&lt;/td&gt;
&lt;td&gt;0.44 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retained state after 20k tool calls&lt;/td&gt;
&lt;td&gt;2.1 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two honesty notes, because both invite a fair challenge:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token counts are estimates.&lt;/strong&gt; The token figures come from a built-in heuristic — word runs at roughly 4 chars per token, punctuation runs at roughly 2 — not from a real BPE tokenizer, because I did not want to ship a vocabulary file. It is checked against &lt;code&gt;cl100k_base&lt;/code&gt; reference counts and stays within a 0.6×–1.8× band. Byte counts are exact. Both sides of the before/after ratio carry the same bias, so the &lt;em&gt;reduction&lt;/em&gt; holds even where the absolute number drifts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold start is weaker.&lt;/strong&gt; The first &lt;code&gt;tools/list&lt;/code&gt; of a session has no execution history, so it ranks on lexical signal alone and falls back to upstream order. It still roughly halves the payload — the compression does that on its own — but the &lt;em&gt;selection&lt;/em&gt; gets meaningfully better after a few tool calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on your own tool list
&lt;/h2&gt;

&lt;p&gt;I packaged this as &lt;strong&gt;mcp-diet&lt;/strong&gt; — a transparent stdio proxy, Go, no dependencies outside the standard library, MIT.&lt;/p&gt;

&lt;p&gt;You can measure your own setup without installing anything into your agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/albererinofigo-droid/mcp-diet/cmd/mcp-diet@latest
mcp-diet analyze your-tools-list.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tools       28 (full 8, compressed 20, dropped 0)
bytes       26283 -&amp;gt; 10943  (-58.4%)
est tokens  7079 -&amp;gt; 3074  (-56.6%)
prune time  0.455 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To actually use it, wrap the server command you already run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"filesystem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-diet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"--server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx -y @modelcontextprotocol/server-filesystem /srv"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing else changes. Same protocol on both sides, and everything that is not a &lt;code&gt;tools/list&lt;/code&gt; response is forwarded byte-for-byte.&lt;/p&gt;

&lt;p&gt;Current limitation worth stating plainly: &lt;strong&gt;stdio only&lt;/strong&gt;. SSE and streamable-HTTP transports are not implemented yet, though the pruning core is transport-agnostic and usable as a library.&lt;/p&gt;

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/albererinofigo-droid/mcp-diet" rel="noopener noreferrer"&gt;https://github.com/albererinofigo-droid/mcp-diet&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you run the &lt;code&gt;analyze&lt;/code&gt; command on your own setup, I would genuinely like to see the number. My guess is that most people are paying more than they think, on every single turn.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>go</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
