From install to your first live query against ERPNext β no guessing, no boilerplate.
Requires Bun β₯ 1.3.0.
$ bun add -g frappe-ctl $ frappe-ctl --version 0.2.0
No npm/yarn needed. Bun installs the binary globally and it runs natively β no Node transpile step.
curl -fsSL https://bun.sh/install | bash β takes about 10 seconds.
Bun is a fast JavaScript runtime that ships as a single binary.
Frappe uses token key:secret auth β not Bearer, not Basic.
In your Frappe/ERPNext site, click your avatar (top right) β My Settings.
Scroll to the API Access section. If no key exists yet, click Generate Keys.
You need two things: API Key and API Secret. The secret is only shown once β copy it now.
.erpnext.com or .frappe.cloud, use OAuth instead:
frappe-ctl auth login --client-id <your_client_id>.
This opens a browser for PKCE login β no API key needed.
Ask your site admin for the OAuth client ID.
Profiles store your site URL + credentials. You can have multiple (dev, staging, prod).
$ frappe-ctl profile add prod \ --url https://yoursite.erpnext.com \ --key <api_key> \ --secret <api_secret> $ frappe-ctl profile use prod
Config is saved to ~/.config/frappe-ctl/config.json. You can override it per-command with --site <profile>.
$ frappe-ctl profile add uat --url http://localhost:8080 --key k --secret s $ frappe-ctl profile list prod https://yoursite.erpnext.com (active) uat http://localhost:8080 $ frappe-ctl --site uat next get Customer # override per-command
FRAPPE_CTL_CONFIG_DIR=/tmp/agent-session-xyz to give an agent a sandboxed config directory β it won't touch your personal profiles.
Verify your connection and explore before writing anything.
$ frappe-ctl next count Customer 142
If you see a number, you're connected. If you see an auth error, double-check your API key and secret.
$ frappe-ctl next get Customer --limit 5 βββββββββββββββββββββββββββ¬βββββββββββββββ¬βββββββββββββββ β name β customer_nameβ customer_typeβ βββββββββββββββββββββββββββΌβββββββββββββββΌβββββββββββββββ€ β CUST-0001 β Acme Corp β Company β β CUST-0002 β Jane Doe β Individual β β ... β βββββββββββββββββββββββββββ΄βββββββββββββββ΄βββββββββββββββ
$ frappe-ctl next describe "Sales Order" --required # Shows only the fields that are required to create a Sales Order # Much smaller than the full 170-field schema
$ frappe-ctl next search Customer "acme" CUST-0001 Acme Corp Company
$ frappe-ctl next get Customer CUST-0001 --sparse --strip-meta # --sparse strips null/empty fields (~55% smaller) # --strip-meta removes Frappe system fields (owner, creation, etc.)
| Command | What it does |
|---|---|
| next count Customer | How many Customers exist |
| next get "Sales Order" --filter "status=Open" | Filter by field value |
| next describe "Purchase Order" --compact | Compact schema β 94% smaller |
| next search Project "road" | Fuzzy text search by title field |
| next link "Sales Order" SO-001 customer | Follow a Link field in one call |
| next validate "Sales Order" --data '{"customer":"Acme"}' | Pre-flight required fields before writing |
| next diff Project PROJ-001 --data '{"status":"Completed"}' | Preview what a patch would change |
| next resources | List all DocTypes for ERPNext |
Two ways β skill file for any AI, MCP server for Claude Desktop / Cursor.
frappe-ctl.skill.md ships with the package. Drop it into your AI and it immediately knows how to use the CLI: token-efficient patterns, verb reference, safety rules, output parsing.
| Platform | How to load | Auto? |
|---|---|---|
| Claude Code | Add @frappe-ctl.skill.md to your project CLAUDE.md |
manual |
| Claude Desktop | Add file as project knowledge or paste into system prompt | manual |
| Cursor | .cursor/rules/frappe-ctl.mdc β already in the repo |
β auto |
| OpenAI Codex CLI | AGENTS.md at project root β already in the repo |
β auto |
| ChatGPT / Perplexity | Paste frappe-ctl.skill.md contents into custom instructions |
manual |
After installing globally, find the skill file at:
$ cat $(npm root -g)/frappe-ctl/frappe-ctl.skill.md
CLAUDE.md:
@./node_modules/frappe-ctl/frappe-ctl.skill.mdClaude Code loads it automatically on every session.
Exposes frappe-ctl as typed tools so Claude / Cursor can call ERPNext directly without you writing commands. Best for Claude Desktop, Cursor, and VS Code.
Edit claude_desktop_config.json.
Mac: ~/Library/Application Support/Claude/
Windows: %APPDATA%\Claude\
Edit .cursor/mcp.json or VS Code MCP settings.
Same JSON config format.
{
"mcpServers": {
"frappe": {
"command": "frappe-ctl",
"args": ["mcp"],
"env": {}
}
}
}
Exposes 5 read-only tools: frappe_get, frappe_count, frappe_search, frappe_describe, frappe_validate.
{
"mcpServers": {
"frappe": {
"command": "frappe-ctl",
"args": ["mcp", "--allow-mutations"],
"env": {}
}
}
}
Adds frappe_create, frappe_patch, frappe_delete. Delete still requires force: true.
"args": ["mcp", "--site", "prod"]
Restart Claude Desktop / Cursor, open a new chat, ask:
How many open Sales Orders are there on my ERPNext site?
If connected, the AI calls frappe_count and returns a live number.
"--site", "prod" to the args array to pin the MCP to a specific profile.
Hard-block all mutations for read-only agent sessions or demos.
$ FRAPPE_CTL_READONLY=1 frappe-ctl next get Customer # All write verbs (create, patch, delete, submit, ...) are blocked at the CLI level # Even --allow-mutations on the MCP is ignored when this env var is set
Useful when giving an agent access to explore a production site without any write risk. Set it in the MCP env block:
"env": { "FRAPPE_CTL_READONLY": "1" }
API key or secret is wrong, or the user doesn't have API access enabled. In Frappe go to User β API Access β Generate Keys. Make sure you copied the secret (not just the key β they're different values).
The user has API access but lacks permission for this DocType. Add the DocType to the user's role permissions in Frappe.
Bun's global bin directory is not in your PATH. Add it:
$ echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.zshrc $ source ~/.zshrc
Frappe returns 417 when a filter field is not in its allowlist, or the in operator receives an array instead of a comma-separated string. Use "field,in,A,B,C" syntax.
JSON is the default when stdout is not a TTY (i.e., when piped). Force it explicitly:
$ frappe-ctl next get Customer -o json
Fully quit and restart Claude Desktop (Cmd+Q, not just close the window). Also verify the frappe-ctl binary is in your PATH by running which frappe-ctl in a new terminal.
Open an issue at github.com/MalharDotTech/frappe-ctl/issues. Include the command you ran and the full error output.