To everyone building agents on frappe: where's the floor?

hey all, john here again.

you’ll have seen pieces of this from me scattered across a few threads this week. i’m pulling it into one place, because the ask under all of them deserves its own room.
I’ve spent the last while reading nearly everything this forum has posted about agents and mcp, and i want to start by saying it plainly: this community is building the real thing, fast, and in the open. so before i make my case, i want to credit the people already doing the work, because the case only stands on top of what they’ve built.

rushabh framed it best in the agentic-world thread. agentic coding feels magical, he said, but it’s “a trick under the hood,” the model is just “a clever binding to something underneath.” that’s exactly right. and on an erp, the something underneath is your books. so the question i want to put on the table for all of us is simple: what sits between the agent and the ledger.

look at what’s already shipped.

casys.ai put out an mcp server with over a hundred tools across the whole erpnext surface. it’s the breadth reference everyone points at, and it’s active and open. rakeshgangwar’s erpnext-mcp-server is the standalone people actually clone and run. frappe assistant core (buildswithpaul) went furthest on the plumbing: oauth2, permission scoping, an audit log, real infrastructure, and it’s the most-starred of the lot for a reason. applied relevance built the python path with deep filtering and broad api coverage. mascor’s frappe-mcp-server reached for the hard part on purpose, granular permissions and audit logging, and it points the right way.

agentready took the gateway road: discover the real installation, wrap it in a small governed action set, put approvals and audit around execution. and they said the honest thing themselves, that a gateway can’t shrink the erp credential sitting underneath it. mith tech wrote the security playbook: don’t connect as administrator just to test, start read-only, build a purpose-built user with a tight role. erpgulf shipped changai, an open agent with natural language and voice. damcio20 is building a kiosk on erpnext with a local llm for the shop floor. sambhaji and varun turned a skills race into a merge, co-operation over competition, which is the exact spirit i’m writing in.

and the official frappe mcp library and skills are what they should be: a good door and a good toolbelt. the library runs the tool it is handed and leaves the guarding to the app and the framework around it. that is the right design for a library. the door is not the floor. the floor has to live somewhere else.

so here’s the one thing i keep coming back to, and it is not a knock on anyone above.

on frappe, an api credential carries no scope. when a key authenticates, the code runs frappe.set_user(user), and from that line on the request is that user, with every permission the user has. the key is a field on the user, not its own object. roles are real and they work, but they attach to the person, not to the credential. so a token you minted for one small job can call anything that user can call. and narrowing the user doesn’t close it. a scoped-down user’s key still carries that whole user’s reach, with no consent before a write and no receipt after. it only lowers the ceiling.

that is fine, most of the time. on a dev site, on read-heavy work, with one person watching the agent in a chat window, you do not need a floor. it would only be in the way. use these tools. they work.

a floor starts to matter when three things change. the human leaves the loop, which is the whole point of an agent. the books are real, so a wrong submit is a filed document and a wrong delete is gone. and someone asks later what happened, which nobody needs until they need it badly.

and here is the tell that this isn’t my hobby horse. look at what microsoft just shipped in business central, their own erp. they made agents first-class and generally available, a sales order agent and a payables agent. and look at how they governed them, because it is this exact floor. each agent runs under its own business central user account, not a person’s, scoped by a permission set built to touch only what that one job needs. every consequential step, sending a quote, posting an invoice, stops for a human to review and approve before it happens, and business central records who reviewed it and when. that is a dedicated agent identity, scoped below any human, a human yes before the write, and a receipt of who said it. plan, consent, prove, shipped in the biggest proprietary erp there is.

and microsoft is blunt about where that floor has to sit. their own guidance for agent identity says that the moment an agent authenticates with a plain api key instead of a minted identity, conditional access “won’t apply.” same truth we are living on frappe: a floor has to bind the credential itself, it cannot just sit in a tool above it.

the only thing left to argue is ownership. microsoft’s version is proprietary and lives in their cloud. erpnext’s can be open, and live in the platform. that is the whole point of writing this.

so my ask, and it’s two honest roads to the same place.

one, fix it in core. the extension point already exists. auth_hooks runs right after the key authenticates and before the request dispatches, which is exactly where a per-credential allowlist belongs. this is platform work, and rushabh already said the agentic refactor lives in develop and v17. a credential floor belongs in that future.

two, until it does, use ours. i built pacioli-guard for exactly this: a bench app that binds any credential to an allowlist of methods and doctypes, deny by default, no core fork, given freely. run it as your floor, or read it as the reference for what core should do. and a governed broker on top that makes every write go plan, consent, prove, so an agent can’t forge a yes and can’t quietly erase what it did.

either way, same ask. fix the floor. i’d rather say it now, with the code open, than after the first agent does something a scoped credential would have stopped.

i’m not selling anything. it’s open, it’s apache, check every word: GitHub - john-broadway/pacioli: Least-privilege governance for ERPNext — the credential floor + a governed agent broker, MCP · A2A · API, one spine. No debit without a credit: PLAN · CONSENT · PROVE · UNDO. · GitHub . and if you want the full read of where each of these servers actually stops, i wrote it up line by line and i’ll link it in a reply.

we’re early. let’s build the floor together. adopt it into frappe, or run pacioli, or both :wink:

the floor under an erpnext agent

what i found when i read the code.

i sat down and read the actual source. frappe’s own auth path, and the erpnext mcp servers people are shipping right now. this is what i found, with file and line, so you can check every word yourself. it is not a pitch. it is a set of facts about where an agent’s power actually comes from on an erpnext site, and where it is stopped. or not stopped.

versions read: frappe 16.27.1, erpnext 16.28.0 (current v16, mid-july 2026).
every competitor claim below comes from a fresh clone of that project’s main branch. i read the write handlers on the real bytes and grepped the whole tree, not one file.

when a floor matters, and when it doesn’t

first, honestly: the servers below are good tools, and for a lot of what people use them for, a floor would just be in the way. a developer wiring an agent into a test site, a read-heavy workflow, one operator watching the agent work in a chat window: that setup does not need plan, consent, and proof. it needs the tool to work, and these tools work. if that is you, use them, and skip the rest of this.

a floor stops being optional when three things change.

one, the human leaves the loop. the safety of a raw tool server is the operator’s attention. you see the delete because you are watching. but the whole point of an agent is to act while you are not watching. the moment it runs unattended, there is nothing left between the agent and the ledger.

two, the books are real. on a test site a wrong action costs nothing. on a real company’s ledger a wrong submit is a filed document, a wrong cancel breaks a reconciliation, a wrong delete is gone.

three, someone asks later what happened. when an agent posted a wrong invoice three weeks ago, can you prove which agent, on whose say-so, under what plan? nobody needs that receipt until they need it badly.

none of the three is exotic. it is just the difference between a demo and running it for real.

the floor frappe gives you

start with the thing under everything: how an api credential is checked.

frappe/auth.py, function validate_api_key_secret (lines 713-739). an api key is looked up as a field on the user doctype. on a secret match the code runs one line that matters:

frappe.set_user(user)

that is the whole floor. the request becomes that user. from there every permission decision is a role check against frappe.session.user, identical to a password login. there is no scope on the key, no method allowlist, no doctype restriction. the api key is not even its own object. it is a column on user.

so a token you minted so one integration could call one method can equally POST /api/resource/ directly. the credential carries nothing that says “only this.”

people ask about oauth. oauth does not save it either. frappe.whitelist() (frappe/init.py:439-476) takes allow_guest, xss_safe, methods. there is no scope parameter. no endpoint can declare a required scope, so a token’s scopes are never checked against what an endpoint needs. the oauth scope check at auth.py:674 reads the scopes off the token itself and validates the token against its own scopes. it is circular. the default oauth client scope is literally “all openid”.

this is not a bug and it is not a criticism of frappe. it is the platform. roles are real and they work, but they live on the user, not on the credential. that distinction is the whole story of what happens next.

three ways people are wiring agents in, and where each one stops

A. raw tool servers

wrap erpnext’s rest/rpc as mcp tools and hand them to the agent. real, useful, popular projects. here is what their own code exposes.

Casys-AI/mcp-erpnext (the “120 tools” project from this forum, MIT, shipping actively). from src/tools/operations.ts:

  • erpnext_doc_create: “Create any ERPNext document. Works on any DocType including master data…”
  • erpnext_doc_update: “Update any ERPNext document… Works on any DocType.”
  • erpnext_doc_delete: “Delete any ERPNext document.”
  • erpnext_doc_submit, erpnext_doc_cancel: submit/cancel any document.

the delete/submit/cancel tools carry destructiveHint: true. that hint is metadata for the client to read. it gates nothing. i read the handlers: the delete tool validates the name and calls the client’s delete. submit reads the current document then calls frappe.client.submit. nothing in between plans the change, asks for consent, or writes a receipt. the credential is ERPNEXT_API_KEY:ERPNEXT_API_SECRET from the environment.

rakeshgangwar/erpnext-mcp-server (MIT, the highest-starred of these). from src/index.ts:

  • delete_document: “Permanently delete a document from ERPNext. This action cannot be undone.”
  • call_method: “Call an ERPNext/Frappe whitelisted server-side API method. Can invoke any whitelisted method.” a generic rpc tool.
  • plus create_document, update_document, submit_document, cancel_document.

i read the handlers the same way. delete_document issues a direct delete. call_method posts to any /api/method endpoint, a generic passthrough. submit and cancel call frappe.client.submit and cancel directly. no plan, no consent, no receipt anywhere in the write path.

appliedrelevance/frappe-mcp-server (ISC, python) follows the same pattern in src/tools/documents.py: create/update/delete/submit/cancel/amend plus a generic call_method. i grepped its whole source, and no consent, approval, or audit term appears anywhere in it.

where A stops. the agent holds an api key that equals a full user. the tool layer adds no consent, no plan, no receipt. so the agent can create, update, submit, cancel, or permanently delete any document that user’s roles allow, in one call, with nothing recorded but erpnext’s own mutable logs. watched, on a test site, that is fine. unwatched, on real books, that one call is the whole risk, and nothing in the code catches it.

the ones that reached furthest, credited honestly. two projects in this family did real governance work, and both deserve the credit.

buildswithpaul/Frappe_Assistant_Core (AGPL-3.0, and the most-starred erpnext mcp project by a wide margin) is the most complete of the tool servers. i cloned it and read the write path. every document tool routes through validate_document_access(user=frappe.session.user, …) and frappe.has_permission(doctype, perm_type, user=user) (its own code calls that check “the primary security control”), with ignore_permissions=False on the actual writes and its test suite asserting so. it adds a role model (“Assistant User”), strips admin-only and sensitive fields by role on create, refuses writes and deletes against already-submitted documents, gates tool access by role through an FAC Tool Role Access doctype, ships oauth2, and logs every call to an Assistant Audit Log. that is real work and it is the careful end of this whole category.

where it still stops is the same seam, and it is structural, not a knock on the effort. the security control is the connecting user’s roles, checked through the tool. that is roles-on-the-user (case C below), not a scope on the credential. the credential underneath is still a set_user key: anything holding it that calls erpnext’s rest api directly, skipping these tools, has that user’s full reach, and there is no app-side floor to refuse it. consent before a write is the user’s role holding the permission, not a per-action human yes the agent cannot forge (the agent passes submit=true and force=true on its own call). and the Assistant Audit Log is a frappe doctype record, not a hash-chained append-only receipt. it shrinks the blast radius further than anyone else here. it does not put a floor under the credential.

mascor/frappe-mcp-server (MIT, low traction) reached the same direction on a smaller scale. its api.py calls check_doctype_allowlist(doctype, ‘create’|‘update’|‘delete’) before each write, and logs every call to an “MCP Audit Log” doctype. where it still stops: the allowlist is a doctype gate, not a per-action human consent; the audit log is a normal desk doctype an admin can edit; and delete_doc runs frappe.delete_doc(…, ignore_permissions=True), allowlist-gated but bypassing frappe’s own permission engine on the actual delete.

one more, because it matters. the official frappe/mcp library dispatches a tool call with tool_result = fn(**arguments) and passes readOnlyHint/destructiveHint through as metadata, never checking them before execution. that is correct for a library: it runs the registered tool and leaves authorization to the application and framework around it. the mcp layer is a door. it is not the floor, by design. the floor has to be somewhere else.

B. gateway / proxy enforcement

route the agent through an external gateway that applies policy, then the gateway talks to erpnext. AgentReady (agent-ready.tech, commercial) is the clearest example. in their own words on this forum, they “apply policies, approvals and audit logging around execution” and differentiate risk: “reads are low-risk, draft writes require approval, and submit is treated as high-risk.” that is a real design and i take it seriously.

where B stops is not a competence question, it is a placement question, and the vendor stated it themselves. their words: “Gateway enforcement does not shrink the underlying ERP credential if that credential is used outside the Gateway.” the gateway governs what goes through the gateway. the erpnext credential underneath it is still a set_user key with a full user’s reach. anything that holds that key and skips the gateway posts directly, and erpnext has no floor of its own to refuse it.

C. a restricted erpnext user

“just make a limited user and give the agent that.” it helps, and it is the right instinct. but roles attach to the user. the credential inherits that user’s full reach across every endpoint the roles allow. there is still no scope on the key, still no consent before a submit, still no immutable record tying an action to an agent, a purpose, and an approval. a limited user shrinks the blast radius. it does not put a floor under the credential.

what i built, and the part that proves i did the work two artifacts, one law. no debit without a credit, applied to access: no capability without a grant.

the floor, pacioli-guard. a frappe app. it rides frappe’s own public auth_hooks extension point, which runs after the api key authenticates and before the request dispatches (auth.py:639, 742-744). it binds any credential to an allowlist of methods and doctypes, deny-by-default. no core fork, a plain bench install-app. it governs every credential on the site, agent or not. if the raw credential cannot do it, nothing holding that credential can do it.

the spine, the broker. a governed mcp server. every write rides one path: plan, consent, execute, prove. plan runs erpnext’s own accounting preview and binds to the document’s modified version, so a stale plan is refused. consent is a single-use, human-minted marker the agent cannot issue. minting is a cli command, not an mcp tool, because if the agent could mint, consent would be self-granted. prove is an append-only, hash-chained receipt. there is no generic delete, no raw rest passthrough, no confirm flag the agent sets on its own call.

what those two buy is three things you cannot add later by being careful: the credential cannot exceed its grant, so a leaked or reused key is bounded instead of total; a write carries a human yes the agent cannot forge or self-issue; and every action leaves a receipt you cannot quietly edit. and the honest limit, in the same breath: the consent step keeps a human in the loop for every write, on purpose. i am not selling an agent you can point at your ledger and walk away from. i am selling that when you are not watching, the credential still cannot wander, and when you approve a write, that approval is real and the record is permanent.

the part that proves the diligence. the broker’s own credential must itself be guard-scoped, or the whole spine is bypassable. anything holding an unscoped broker key could call rest directly and post with zero plan, consent, or prove. i say that in my own docs, in plain words. a floor you can turn off silently is not a floor. i wrote down the one way mine can fail, because that is what reading the code honestly makes you do.

the delta, from the code

the ask

agents are coming to erpnext. the floor is soft, and it is soft by construction, not by anyone’s mistake. so the ask is not switch to me. there are two honest ways to fix the floor.

one, fix it in core. the extension point already exists. auth_hooks runs in the right place. a per-credential allowlist belongs in the platform, and frappe could put it there using its own supported hook.

two, until it does, use mine. pacioli-guard is that floor today. open source, no core fork, given freely. run it as your floor, or read it as the reference for what core should do.

either way, same ask. fix the floor.

check us