SL

soumyasethy/lazychat-erpnext

Productivity workflow
21 stars 7 forks Quality 65 Trend 14MIT

LazyChat — an AI assistant docked inside your ERPNext desk. Chat with your data, draft reports, stage edits (Apply-gated).

Overview

95 permission-scoped tools · two-phase mutations · composer-critic verification · BYO LLM ↑ 78-second flagship walkthrough — stakeholder ask → tool dispatch → report URL → BYO LLM in one shot. (Want HD? ▶ download the MP4 .) ↑ Still hero — the post-dispatch state of the panel, mid-conversation. "Hey, I need to verify which December purchase invoices have payment entries against them. Can we get a report by EOD?" That ask used to mean opening 3 Frappe doctypes, writing a custom Query Report, debugging joins, fixing field names, and probably a meeting. With lazychat, the consultant types the stakeholder's words verbatim into the chat panel. Step 1 — Type the stakeholder's request The consultant pastes the verbatim ask into the chat composer. Step 2 — Lazychat dispatches tools, returns data describe_doctype → run_sql_select → inline result table. Real data. No copy-pasting from /api/method .

README

From stakeholder request to delivered report — in minutes

“Hey, I need to verify which December purchase invoices have payment entries against them. Can we get a report by EOD?”

That ask used to mean opening 3 Frappe doctypes, writing a custom Query Report, debugging joins, fixing field names, and probably a meeting. With lazychat, the consultant types the stakeholder’s words verbatim into the chat panel.


Bring your own LLM in 30 seconds

Self-hosted LM Studio? Anthropic? NVIDIA NIM? OpenRouter? Together? Groq? You don’t fill out a form. You paste a curl snippet from the provider’s own docs — and lazychat parses it into endpoint, model, auth, headers, streaming flag, even provider-specific payload extras. The API key stays in the browser (browser-LLM path); no server-side credential storage, no shared org-key risk.

The 4-step flow

Tested provider snippets (paste these verbatim)

What the parser auto-fills from the curl

Curl part Form field
https://api.anthropic.com/v1/messages URL ends /messages → Format = anthropic
https://*/v1/chat/completions URL ends /chat/completions → Format = openai
-H "Authorization: Bearer XYZ" Auth type = Bearer; Token = XYZ
-H "x-api-key: XYZ" Auth type = API Key; Header name = x-api-key; Token = XYZ
-H "Accept: text/event-stream" Streaming = ✓
Any other -H "X: Y" (e.g. anthropic-version, HTTP-Referer) Extra headers (kv list)
Payload "model":"X" Model = X
Payload "max_tokens":N / "temperature":N / "top_p":N Defaults block
Payload "stream":true Streaming = ✓
Any payload key not in the above Extra payload (preserved as JSON, merged into request body verbatim)
Hostname (api.anthropic.com) Label auto-generated as Anthropic Direct-style title

Test connection sends one messages: [{"role":"user","content":""}] request to the configured endpoint with the parsed auth + extras. Returns the HTTP status + first 800 chars of the response body in a green/red panel — instant proof the credential and endpoint work before you start using the model in real chats.

Same auto-fill works for requests.post(...) snippets too — paste a Python snippet from a provider’s quickstart and it parses identically.


Charts and dashboards rendered in-chat

Tools like make_chart, dashboard_chart_data, and number_card_value don’t return raw JSON to the user — the chat-ui’s ChartBlock renders them inline as proper Vega charts and KPI cards, in the panel, alongside the conversation. The consultant asks “chart of paid PIs by month” and the chart appears as the answer. No tab-switching, no exporting to a BI tool, no Frappe Dashboard form-filling.

Bar / line / area / donut / scatter all map to Frappe Charts via the make_chart tool. Multi-chart dashboards (with cards + charts grouped) ship via prepare_create_dashboard — you ask, lazychat stages, you click Apply, the dashboard appears at /app/dashboard-view/ ready to bookmark.


How tool results render in the chat

Tool calls don’t return raw JSON dumps to the user. Lazychat’s response renderer formats the LLM’s reply using the full markdown stack — GitHub-flavored tables, syntax-highlighted code with shiki, mermaid diagrams, KaTeX math, blockquote callouts, structured headings — so a “tool result” reads like an answer, not a debug log.

Plus: KaTeX math (for reconciliation formulas), JSON tree expanders for raw tool outputs, automatic link-card previews for any URL the LLM cites, attachment renders for extract_file_content PDF/DOCX/CSV outputs, and a streaming markdown parser that keeps tables and code blocks structurally valid mid-token. The tool gives the data; the renderer makes it readable.


Install

Frappe Cloud: install from the marketplace in one click.

Self-hosted bench: see INSTALL.md — the standard bench install (the pre-built chat-ui ships with the app, so no build step needed), building from source, and HMR dev are all documented there.

After install, the app seeds disabled-by-default LLM Provider rows (OpenAI, Anthropic, NVIDIA, OpenRouter, Vercel AI, LM Studio) — enable one and add your API key from /app/llm-provider, or skip server-side config entirely and let users bring their own keys via the chat-ui’s model picker (browser-LLM path).


Why lazychat-erpnext

  • Built for ERPNext, not bolted on. Every tool runs as frappe.session.user — Frappe permissions, role checks, workflow guards, and the audit trail apply automatically. No god-mode bypass; no separate auth surface.
  • Mutations require explicit Apply. The LLM stages every write to a Redis token; you click Apply (or the 3-second auto-Apply countdown for low-risk actions) to commit inside frappe.db.savepoint. A 30-second composer-critic LLM second-opinion shows up as an amber strip when it disagrees with the staged action.
  • Bring any model. Anthropic Claude, OpenAI, NVIDIA NIM, OpenRouter, Vercel AI Gateway, Together, Groq, LM Studio. Same 95-tool registry; the API key never has to leave the browser if you don’t want it to.

What you get

Built-in: schema-aware SQL retry on Unknown column, two chat paths (server-orchestrated or browser-LLM), real-execution probe before staging Query Reports, structured form prefill for HTTP-414-defying URLs, knowledge bases with reindex, scheduled jobs, dashboards, custom fields, client scripts, and an admin panel that moves all configuration into the chat-ui itself.

Token + cost tracking NEW

Every LLM turn writes a row to the Lazychat Usage Log doctype — model · provider · input/output tokens · cost estimate (USD) · session id · path (backend / browser). Cost auto-computed from LLM Model.input_price_per_mtok / output_price_per_mtok first, falls back to a built-in rate table for common Claude / GPT models, zero when both miss.

Open the chat-ui’s Command Palette → Account & usage… to see:

  • 4-tile totals (calls / input / output / cost)
  • By-model rollup — sorted by total tokens, shows provider + cost per model
  • Daily breakdown for the last 14 days
  • Range picker (Today / 7d / 30d / 90d)
  • System Manager users see ALL users’ usage; everyone else sees their own

No setup required — works for Claude / OpenAI / NVIDIA / OpenRouter / Vercel / Anthropic / Together / Groq / LM Studio, regardless of whether you’re on the backend-LLM or browser-LLM path. Set per-model rates on /app/llm-model/ if you want exact billing.


Architecture

The Frappe app ships a 280-line vanilla-JS shim (public/js/lazychat_panel.bundle.js) loaded via app_include_js on every Desk page. The shim mounts the chat-ui (a React app, sibling repo lazychat.ai, bundled into public/lazychat_dist/) as a same-origin iframe, sets up the postMessage protocol, and intercepts /commit slash commands to call the server.

Tool dispatch goes through one of two paths, both backed by the same 95-tool registry:

Path LLM lives Tool dispatch Best when
Backend-LLM Frappe (LLM Provider doctype) run_agentic_turn calls execute_tool in-process Org deployments, shared keys, central audit
Browser-LLM chat-ui (BYO key in localStorage) chat-ui calls mcp.handle JSONRPC per tool_use Single-user / power-user; key never touches server

Default chat_path = auto: chat-ui inspects the active model — built-in → backend; custom-added → browser. Both paths run with frappe.session.user’s permissions, both write to Claude Conversation, both share tools.py. Zero drift, one implementation.


Configuration

Primary admin surface (in-app): Command Palette → Server config → 3 tabs:

  • General — gates (allow_email, allow_dangerous_tools, cycle9_enabled …), chat_path radio, iframe_base_url, llm_proxy_allowed_hosts chip list.
  • LLM Providers — Anthropic / OpenAI-compatible CRUD with Test Connection + Reveal API Key.
  • LLM Models — model picker contents; is_default toggle (single-default invariant enforced).

Or edit the Lazychat Settings doctype directly. All defaults are allow-on for self-hosted single-org installs; defense-in-depth is preserved (System Manager role check at tool dispatch + /commit confirmation per call).

Field Default What it does
enabled true Master switch — mount the panel at all
iframe_base_url /assets/lazychat_erpnext/lazychat_dist/index.html Where chat-ui loads from. Override for HMR (http://127.0.0.1:5173) or remote chat-ui
iframe_query_params ?frame=sidebar Appended to base_url
chat_path auto auto / browser / backend — see Architecture above
mcp_endpoint /api/method/lazychat_erpnext.desk_assistant.mcp.handle Read-only; browser-LLM path uses this
legacy_widget_enabled false Mount the OLD vanilla-JS widget INSTEAD of the iframe (mutually exclusive)
allow_email true Enable prepare_send_email
allow_dangerous_tools true Enable prepare_run_sql + prepare_run_python (still gated by System Manager role + /commit)
allow_email_setup true Enable prepare_create_email_account
cycle9_enabled true Enable composer-critic verdict, verification briefs, exemplar memory
bulk_update_max_rows 500 Ceiling for prepare_bulk_update blast radius
llm_proxy_allowed_hosts [anthropic, openai, nvidia, openrouter, vercel, …] Browser-LLM proxy allowlist

site_config.json overrides win over the doctype values (backward compat). boot.py:get_lazychat_settings() is the single resolver — use this helper anywhere on the server side.


Tool catalog — all 101

Grouped into 12 categories. Every tool runs scoped to frappe.session.user’s permissions; mutations stage to a Redis token and require explicit Apply. The Try it line is verbatim text you can paste into the chat panel right now (assuming you have ecommerce-shaped data — the prompts work great against the canonical ERPNext demo dataset).


Mutations — prepare_* (40)

Every state-changing tool is a prepare_ that stages the action to a 5-min Redis token, returns a preview, and waits for /commit (typed by the user or fired by the chat-ui’s Apply button). The server re-checks permissions and runs inside frappe.db.savepoint. The LLM is physically incapable of committing on its own — commit_prepared_action is not in the tool registry.

Mutations marked 🔍 critic get a composer-critic LLM verdict appended to the preview ({verdict, severity, mismatches, suggested_revisions}). The chat-ui renders an amber strip when verdict=mismatch. As of Cycle 12 M2: 12 tools wire the critic.

Mutations marked ⚡ low-risk (the LOW_RISK_ACTIONS set in chat-ui) auto-Apply with a 3-second countdown in Edit-auto mode. Everything else requires explicit Apply click.


External MCP clients

The same tool registry that powers the in-Desk panel is reachable from any MCP-compliant client. Three auth modes, three endpoints — pick the one that matches your client.

A — Claude Code (CLI) — simplest

Built-in HTTP MCP transport. ~30 seconds. No tunnel needed if Frappe is on the same machine.

# 1. Set a static Bearer token in site_config
bench --site  set-config lazychat_mcp_bearer_token "$(python3 -c "import secrets;print(secrets.token_urlsafe(32))")"

# 2. Wire into Claude Code (user scope = available in any directory)
claude mcp add --scope user --transport http lazychat-erpnext \
  http://localhost:8000/api/method/lazychat_erpnext.desk_assistant.mcp.handle_bearer \
  --header "Authorization: Bearer "

# 3. Restart Claude Code → all 101 tools available

B — Claude Desktop / MCP Inspector / any token-aware client

Works against either endpoint:

  • mcp.handle with Frappe API key+secret (Authorization: token KEY:SECRET) — standard Frappe auth, generate key at /app/user → API Access → Generate Keys.
  • mcp.handle_bearer with the static Bearer token from step A above (Authorization: Bearer ) — simpler if you don’t want to manage Frappe API keys.
{
  "mcpServers": {
    "lazychat-erpnext": {
      "command": "npx",
      "args": ["-y", "mcp-remote",
               "https://your-bench.example.com/api/method/lazychat_erpnext.desk_assistant.mcp.handle_bearer",
               "--header", "Authorization: Bearer YOUR_TOKEN"]
    }
  }
}

C — claude.ai web Custom Connector (Pro / Team / Enterprise)

OAuth 2.1 with auto-discovery. The MCP Authorization spec (2025-06) is fully wired: /.well-known/oauth-protected-resource (RFC 9728), /.well-known/oauth-authorization-server (RFC 8414), WWW-Authenticate: Bearer resource_metadata="..." on 401. Claude.ai discovers everything automatically.

Setup (one-time, ~3 min):

In Frappe Desk, open /app/oauth-client → + New:

Field Value
App Name Claude.ai (lazychat MCP)
Scopes all openid
Grant Type Authorization Code
Response Type Code
Redirect URIs (one per line) https://claude.ai/api/mcp/auth_callback``https://claude.com/api/mcp/auth_callback

Save. The auto-generated name becomes your client_id; client_secret is shown on the doc (hold-Shift-click the Client Secret field to reveal).

Both callback URLs must be present per Anthropic docs — claude.ai users hit one, claude.com users hit the other.

In claude.ai → Settings → Connectors → Add Custom Connector:

Field Value
Remote MCP server URL https://your-public-bench.example.com/api/method/lazychat_erpnext.desk_assistant.mcp.handle
OAuth Client ID (Advanced settings) from the bench command above
OAuth Client Secret from the bench command above

Click Add. claude.ai redirects you to Frappe → log in → “Confirm Access” → back to claude.ai with all 101 tools registered.

Public-URL caveat: claude.ai’s backend lives in Anthropic’s cloud — localhost is not reachable. Either deploy Frappe on a public HTTPS URL or use cloudflared (free, no interstitial unlike ngrok-free) to tunnel localhost:8000.

Permission model — same for all three

The configured user (Bearer’s run-as user, OAuth-token’s user, or API-key’s owner) is what frappe.session.user reads. Every tool re-checks frappe.has_permission(...) on every call. Two-phase mutations (prepare_* → user-confirms → commit_prepared_action) still apply. No god-mode bypass on any path.


Smoke tests

Two layers, both must be green to ship:

# Layer 1 — in-process (95 cases as of cycle 13 M3)
cp lazychat-erpnext/scripts/smoke-test-tools.py \
   /apps/lazychat_erpnext/lazychat_erpnext/_smoke.py
cd  && bench --site  execute lazychat_erpnext._smoke.run
# expected: === 244 pass, 0 fail, 2 skip ===

# Layer 2 — HTTP MCP wire (all 101 tools)
python3 lazychat-erpnext/test/curl_smoke.py
# expected: tools registered: 95, called: 95

The smoke gates exist to catch drift between schema, implementation, and live behavior — please run them before opening a PR. See CONTRIBUTING.md for the full checklist.


Cycle 15 — Apply-path hardening (2026-05-19)

Real-user transcript on claude-haiku-latest: agent stuck looping on a Print Format creation. Four root causes diagnosed and fixed in one cycle.

Bug Fix Where
URL slug 404 on prepare_create_* Apply New _doctype_url_slug() helper replaces frappe.scrub() in commit-response link; Print Format now → print-format (hyphen), not print_format (underscore) tools.py
Agent loops on duplicate-create instead of switching to update New _exists_redirect_to_update() helper pre-checks 14 typed prepare_create_* wrappers; returns structured prepare_update_doc hint on duplicate tools.py
PREP_TTL_SEC = 300 too short for multi-turn loops Bumped to 1800 (30 min); _retrieve_action refactored to distinguish malformed / expired-or-consumed / wrong-user with actionable messages tools.py
Agent emits ghost CTAs (“Click Apply” with no card attached) MAX_TURNS 8 → 16; new CTA HONESTY + DUPLICATE PIVOT rules in system prompt (mirrored chat-ui); ghost-CTA detection in agentRunner.ts surfaces an inline warning claude_bridge.py + agentRunner.ts

End-to-end browser replay

Verified claude-haiku-latest with the exact transcript that triggered the original bug. Full evidence in .github/assets/cycle-15/ including the browser-replay result writeup.

Smoke gates: in-process 283 → 293 (+10 new T103a-T103o cases, zero new failures), HTTP-wire 91/91 unchanged, chat-ui vitest 472 → 475 (+3 ghost-CTA detection). Spec: docs/superpowers/specs/2026-05-16-cycle-15-apply-path-hardening-design.md.


Roadmap

Cycle Status Theme
2 — Multi-tab agent runtime ✅ done Streams, sessions, queue, retry — chat-ui side
3 — Rich rendering + extension primitives ✅ done Markdown, syntax highlight, charts, custom components
5 — MCP timeouts + observability + voice + Desk navigation ✅ done 60s per-tool timeout, 250 KB result cap, voice input, clickable Desk links
7 — Compound questions + self-correcting /commit ✅ done run_sql_select, run_python_readonly, plan-first prompt, schema-aware error retry
8 — Real Modes + Effort ✅ done Ask / Edit-auto / Plan / Auto, low/medium/high/max effort tiers
9 M1-M4 — Cycle 9 (composer-critic + exemplars + PEVR primitives) ✅ done Verification briefs, intent_signature exemplar memory, schema graph cache
10 — chat-ui admin panel + allow-all defaults ✅ done All ERPNext config moved into the chat-ui
11 M1-M4 — UX hardening ✅ done CommitCard, stage-and-redirect prefill, structured SQL gate, live tool progress
12 M1-M2 — Critic coverage expansion ✅ done Critic now grades 12 prepare_* tools (helper-extracted)
13 — README rewrite 🚧 in progress This document
15 — Apply-path hardening ✅ done URL slugs · duplicate-pivot · token TTL · turn budget (this section above)
Future — GitHub Actions CI badges, per-tool deep-dive docs, multi-language 📅 deferred

Contributing

See CONTRIBUTING.md for branch convention, commit style, smoke-gate requirements, and the new-tool checklist.

Sister repo: lazychat.ai (the chat-ui React app this Frappe app embeds). Any change to the postMessage protocol, host SDK, or extension primitives lands there.


License

MIT © Soumya Sethy.

View the full README on GitHub

Install

npx -y mcp-remote https://your-bench.example.com/api/method/lazychat_erpnext.desk_assistant.mcp.handle_bearer --header Authorization: Bearer YOUR_TOKEN
New to MCP? Start with the mcpskill.org guide →