On this page
  1. Install
  2. Get your API key from Frappe
  3. Create a profile
  4. First 5 commands
  5. Connect your AI assistant
  6. Safe read-only mode
  7. Troubleshooting

1. Install

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.

β„Ή
Don't have Bun? Install it with curl -fsSL https://bun.sh/install | bash β€” takes about 10 seconds. Bun is a fast JavaScript runtime that ships as a single binary.

2. Get your API key from Frappe

Frappe uses token key:secret auth β€” not Bearer, not Basic.

1

Open User Settings

In your Frappe/ERPNext site, click your avatar (top right) β†’ My Settings.

2

Find API Access

Scroll to the API Access section. If no key exists yet, click Generate Keys.

3

Copy both values

You need two things: API Key and API Secret. The secret is only shown once β€” copy it now.

⚠
Frappe Cloud / erpnext.com? If your site URL ends in .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.

3. Create a profile

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>.

Multiple sites

$ 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
βœ“
Agent sessions Set FRAPPE_CTL_CONFIG_DIR=/tmp/agent-session-xyz to give an agent a sandboxed config directory β€” it won't touch your personal profiles.

4. First 5 commands

Verify your connection and explore before writing anything.

Check the connection

$ 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.

List some docs

$ frappe-ctl next get Customer --limit 5
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ name                    β”‚ customer_nameβ”‚ customer_typeβ”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ CUST-0001               β”‚ Acme Corp    β”‚ Company      β”‚
β”‚ CUST-0002               β”‚ Jane Doe     β”‚ Individual   β”‚
β”‚ ...                                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Inspect a DocType's schema

$ 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

Search instead of filter

$ frappe-ctl next search Customer "acme"
CUST-0001   Acme Corp   Company

Get a single doc (token-efficient)

$ 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.)
CommandWhat it does
next count CustomerHow many Customers exist
next get "Sales Order" --filter "status=Open"Filter by field value
next describe "Purchase Order" --compactCompact schema β€” 94% smaller
next search Project "road"Fuzzy text search by title field
next link "Sales Order" SO-001 customerFollow 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 resourcesList all DocTypes for ERPNext

5. Connect your AI assistant

Two ways β€” skill file for any AI, MCP server for Claude Desktop / Cursor.

Skill file β€” give any AI instant context

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.

PlatformHow to loadAuto?
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 Code one-liner Add this to your project CLAUDE.md:
@./node_modules/frappe-ctl/frappe-ctl.skill.md
Claude Code loads it automatically on every session.

MCP server β€” direct tool calls

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.

πŸ€–

Claude Desktop

Edit claude_desktop_config.json.

Mac: ~/Library/Application Support/Claude/
Windows: %APPDATA%\Claude\

⚑

Cursor / VS Code

Edit .cursor/mcp.json or VS Code MCP settings.

Same JSON config format.

Read-only (safe for exploration)

claude_desktop_config.json Β· mcpServers
{
  "mcpServers": {
    "frappe": {
      "command": "frappe-ctl",
      "args": ["mcp"],
      "env": {}
    }
  }
}

Exposes 5 read-only tools: frappe_get, frappe_count, frappe_search, frappe_describe, frappe_validate.

With write access

claude_desktop_config.json Β· mcpServers
{
  "mcpServers": {
    "frappe": {
      "command": "frappe-ctl",
      "args": ["mcp", "--allow-mutations"],
      "env": {}
    }
  }
}

Adds frappe_create, frappe_patch, frappe_delete. Delete still requires force: true.

Pin to a specific profile

"args": ["mcp", "--site", "prod"]

Verify it's working

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.

β„Ή
Non-default profile? Add "--site", "prod" to the args array to pin the MCP to a specific profile.

6. Safe read-only mode

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:

claude_desktop_config.json
  "env": { "FRAPPE_CTL_READONLY": "1" }

7. Troubleshooting

401 Unauthorized

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).

403 Forbidden

The user has API access but lacks permission for this DocType. Add the DocType to the user's role permissions in Frappe.

command not found: frappe-ctl

Bun's global bin directory is not in your PATH. Add it:

$ echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.zshrc
$ source ~/.zshrc

HTTP 417 on filters

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.

Output is a table but I want JSON

JSON is the default when stdout is not a TTY (i.e., when piped). Force it explicitly:

$ frappe-ctl next get Customer -o json

MCP not appearing in Claude Desktop

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.

Still stuck?

Open an issue at github.com/MalharDotTech/frappe-ctl/issues. Include the command you ran and the full error output.