Skip to content

feat: expose_doctype() — register CRUD MCP tools from a DocType - #6

Open
Krishnakalani111 wants to merge 6 commits into
frappe:mainfrom
Krishnakalani111:feat/expose-doctype-tools
Open

feat: expose_doctype() — register CRUD MCP tools from a DocType#6
Krishnakalani111 wants to merge 6 commits into
frappe:mainfrom
Krishnakalani111:feat/expose-doctype-tools

Conversation

@Krishnakalani111

@Krishnakalani111 Krishnakalani111 commented Jul 11, 2026

Copy link
Copy Markdown

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

image Screenshot 2026-07-11 at 10 22 45 PM Screenshot 2026-07-11 at 10 18 00 PM

Open questions

Happy to add these in this PR or a follow-up,

  • A fields=[...] allowlist on expose_doctype so a caller can scope
    which DocType fields become tool-callable.
  • Child Table fieldtypes

Docs

The README's Documentation section currently covers @mcp.tool and @mcp.register but has nothing on expose_doctype() — a subsection needs to be added there alongside them. Happy to include in this PR or a follow-up.

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 Krishnakalani111 changed the title feat: expose_doctype() — register CRUD-ish MCP tools from a DocType feat: expose_doctype() — register CRUD MCP tools from a DocType Jul 11, 2026
@Krishnakalani111
Krishnakalani111 marked this pull request as ready for review July 11, 2026 17:13
@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

Safe to merge after adding an upper bound to the list tool's limit parameter.

The list handler passes limit uncapped to frappe.db.get_list, so a single tool call could fetch an unbounded number of rows and exhaust memory.

frappe_mcp/server/tools/doctype.py — the _list function's schema constraints.

Reviews (2): Last reviewed commit: "refactor: defer create/update schema der..." | Re-trigger Greptile

Comment thread frappe_mcp/server/tools/doctype.py Outdated
@Krishnakalani111

Krishnakalani111 commented Jul 11, 2026

Copy link
Copy Markdown
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant