I keep replaying that demo. One agent wired to everything: it read the ledger, queried reconciliation, pulled invoices from the document store, and posted adjusting entries, all from a single conversation while the room watched it work. A demo that works is the most dangerous sentence in this line of work.
On a worse day, the same agent could post a wrong adjusting entry to a real ledger because a retrieval came back stale and the model reasoned confidently over it. One process held every credential and every tool, so the blast radius of a single bad turn was the whole finance stack. That is not a model you can put in front of a controller and an auditor and keep your job.
I tore it apart and rebuilt it as a fabric instead of a better agent.
The fabric, not the brain
The shape that worked is small. Each financial system gets its own MCP server: the ledger server speaks ledger and nothing else, the reconciliation server speaks reconciliation, the document store sits behind a server that knows only documents. Each one exposes a handful of tools scoped to exactly what that system does, holding only the credentials that system needs. Above them sits an orchestrator that decides what to call and holds no credentials of its own.
The orchestrator reasons while the servers do the work, and every arrow that mutates state passes through a gate first.
Writes are physically routed differently from reads, and the diagram is built to make that hard to miss. A read tool returns data and the orchestrator moves on. A write tool never executes on call. Instead it stages a proposed change, emits an approval request, and parks until a human, or a policy engine standing in for one on the small stuff, releases it or kills it. The model never touches the ledger directly, only a queue of intentions, and a separate gate decides which of those become facts.
The payoff has almost nothing to do with the agent being smarter, because the agent is exactly the same agent. What changed is that the failure modes shrank to something I can reason about. When the ledger server is the only thing holding ledger credentials and the only thing that can post an entry, a compromised or confused orchestrator cannot drain anything, it can only ask, and every ask is logged with the inputs that produced it.
Why one big agent was the wrong shape
Three reasons, and I had to live each of them.
Blast radius. One agent with every tool is one credential blast radius. A prompt injection in a fetched invoice, a hallucinated account number, a tool call that fires twice because of a retry, and the damage is unbounded across systems. Split the credentials across servers and the worst a single bad turn can do is bounded by one system’s scope. You stop designing for “what if the model is wrong” as a catastrophe and start treating it as a normal, contained event.
Auditability. An auditor is not going to read a chat transcript. What they want to know, for this posted entry, is what tool ran, with what arguments, who approved it, and at what time. When every write goes through a named tool on a named server behind a gate, that record falls out for free. The trace is the audit trail. With one agent improvising over a pile of functions, you are reconstructing intent from logs after the fact, which is exactly the position you do not want to be in when the regulator calls.
Testing. This is the unglamorous one and it mattered most. I can test the ledger server in isolation. Feed it a posting request, assert it validates the account, assert it refuses an unbalanced entry, assert the gate fires. No model in the loop, no orchestrator, no flakiness from a language model’s mood that morning. Each tool is a small unit with a contract. The monolith was untestable in any honest sense. You could only test the whole thing end to end, watch it pass, and pray the next prompt didn’t find a new path through it.
A tool definition with the guardrail built in
The guardrail lives inside the tool itself, never bolted on afterward as a separate wrapper. A write tool that cannot enforce its own invariants is not a tool I will ship. The posting tool on the ledger server looks roughly like this.
@server.tool()
async def propose_ledger_entry(
debit_account: str,
credit_account: str,
amount_minor: int, # always integer minor units; floats post wrong entries
currency: str,
memo: str,
idempotency_key: str, # the orchestrator MUST pass this; retries are not new entries
) -> ToolResult:
# Guardrails run before anything is staged. A bad call fails loud, here,
# not three steps later when it's a mystery in the reconciliation report.
if amount_minor <= 0:
return reject("amount must be positive; reversals use the reversal tool")
if not ledger.account_exists(debit_account) or not ledger.account_exists(credit_account):
return reject("unknown account; the model does not get to invent GL codes")
if amount_minor > POLICY.auto_threshold_minor:
require_human = True # over the line, a person signs. no exceptions, no override flag.
else:
require_human = POLICY.always_require_human # which, in finance, is true
# We never post here. We stage and ask. The gate decides if this becomes real.
proposal = ledger.stage(
debit_account, credit_account, amount_minor, currency, memo,
idempotency_key=idempotency_key,
)
return await approval.request(
proposal,
require_human=require_human,
# the approver sees the exact arguments, not a model's summary of them
evidence=proposal.as_diff(),
)
Two things in there earn their keep. The amount is integer minor units, never a float, because a float is how you post a cent wrong and spend a Tuesday finding it. The idempotency key is mandatory for a subtler reason: the single most common production failure of an agent is the right decision executed twice on a retry, not a wrong one. The gate de-dupes on that key, and without it, “post this entry” plus a network blip becomes two entries and a reconciliation break you will chase for an afternoon.
The approver sees the exact debit, credit, amount, and the source document the proposal was built from, not “the agent wants to record a vendor payment.” Judgment needs the real arguments, not the model’s prose about them.
What it cost
This shape is more work up front than one clever agent. You are running and versioning several servers instead of one, the orchestrator has to know which server owns which capability, and you have to keep those contracts honest as the underlying systems change. The glue between the servers and the orchestrator is real, ongoing work.
The gates also add latency and friction. A controller who has to approve every machine proposal is not a controller but a bottleneck wearing a fancier title, so the threshold is what makes or breaks this. Small, routine, fully-reconciled entries auto-release under policy, and the human’s attention is spent only where the amount or the ambiguity earns it. Get that line wrong toward caution and people route around your system. Get it wrong toward speed and you have rebuilt the monolith with extra steps.
I will take the friction, because in finance the question is never only “can the machine do this,” but “can you prove, afterward, exactly what it did and that someone with authority let it.” A fabric of small servers with gated writes answers that by construction. One big agent answers it with a transcript and a shrug.
The single brain still demos beautifully, and it always will, which is exactly what makes it dangerous. What the fabric buys me is the bad day, the one that always comes, when someone asks what the machine did and who let it. The blast radius fits inside one server, the answer fits inside one trace, and I can hand that trace to the controller and the auditor without leaving the room.