The first time I watched the assistant in the ERP I’m building create an invoice, record a partial payment against it, recompute the balance, and then flag the client for a follow-up — all without me touching a screen — I did not feel excitement. I felt the specific cold awareness you get when you realize a thing can now do something irreversible. That feeling is the whole design problem, and MCP is how I make it survivable.
Let me be precise about what MCP is and is not, because the acronym gets thrown around as a buzzword. The Model Context Protocol is a standard way to expose tools, resources, and prompts to a model. That is it. It is plumbing. What makes it the right plumbing for an ERP is not the protocol itself but what the protocol forces you to do: it forces you to define every action as a typed, named, schema-bound, permissioned tool. And once you have done that, you have accidentally built the single best control surface a business can have.
The button is the unit of operations
Think about how a person runs a business inside software. They do not write SQL. They click buttons. Each button is a small, named, intentional action: record payment, approve excuse, assign service, suspend employee, convert lead. The button has preconditions (you must be allowed), an effect (it changes state), and a result (it tells you what happened). A business is, operationally, the sum of its buttons and who is allowed to press which.
MCP tools are buttons for machines. When I expose invoices_record_payment as a tool, I am not building an AI feature. I am taking a button that already exists and making it pressable by a non-human actor under the exact same rules. The MCP server fronting my ERP exposes several hundred of these — attendance, payroll, invoices, leads, tasks, schedules, expenses — and each one is the same operation the UI invokes.
An operating business is a set of permissioned buttons. If your AI can press the same buttons a person can, under the same permissions, it can run operations. If it can’t, it can only chat about them.
This is the inversion that matters. Most teams think about “adding AI” by asking what new thing the AI should do. I think about it by asking which existing buttons are safe to expose and to whom. The intelligence is in the model. The power is in the tool surface. Keeping those separate is the entire game.
Three properties every tool must have
A tool is not just a function with a JSON schema. To be safe as a control surface for a real business, every tool I ship carries three properties without exception.
Typed. The input and output are schema-bound. record_payment takes an invoice id, an amount in a known unit, and a method. It returns a typed result, not prose. This is not pedantry. The cents-as-dollars disaster — a model reading $34,000 where the truth is $340 — happens precisely when units are ambiguous at the boundary. Typed tools with explicit money handling are the fix. The schema is the contract, and the contract is enforced before any state changes.
Permissioned. Every tool call carries the caller’s identity and is checked against that caller’s envelope. The AI does not get root. It gets a grant — a specific, scoped set of permissions, often narrower than a human admin’s. An agent acting on behalf of a manager can do what that manager can do, and nothing more. In a multi-tenant platform this is doubled: the tool also enforces tenant isolation, so a call can never reach across the boundary into another business’s data. There is no acceptable failure rate on that check.
Audited. Every invocation leaves a record identical in kind to a human action. Who called it, with what arguments, what changed, what came back. When an agent does something wrong — and over enough volume it will — I need to reconstruct the trajectory exactly. You cannot operate a business on actions you cannot replay.
tool invoices_record_payment {
input: { invoiceId: Id, amount: Money, method: Enum } # typed
guard: caller.can('invoice.pay') && caller.tenant == invoice.tenant # permissioned
effect: append payment; recompute amountPaid; maybe set PAID
audit: log(caller, args, diff, result) # audited
result: { status, amountPaid, balance } # typed back
}
That shape repeats across every operation. The uniformity is the point. When all several hundred tools obey the same three properties, the safety reasoning becomes tractable. I am not auditing the AI. I am auditing the tool surface, and the AI inherits whatever guarantees that surface provides.
Gate the irreversible
Not all buttons are equal. Reassigning a task is cheap to undo. Hard-deleting an employee record, paying an invoice, or sending an outreach blast is not. My rule, which I hold across every system I build, is that autonomy is a cost and irreversible actions get gated.
In practice that means tiering the tool surface:
| Tier | Examples | Who can autonomously invoke |
|---|---|---|
| Read | list invoices, get KPIs, search people | AI freely |
| Reversible write | create draft, reassign task, add comment | AI within grant |
| Consequential write | record payment, convert lead, suspend user | AI with confirmation step |
| Irreversible | hard-delete, bulk-remove, send money | Human-in-the-loop, always |
The model can fly the reversible tiers on its own. The bottom tier requires a person to approve the specific action, with the specific arguments, in context. This is not a lack of confidence in the model. It is recognition that the blast radius of a bad call at the bottom tier is unbounded, and unbounded blast radius is never worth the convenience of removing one click. I would rather the system be slightly less autonomous and never wire money to the wrong account.
What this unlocks
When the control surface is built this way, the same tools serve callers I never specifically designed for, and that is where the leverage compounds.
The in-app assistant calls them, so a manager can type “check who’s late today and file excuses for the two people who messaged me” and watch it happen as a sequence of audited tool calls. An external agent can call them over MCP, so the business becomes drivable by whatever orchestration I point at it. A scheduled job calls them, so recurring invoices and due reminders run themselves through the same path a human would have used, with the same guardrails. A voice agent — the same lineage as HANNA, my cold-call system — could place a call, take an order, and push it straight into the ledger through the identical tool surface.
None of those required new business logic. They required new callers of an existing, disciplined surface. That is the dividend of doing the boring work once. The marginal cost of a new automation drops to near zero, because the dangerous part — defining what can safely happen and under whose authority — was paid up front.
The failure modes I actually watch
I am skeptical of demos that can’t survive production, so let me name what goes wrong, because it does.
The model chains tools in a plausible-but-wrong order. It records a payment before the invoice exists, or it converts a lead and then tries to act on the stale lead record. The fix is not a smarter prompt. The fix is that each tool validates its own preconditions and returns a clean typed error the model can recover from, so a wrong order fails loudly instead of corrupting state.
The model hallucinates an argument — an invoice id that looks right but isn’t. Typed lookups and existence checks at the tool boundary turn that into a rejected call rather than a misapplied payment.
The model is confidently wrong about money. This is why I refuse to let financial figures flow through convenient caches. Numbers the AI reports or acts on must trace to reconciled sources, because an ERP that shows confident numbers it cannot defend is worse than no ERP at all.
Latency and cost creep. A naive agent makes forty tool calls where four would do, and each round trip adds a few hundred milliseconds and a slug of tokens. I instrument trajectories and prune. The cheapest tool call is the one the model didn’t need to make because the first result already told it what it needed.
Why I built it this way
I did not adopt MCP because it was fashionable. I adopted it because I was already going to have to define every action as a typed, permissioned, audited tool to make this ERP trustworthy with an AI in the loop — and once I had done that, exposing it over a standard protocol was nearly free. The protocol followed the architecture, not the other way around. That same control surface is the spine of Vontra, the AI-native business operating system I’m building.
The deeper claim is this: the right way to make a business AI-operable is not to teach the AI about your business. It is to render your business as a clean set of permissioned buttons and let the AI press them under the same rules everyone else follows. Do that, and the AI is not a special integration. It is just another authenticated user with a particular set of grants and a very fast clicking finger. Built for missions means built so that the fast clicking finger can never press the one button you can’t take back. That constraint, not the chatbot, is the product.