← Back to Blog

Open WebUI MCP Token Bloat: Load Tool Schemas on Demand

Open WebUI MCP Token Bloat: Load Tool Schemas on Demand

Turn on three MCP servers in Open WebUI, ask the model what day it is, and you have spent tens of thousands of tokens before it reads the question. Turn on a fourth and it starts calling the wrong tool.

Those are the same bug. Open WebUI shows the model every tool it has, in full, on every call. The fix is to show it a list of names.

What Open WebUI Actually Sends on Every Call

MCP, the Model Context Protocol, is the open standard for wiring AI systems to external tools and data. We covered the servers worth running in Best MCPs for Finance. This is about what happens after you connect them.

In native function calling mode, where the model picks its own tools, Open WebUI builds the request's tool array by walking every resolved tool and attaching its full spec: name, description, and the complete JSON Schema for its parameters. The line lives in backend/open_webui/utils/middleware.py, mapping every entry in tools_dict to {"type": "function", "function": tool["spec"]}. Not the names. The whole specification, every time. The tool loop then reuses that same body on each hop, so a task that calls three tools pays for the full catalog three times.

The sizes are not small. Anthropic, publishing its own measurements when it shipped deferred tool loading in November 2025, put GitHub's MCP server at roughly 26k tokens for 35 tools and Slack's at roughly 21k for 11. A five-server setup runs about 55k before the model does any work.

Open WebUI adds to that, because its builtin tools ride in the same array. The author of the filter this post ends at measured 17 MCP tools plus 35 builtins at about 29k prompt tokens per call, falling to about 600 on the first call once schemas were deferred. That figure is the author's own, taken on their own stack, so verify it against yours rather than quote it. The shape of the problem survives the specific number: your bill scales with what you left switched on, not with what you asked for.

The Accuracy Problem Is the Expensive Half

Most teams read this as a billing problem and reach for a cheaper model. It is a retrieval problem, and a cheaper model makes it worse.

Anthropic's documentation for its tool search tool states the threshold plainly: a model's ability to pick the right tool degrades past 30 to 50 tools. On its internal MCP evaluations, deferred loading moved Opus 4 from 49% to 74% and Opus 4.5 from 79.5% to 88.1%. Those are Anthropic's own numbers for Anthropic's own feature, so take the direction as the finding and the magnitude as a claim.

The direction is easy to believe if you have read your own tool array. Somewhere in those eighty entries are files_ls, drive_list_files, and search_files, three near-identical descriptions of three different systems. A model choosing among three gets it right. A model choosing among eighty is doing fuzzy retrieval over a haystack you built for it, mid-generation, with no way to ask a clarifying question. You are paying, on every call, for the thing making the model worse.

Why Switching Servers Off Does Not Work

Two obvious fixes present themselves, and both fail.

Switching servers off means the capability is missing when you need it, and it pushes that decision onto whoever is chatting, at the moment they are trying to do something else. In practice everything stays on, which is how the array got to eighty entries.

Picking a tool set per turn fails for a subtler reason. Prompt caches are prefix caches, and Anthropic's caching documentation gives the order: tools, then system prompt, then messages. Modifying tool definitions invalidates the entire cache. A tool set recomputed each turn is a prefix rewritten each turn, so it can never be cached at all. That trades cheap cached tokens for expensive uncached ones and calls it an optimization.

So you want the array small, and you want it never to change. Those pull against each other only if the set has to shrink as well as grow. Let it only grow, and both hold.

Show the Names, Defer the Schemas

The model sees one tool, load_tools, plus the names of every hidden tool grouped by the server that owns it. It asks for files_ls and gmail_send_message by name, those schemas arrive in the next call, and it calls them directly. A chat carries schemas for the tools it used, not for every tool you own.

This is the oldest trick in systems engineering. Nobody statically links every library on the machine into every binary. The symbol table says what exists; the code arrives when something calls it. A tool name is the symbol table, and its JSON Schema is the code.

Growth-only is what keeps it cache-friendly. The paragraph listing hidden tools names all of them, loaded and unloaded alike, and is built from sorted input, so it stays byte-identical on every hop and every later turn. The array itself changes once per tool, at the moment that tool loads, then re-stabilises. A chat pays at most one cache miss per tool it genuinely needs, against a per-turn scheme that pays a full miss forever.

None of this is a workaround bolted onto the platform. It is the platform's own shape. Anthropic ships it at the API level as defer_loading, which keeps deferred tools out of the cached prefix and appends discovered ones inline, and documents a client-side path for teams that want their own search logic. Open WebUI needs that client-side path, because it talks to whatever model you point it at and cannot assume one vendor's server-side beta is present. If the deferral does not live in the client, for most deployments it does not live anywhere.

The Reference Implementation, and What It Does Not Do

openwebui-dynamic-mcp is one Apache 2.0 file that does this as an Open WebUI filter. Paste deferred_tools.py into Admin Panel, Functions, and enable it globally. It needs Open WebUI 0.11.2 or newer and models set to Native function calling, because Legacy mode never builds the array the filter rewrites.

Its most important property is one the README states about itself: it is not a permission boundary. Hiding a schema changes what the model is shown, not what the server will execute. Open WebUI resolves a tool call against its own catalog, so a hidden tool stays callable if the model names it, and a model that has read the list knows every name. Deferral is a context strategy. Access control is a separate system, and this does not build it for you. We laid out what it needs in Governing MCP Servers, Skills, and Plugins at Enterprise Scale.

The other liabilities are smaller and still real. Discovery runs on names alone, so a server with an opaque id or a tool called rungoes effectively invisible, where Anthropic's server-side search reads descriptions and argument names too. The first use of any tool costs an extra round trip, a latency tax paid for a token saving. The record of what a chat has loaded lives in the Open WebUI process, so a restart or a second worker costs that chat a reload. And the repository is four days old at this writing, version 0.1.0, with single-digit stars. It is one file. Read it before you enable it globally, which is advice worth taking about anything you enable globally.

What the Model Actually Needed

Three servers, one question. The gap between what that costs today and what it should cost is not a pricing problem, and no cheaper model closes it.

The model never needed to read eighty schemas to know what it could do. It needed eighty names, and one way to ask.