Hand a small local model over 450 tool definitions and it stops working. Not slowly. Not approximately. It breaks. The catalog overflows the context window before you've typed a question, and what was supposed to be a useful AI assistant turns into a context error.

That's the constraint we ran into the day we connected ToolPiper's full MCP catalog to a tight-context local model. Cloud frontier models swallowed it without complaint. Llama 3.2 3B did not. The catalog alone was bigger than half its working memory.

The MCP wire is small. The inference payload isn't.

ToolPiper exposes over 450 MCP tools, and the MCP wire payload, what an external AI client like Claude Code or Cursor receives on tools/list, is about 121 KB total. Names plus stripped schemas, no descriptions. A long-context client holds that, and a filtered connection lists a fraction of it.

The hard problem is one layer deeper. When ToolPiper's own chat layer makes a function-calling call to a language model, local or cloud, it has to inject full tool definitions into the prompt: name, full description, parameters with per-property docs and enum values. At that fidelity the over 450 tool catalog runs to over 100,000 tokens. A frontier model with a 200K context can hold that, at about two thirds of its window. A local model with an 8K context cannot, and won't, ever. The catalog by itself is more than twelve times the model's entire working memory.

So the only models that can be handed every tool every turn are frontier-class ones with a big context window to spare. Everything smaller, which is most of what people actually run on their own hardware, needs the catalog filtered before it reaches the model. There is no clever prompt engineering that fixes this. The arithmetic is what it is.

Local AI on a laptop has no layer between the client and the model. Whatever the client sends is what the model sees, pays for, and tries to reason about. So we built one.

Meet PiperMatch

The piece that does this in ToolPiper is called PiperMatch. It's an on-device tool matcher, and ToolPiper installs it on your Mac on its first launch. When a query arrives, PiperMatch ranks the catalog against what you actually asked for and returns the handful of tools likely to matter for that turn.

It runs on the Apple Neural Engine. PiperMatch never sends the query off your Mac. Selection takes a few hundred milliseconds at most, and the chosen tools are what gets handed to the language model.

We got it small enough to run privately on your hardware.

Why small models need a small, well-picked set

The general advice you'll see online says small local models "struggle" with many tools. That undersells it. With a tight context window and a long catalog, every extra definition crowds out the user's actual message and the model's working memory. So ToolPiper sends only the tools that apply, within one eighth of the model's window, which is 4,096 tokens at the 32,768-token window it gives a local model.

So the goal of routing for small models is to get the right tool into that set, fast. If you nail that, the model is sharp and decisive. If you miss, the model can still ask for more through request_tools, but it shouldn't have to.

The pick has to be right the first time. ToolPiper makes that call before the model is involved and sends the right tools directly. The model never has to know there were over 450 options. It sees a small focused set and gets to work.

One catalog, sized to each model

Picking the right tools is half the work. Fitting them into the turn is the other half.

Every function-calling tool definition has a name, a description, and a JSON schema for its parameters. The per-parameter docs and the enum-value commentary are the heaviest pieces. On a verbose tool that's hundreds of tokens. On a verbose catalog of over 450, it's tens of thousands.

ToolPiper sends every picked tool with its full definition: name, description and input schema.

How many go out is decided per turn, based on how much room the loaded model has. A small local model gets a small set of tools. A long-context cloud model gets a wider set. You don't have to configure any of this: the selection logic reads the model's actual capacity and sizes the set to fit, and the MCP Schema Size control in Tool Permissions is there if you want a leaner or heavier set.

The MCP wire itself is separate from all of this. It's already stripped to the floor. External clients reading tools/list see names and just enough schema for argument inference, no descriptions on the wire. The picking and sizing happen later, when ToolPiper actually invokes the model.

The combined effect is real money and real responsiveness. The same catalog that breaks a small model under naive shipping fits comfortably under PiperMatch plus schema sizing.

You decide what's in the schema

PiperMatch handles the per-turn decision. The other half of catalog management is per-tool and per-category control, owned by the user, applied once and respected everywhere.

ToolPiper's Tool Permissions panel lets you mark any individual tool, or an entire category, as Allow, Ask, or Deny. Denied tools never reach PiperMatch and never enter the inference schema. The model literally cannot see them, so it cannot try them. This is not a runtime rejection; it is upstream of the catalog. A denied tool costs zero tokens.

The same is true at the category level. Don't want the model touching the macOS system actions? Deny system and the 204 tools in that category drop out of every payload. Don't want it touching Git? Deny the filesystem subset you care about, or the whole category. The Schema Budget card on the same panel shows your current allow/ask/deny token totals as you toggle, so you can see the catalog shrink in real time.

For external clients, each connection can list its own slice of the catalog: Claude Code can connect to /mcp?profile=automation while Cursor connects to /mcp?category=core, with both routed through the same running ToolPiper. Connected Apps, one panel over in Security, shows which apps connected with a token and what each one holds.

How the connection is secured

On the same machine, you reach the whole catalog with no bearer to handle. ToolPiper mints a per-launch ambient token at ~/Library/Application Support/ToolPiper/.toolpiper-token (mode 0600, rotated on every launch) and trusts loopback at the socket level, so Claude Code, Cursor, and anything else on your Mac connect with zero config. There's no key to paste into a client file. Off-machine callers, a LAN device today and a PiperMesh peer when it launches, are the ones that must present a scoped Bearer tp_<64 hex> token with the right audience, 64 hex characters of CSPRNG entropy that ToolPiper never writes into ~/.claude/ or any config you don't control.

Connected Apps is also where every authenticated client shows up as a labelable row: you can see its current MCP-tools access state and revoke it in one click. Revocation kills the bearer immediately and the row leaves the list; reconnecting means the app authorizes again. Below the tool list in MCP Tools, the Tool Governance section lets you deny a specific tool (say system_run_command) for every client at once, or flip the built-in tool marketplace from open browse to an explicit allow-list. Governance can only narrow what a tier already permits, never widen it. On your own Mac it's a per-device overlay you edit locally. On the Team plan, an admin sets the same policy from modelpiper.com, each deployment pulls it, and the controls lock behind a "Managed by your organization" badge. The per-device controls are live on every tier.

How often it picks the right tool

Across 451 requests in our test set (2026-10-07), 435 had the right tool inside the 25 picked and 360 ranked it first.

What you experience as a user

Most of this is invisible by design. You install ToolPiper, you pick a model for its chat, voice or automations, and it works. The catalog is there. The right tools show up when you need them. Tokens don't blow up. Small models don't choke. A client like Claude Code or Cursor builds its own requests, so it gets the stripped list, filtering and request_tools.

If you want to see it run, ToolPiper's logs viewer surfaces a recommendation event on every turn. You'll see how large the catalog was, how long the pick took, and the name it ranked first. It's a useful sanity check the first few times you ask the system to do something. After that you stop looking, because the answer is always "a small relevant set, cheaply shipped."

Where this approach holds back

Two cases worth being honest about.

The first is when the model's context is big enough that the whole catalog fits without crowding anything. ToolPiper's own chat still ranks at that scale, because a picked set costs less. If you want every tool on every turn, choose Custom under MCP Schema Size and ToolPiper sends your whole selection unranked.

The second is genuinely novel phrasing. A query expressed in unfamiliar vocabulary can rank a less-relevant tool higher than the right one. When that happens the workaround is usually a small rephrase.

Why this matters beyond ToolPiper

Local AI is going to spend the next few years catching up to cloud AI on the experiences that depend on tools rather than raw model intelligence. Browser automation, system actions, file operations, search, scraping, all of it routes through tool calls now. A model that can't use tools well is a model with one hand tied.

The way you make small models good at tools is to do the routing on-device. That work is picking the right tools and sizing what gets sent. PiperMatch does the picking and schema sizing does the rest. Together they're the reason a small local model on a MacBook can reach the same catalog a frontier model does.

It's also the part of ModelPiper we're most proud of. The user-facing surface is small. The work behind it is not.

Try it

ToolPiper is a free download from modelpiper.com/download. PiperMatch is on by default. Nothing to configure, nothing to enable. Connect a model with a tight context, connect a model with a generous one, and watch the same catalog adapt to both.