""" Level 1 — Foundation tools. These are the absolute minimum. Without these, the AI is blind. """ import json from mcp.types import Tool from frappe_mcp.client.frappe_api import FrappeClient def tools() -> list[Tool]: return [ Tool( name="frappe_ping", description="Check whether Frappe is reachable. Returns version and status.", inputSchema={"type": "object", "properties": {}}, ), Tool( name="frappe_get_session_info", description=( "Returns the current session: logged-in user, roles, site name, " "enabled modules, and Frappe version. Call this first to understand context." ), inputSchema={"type": "object", "properties": {}}, ), Tool( name="frappe_get_doctype_permissions", description=( "Returns what the current user can do on a DocType: " "read, write, create, delete, submit, cancel, amend, print, email, export, import." ), inputSchema={ "type": "object", "required": ["doctype"], "properties": { "doctype": {"type": "string"}, }, }, ), Tool( name="frappe_list_modules", description=( "List all Frappe/ERPNext modules available on the site " "(Selling, Buying, Stock, Accounts, HR, CRM, Support, Custom, etc.)." ), inputSchema={"type": "object", "properties": {}}, ), Tool( name="frappe_get_doctype_meta", description=( "Get comprehensive DocType schema: all fields with types, required flags, " "options, child tables, link fields, and permission hints. " "Essential before creating or editing documents." ), inputSchema={ "type": "object", "required": ["doctype"], "properties": { "doctype": {"type": "string"}, "include_permissions": {"type": "boolean", "default": True}, }, }, ), ] def handlers() -> dict: return { "frappe_ping": _ping, "frappe_get_session_info": _get_session_info, "frappe_get_doctype_permissions": _get_doctype_permissions, "frappe_list_modules": _list_modules, "frappe_get_doctype_meta": _get_doctype_meta, } async def _ping(args: dict) -> str: client = FrappeClient() try: versions = await client.call_method("frappe.utils.change_log.get_versions") frappe_ver = versions.get("frappe", {}).get("version", "unknown") if isinstance(versions, dict) else "unknown" return json.dumps({"status": "ok", "frappe_version": frappe_ver, "url": client.base_url}, indent=2) except Exception as e: return json.dumps({"status": "unreachable", "error": str(e)}, indent=2) async def _get_session_info(args: dict) -> str: client = FrappeClient() user = await client.call_method("frappe.auth.get_logged_user") user_doc = await client.get_doc("User", user) roles = [r["role"] for r in user_doc.get("roles", [])] versions = await client.call_method("frappe.utils.change_log.get_versions") modules_raw = await client.get_list( "Module Def", fields=["name", "app_name"], limit=100, ) return json.dumps({ "user": user, "roles": roles, "frappe_url": client.base_url, "site_name": client.settings.frappe_site_name or "(default)", "frappe_version": versions.get("frappe", {}).get("version") if isinstance(versions, dict) else None, "installed_apps": list(versions.keys()) if isinstance(versions, dict) else [], "modules": [m["name"] for m in (modules_raw if isinstance(modules_raw, list) else [])], }, indent=2) async def _get_doctype_permissions(args: dict) -> str: client = FrappeClient() result = await client.call_method( "frappe.client.get_perm", doctype=args["doctype"], ) return json.dumps(result, indent=2) async def _list_modules(args: dict) -> str: client = FrappeClient() result = await client.get_list( "Module Def", fields=["name", "app_name"], limit=150, order_by="name asc", ) return json.dumps(result, indent=2) async def _get_doctype_meta(args: dict) -> str: client = FrappeClient() meta = await client.call_method( "frappe.desk.form.load.getdoctype", doctype=args["doctype"], with_parent=1, cached_timestamp=None, ) if args.get("include_permissions", True): try: perms = await client.call_method("frappe.client.get_perm", doctype=args["doctype"]) if isinstance(meta, dict): meta["_permissions"] = perms except Exception: pass return json.dumps(meta, indent=2)