I spent a day getting a local model to keep a set of books through an MCP server, and it taught me more about designing the MCP than the months of building it had.
The setup was ordinary. A MacBook with 32 GB, LM Studio, and Gemma 4 26B, a mixture-of-experts model that fires about 4B parameters per token, so it runs at a usable speed on a laptop. On the other side, Saybooks, an open-source set of books with the business rules inside it: an issued invoice can't be edited, a payment can't be applied to a draft, a statement is accepted whole or not at all. Nothing left the machine.
Here is the trade-off as I now understand it. A frontier model forgives an average MCP. It reads a long doctrine once, remembers it, infers what you meant when a tool refuses, and recovers. A local model forgives nothing. So if you want a small model, the MCP has to carry the intelligence instead. The question I wanted answered was how far that can be pushed.
The invoicing side went first time, said the way a person says it. Set up the company, add a client, invoice them and issue it, they paid. Six steps, six right, no coaching. The bank statement took five attempts, and every failure was in my MCP, not in the model. That is the useful part, so here is what each one taught me.
Put the guidance where the model will see it. LM Studio never passes a server's connect-time instructions to the model. I checked the code: the client library stores them and nothing ever reads them. The model gets the tool descriptions and nothing else. So the workflow I'd written into the server's doctrine, read the vocabulary first, propose, show the person, then write, simply never arrived. It now lives in the result of the import, as the next step, and the model followed it on the first run after.
A refusal has to read as one. "Hash: at least 8 characters" reads like advice. The model reported the write as done, and the person would have believed it. Every refusal now begins "REFUSED, nothing was written."
A refusal has to name the fix, not the rule. "A recurring charge needs its vendor named" was true, and the model sent the identical batch three times. "T-0003 (Amazon Web Services): add vendor to that row and send the batch again" ended it.
Don't make it ask for what it was told. My setup doctrine said "ask one question at a time." The model obeyed it and asked for the company name it had been given in the same sentence.
Speak one language across tools. The import result listed the rows under id; the review wanted transaction_id. The model copied the shape it had just been handed, got "transaction undefined does not exist" eight times, and then told the person the review had succeeded. The review now accepts id, because a model that copies your own output back to you is not wrong about which row it means.
Two things stayed the model's, and they are the two the design exists for. It reported success after a refusal three times in one day. And once, a different model invented a purchase order number nobody had given it. The audit trail caught the first, because every call is on it, refusals included. The preview before issue caught the second. Rules in the system, not in the prompt, is the whole point when the model is small.
The last thing I learned was about the laptop, not the model. LM Studio loads a model with four concurrent slots by default and reserves a full context cache for each. Four times 32k on a 17 GB model pushed the machine into 28 GB of swap, and I spent an hour blaming the model for being slow. One slot, and it was quick.
So how far can you push the MCP? Far enough that a person speaking plainly and a 26B model on a laptop got an invoice cycle right on the first try, and a full statement cycle, import, categorise, correct, write, report, on the fifth, with every fix in between made to the MCP and none to the model. I built a small harness that runs those plain scripts against a local model and checks the database afterwards, so the next change to the tools is measured the same way. I'd rather spend the effort there than on a bigger model.