feat: expose_doctype() — register CRUD MCP tools from a DocType - #6
Open
Krishnakalani111 wants to merge 6 commits into
Open
feat: expose_doctype() — register CRUD MCP tools from a DocType#6Krishnakalani111 wants to merge 6 commits into
Krishnakalani111 wants to merge 6 commits into
Conversation
First piece for auto-exposing DocTypes as MCP tools.
One factory (build_tool(doctype, op)) that match/cases on op. Get is the first case.
List supports optional filters/fields/limit/offset/order_by. Both _get and _list now build their schema through the existing get_tool() pipeline so hand-written and auto-generated tools follow one code path.
Both derive their input schema from frappe.get_meta(doctype).fields — skipping NO_VALUE_FIELDS and Frappe's DEFAULT_FIELDS. Create marks reqd=1 fields as required; update requires only name.
Public one-liner API:
mcp.expose_doctype("ToDo")
# -> get_todo, list_todo, create_todo, update_todo
Delete is a valid op but excluded from the default operations tuple;
callers opt in explicitly and the tool carries destructiveHint=True
so MCP clients treat it accordingly.
Krishnakalani111
marked this pull request as ready for review
July 11, 2026 17:13
Confidence Score: 4/5Safe to merge after adding an upper bound to the list tool's The list handler passes frappe_mcp/server/tools/doctype.py — the Reviews (2): Last reviewed commit: "refactor: defer create/update schema der..." | Re-trigger Greptile |
Author
|
cc @18alantom @tanmoysrt for review when you get a chance , this builds on the get_tool() pipeline |
Viral-LyFe
added a commit
to Viral-LyFe/mcp
that referenced
this pull request
Jul 17, 2026
Ports the DocType -> MCP tool generator from frappe#6 (feat: add expose_doctype() and opt-in 'delete' op), with create/update/ delete removed entirely rather than kept as opt-in. Lyfe Hardware's MCP integration is phase-1 read-only by design (see apps/lh/docs/erpnext-developer-handoff.md) — the source PR's DEFAULT_OPERATIONS included create/update by default, with delete as opt-in. That shape is a foot-gun for a deployment that must guarantee no write tool is ever registered: calling expose_doctype(mcp, doctype) without arguments would silently grant write access. This fork's build_tool()/expose_doctype() only accept "get"/"list" — passing "create"/"update"/"delete" raises ValueError. There is no code path that can insert, save, or delete a Frappe document. Kept from upstream: frappe.has_permission() checks on every handler (get requires read, list requires read), the fieldtype -> JSON Schema mapping, and the bounded list() pagination (limit default 20, minimum 1) that addressed an earlier reviewer comment on the source PR about an unbounded query. Also includes two follow-up fixes folded into this same commit for a clean linear history matching what's documented and tested elsewhere: fix: apply permlevel field-hiding in get_<doctype> handler frappe.get_doc() does NOT apply permlevel field-hiding automatically — that's only wired into frappe.client.get_doc's REST path and frappe.model.db_query (list views). Without an explicit call to doc.apply_fieldlevel_read_permissions(), the _get() handler leaked permlevel-restricted field values to any caller with base doctype read access, regardless of permlevel clearance. Found while running acceptance tests against a real Frappe deployment, not assumed correct from the design. Note: the masked field's KEY still appears in as_dict()'s output — only the VALUE becomes None. test: update fakes for apply_fieldlevel_read_permissions() call The fake frappe stand-ins needed apply_fieldlevel_read_permissions() as a no-op to match the real interface the handler now calls.
Viral-LyFe
added a commit
to Viral-LyFe/mcp
that referenced
this pull request
Aug 4, 2026
Ports the DocType -> MCP tool generator from frappe#6 (feat: add expose_doctype() and opt-in 'delete' op), with create/update/ delete removed entirely rather than kept as opt-in. Lyfe Hardware's MCP integration is phase-1 read-only by design (see apps/lh/docs/erpnext-developer-handoff.md) — the source PR's DEFAULT_OPERATIONS included create/update by default, with delete as opt-in. That shape is a foot-gun for a deployment that must guarantee no write tool is ever registered: calling expose_doctype(mcp, doctype) without arguments would silently grant write access. This fork's build_tool()/expose_doctype() only accept "get"/"list" — passing "create"/"update"/"delete" raises ValueError. There is no code path that can insert, save, or delete a Frappe document. Kept from upstream: frappe.has_permission() checks on every handler (get requires read, list requires read), the fieldtype -> JSON Schema mapping, and the bounded list() pagination (limit default 20, minimum 1) that addressed an earlier reviewer comment on the source PR about an unbounded query.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
mcp.expose_doctype("ToDo") -> registers CRUD tools
for a DocType in one line.
Right now if you want an app with, say, 10 DocTypes AI-accessible, that's
40-ish @mcp.tool() functions to write and each one re-implements the
same field-schema derivation, permission check, and get_doc/insert/save
glue. This is that boilerplate pushed into the library.
Schemas come from frappe.get_meta(doctype).fields , skipping
NO_VALUE_FIELDS (Section Break, HTML, Table, etc.) and DEFAULT_FIELDS
(owner, creation, ...). Fieldtype -> JSON Schema is in a small helper
that handles the special cases: Select becomes an enum, Link and
Dynamic Link carry the target as an x-frappe-link-doctype hint so an
LLM knows what to pass. Read-side tools go through the existing
get_tool() pipeline so their inputSchema comes from the handler's type
hints + docstring , same path as hand-written tools.
Permission checks are on by default; there's a check_permissions=False
escape for testing. Delete carries destructiveHint=True and is
excluded from the default operations tuple.
Testing
79 tests covering the fieldtype mapping, per-op schemas, handler
behavior with mocked frappe, and expose_doctype's dispatch. Also ran
it against a live Frappe site via Claude Desktop screenshots below.
Screenshots
Open questions
Happy to add these in this PR or a follow-up,
fields=[...]allowlist onexpose_doctypeso a caller can scopewhich DocType fields become tool-callable.
Docs
The README's Documentation section currently covers
@mcp.tooland@mcp.registerbut has nothing onexpose_doctype()— a subsection needs to be added there alongside them. Happy to include in this PR or a follow-up.