Skip to main content
Glama
erpuae

Synapse

by erpuae

Synapse

Synapse is an MCP server for Frappe and ERPNext. It lets an AI client read and write a site over OAuth. The client acts as a real Frappe user and stays within that user's permissions. Every call is written to an audit log.

POST https://<your-site>/api/method/synapse.mcp.handle_mcp

What makes it different

Many Frappe MCP servers run with full privileges and give the model raw SQL or document access with permissions turned off. That is fine for a personal sandbox. It is not safe on a business system. Synapse works the other way.

  • It never turns off permissions. Every tool runs as the signed in user. DocType permissions, User Permissions, share rules and submit or cancel rights all apply. Writes go through the normal insert, save, submit and cancel path, so validations, hooks and workflows run the same as they do in the desk.

  • It adds a second layer above permissions. "This user may edit Sales Invoices in the desk" and "an AI agent holding this user's token may edit Sales Invoices" are two different decisions. That second decision is the Synapse Profile.

  • It logs every call, including ones that are refused.

  • It has no external dependencies and creates no roles. A plain bench install-app is the whole install.

Related MCP server: Frappe Assistant Core

Install

bench get-app https://github.com/dxbitz-technology/synapse
bench --site <your-site> install-app synapse

Check the current state of a site at any time:

bench --site <your-site> execute synapse.mcp_tools.check.report

It prints what is set up and what is missing, in the order to fix it. A fresh install is fully closed. Nothing is reachable until you create a profile.

How access works

Access is granted by Synapse Profile records. A profile lists a set of roles and the DocTypes and actions those roles may use. A user's access is the sum of every enabled profile whose roles they hold.

  • With no matching profile, nothing is reachable.

  • A tick in a profile is a ceiling, not a grant. The user still needs the matching Frappe permission on the record. That is checked when the document is touched.

  • Full Access on a profile grants every action on every DocType and ignores the grid. The user's own Frappe permissions become the working limit. Use it only for a user whose Frappe permissions are already scoped the way you want.

  • Allow SQL on a profile turns on the raw SQL tool for its users. Read the SQL section first.

Two fixed rules sit above every profile and cannot be overridden:

  • Some DocTypes are never reachable. These hold tokens, credentials and the records that hand them out, plus Synapse's own settings, profiles and log. Reading them is how a read-only user could turn into a writer, or edit the gate that controls them.

  • Some DocTypes are read only and can never be written. These define the schema, the code and the permission model, for example DocType, Custom Field, Server Script, Custom DocPerm, Role and User. A user who could edit Custom DocPerm could grant themselves anything.

Synapse Settings also has a site wide Blocked DocTypes list. Use it to block something that a profile would otherwise allow.

Tools

Tool

Action needed

list_available_doctypes, describe_doctype

read

get_doc, get_value, get_list, get_count

read

create_doc, update_doc, set_value

write

add_child, set_child_value, set_child_rows, delete_child

write

replace_in_field

write

submit_doc

submit

cancel_doc

cancel

delete_doc

delete

run_operation

operate

run_sql_query

a profile with Allow SQL (see below)

The write tools fall into three bands, all under the write action:

  • Document level: create_doc makes a document, update_doc changes fields on one (and replaces a whole child table if pointed at one), set_value sets a single field.

  • Row level: add_child appends a row and returns its name and idx, set_child_value edits one row (one or more fields, optional expect), set_child_rows edits many rows atomically, delete_child removes a row. Rows are addressed by row.name, taken from a prior get_doc, never by position or content, so the log always names the row it touched. Each runs the parent's real save, so totals and tax recompute the same as in the desk.

  • Field text: replace_in_field changes part of a long text field. It counts how many times the text appears and refuses unless that count matches the number you expected, so it cannot rewrite the wrong part.

For a child table, prefer the row-level tools. Use update_doc on a table only to replace the whole thing on purpose. Reaching for update_doc to change one price would silently discard every other row.

set_child_value, set_child_rows and delete_child take an optional expect of current values. When given, the write is refused if the row no longer holds those values, so an edit cannot land on a row that changed since it was read. A submitted parent is refused, the same as a desk edit would be. run_operation runs a document's own method (see below).

Dates come back in the format set in Synapse Settings, ISO by default. Writes accept ISO or DD-MM-YYYY, so a read then write round trip cannot swap the day and the month.

The operate action

run_operation runs a document's own method by name. This is the behaviour behind a desk button, for example a Sales Invoice reposting its accounting entries. Because it can run code, it has its own action, operate, which is granted per DocType in a profile. That grant is what makes it safe to offer. A profile has to say, for this DocType, that operations may run.

It still runs as the signed in user, under Frappe permissions, and every call is logged. Methods that already have their own tool (save, submit, cancel, delete and so on) are refused here, and so is anything private. So operate cannot be used to get around the other tools.

Custom tools

Any installed app can add its own tools to the Synapse endpoint, so an app can expose the specific jobs it knows how to do rather than only the generic document tools. It is all done through one hook. The app declares each tool as data and needs no import of synapse:

# in myapp/hooks.py
synapse_tools = [
    {"method": "myapp.synapse_tools.open_tasks_for", "read_only": True},
]
# in myapp/synapse_tools.py
import frappe

def open_tasks_for(project: str) -> dict:
    """Return the open tasks on a project.

    The description and arguments the model sees come from this docstring and
    the function signature.
    """
    rows = frappe.get_list(
        "Task",
        filters={"project": project, "status": ["!=", "Completed"]},
        fields=["name", "subject", "status"],
    )
    return {"project": project, "open_tasks": rows}

An app that prefers to keep the flags next to the function can instead mark it with @synapse.tool(read_only=True) and list the module path as a plain string in the same hook (synapse_tools = ["myapp.synapse_tools"]). Both forms may be mixed.

A custom tool runs the same way the built-in tools do. It runs as the signed in user, with Frappe permissions on, and every call is written to the Synapse Log. The app author is responsible for what the function does, so it should read and write through the normal Frappe document API and never with permissions off.

A registered tool is not reachable on its own. Two more things must be true:

  1. Enable Custom Tools is ticked in Synapse Settings.

  2. A Synapse Profile the caller holds lists the tool by its exact name, in the Custom Tools table.

This is the same explicit grant model as the rest of Synapse. Full Access does not include custom tools, because a custom tool can run any code its app wrote, so each one is granted by name. A tool whose name clashes with a built-in is refused at load, so an app can never replace a core tool. The readiness report lists every registered tool and whether a profile grants it:

bench --site <your-site> execute synapse.mcp_tools.check.report

Model provider

Synapse Settings has a Model Provider choice. Claude is the only provider that is wired up. The other options are placeholders. The setting records which model family the site presents Synapse for and does not change how tools run.

Setting it up

1. OAuth. Frappe 16 can publish OAuth server metadata and support dynamic client registration. This is what lets an MCP client connect without someone creating an OAuth Client record by hand. It is off by default. In OAuth Settings turn on Show Auth Server Metadata, Show Protected Resource Metadata and Enable Dynamic Client Registration. Synapse does not change these settings. They affect the whole site's OAuth behaviour.

A Frappe OAuth token is not limited to MCP. It authorises the whole /api surface as that user, so scope the user accordingly.

2. Create a Synapse Profile. Add the roles the agent's user holds, then the DocTypes and actions those roles may use. Reading needs only a read tick.

3. Fill in Synapse Settings. Tick Enable Synapse Endpoint and Enable Read Tools. Reads work at this point. For writes, also tick Enable Write Tools. If that switch is off, the endpoint stays read only whatever a profile grants.

Connecting a client

claude mcp add --transport http mysite https://<your-site>/api/method/synapse.mcp.handle_mcp

Then sign in. A browser opens on the site login page. Any MCP client that speaks Streamable HTTP with OAuth works the same way. In Claude Desktop it is Settings, Connectors, Add custom connector, with the same URL.

Raw SQL

run_sql_query does not use Frappe's permission system. A user in a profile with Allow SQL can read every table on the site, whatever their DocType permissions are. Grant it only to users who already have full database access.

It is off until Enable Read-Only SQL Tool is ticked in Synapse Settings and the user holds a profile with Allow SQL. It does not use the profile's DocType grants, because it never names a DocType. Prefer get_list and get_doc. Use SQL only for a join or an aggregate they cannot express. If an agent keeps reaching for SQL, the document tools are probably missing something it needs.

Two layers protect it:

  1. A read only database user, enforced by MariaDB. A query that gets past the text filter still cannot write.

  2. mcp_tools/guard.py. It checks the statement type, blocks comments, blocks more than one statement, blocks a list of keywords and tables, and caps the length. This is text matching, so treat it as a backup, not the main line of defence.

Set up the database user per site. As MariaDB root:

CREATE USER 'mcp_ro'@'localhost' IDENTIFIED BY '<STRONG_PASSWORD>';
GRANT SELECT ON `<DB_NAME>`.* TO 'mcp_ro'@'localhost';
REVOKE FILE ON *.* FROM 'mcp_ro'@'localhost';
FLUSH PRIVILEGES;

Then in site_config.json (never in the repo):

{
  "mcp_ro_db_user": "mcp_ro",
  "mcp_ro_db_password": "<STRONG_PASSWORD>"
}

Without those keys, the tool falls back to the site's normal read write connection and rolls back after every query. It works, but then the text guard is the only boundary. On hosted platforms where a second database user is not possible, that fallback is the only option. Decide before you enable SQL there.

Add more blocked tables per site with mcp_sql_blocked_tables in site_config.json. MariaDB only. connection.py raises NotImplementedError on other backends.

Audit

Every call writes a Synapse Log row: success, refusal or error. Each row records the tool, the user, how they signed in, the IP address, the document touched, the row counts and the timing. Writes also record the values sent and the before and after of each changed field. Calls that are refused before the tool runs are logged too.

If a tool looks blocked and there is no log row for it, the block is in the client, usually its own tool permission prompt. Check that first.

Log rows are written with their own commit after any rollback, so a failed or refused write still leaves a record. System Manager can read and report on the log but cannot create or edit rows from the desk. A daily job drops rows past the retention window. Untick Log Field Values if the data itself must not be copied into the log. Password fields are masked either way.

Tests

bench --site <your-site> run-tests --app synapse

The access model, the SQL guard, the tool schemas and the value conversion do not import anything from frappe, so they also run without a site:

python -m unittest discover -s apps/synapse -p 'test_mcp_*.py'

Licence

GNU Affero General Public License v3.0 or later. See LICENSE.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables LLMs to interact with any ERPNext instance through comprehensive CRUD operations, advanced permissions, and a web chat interface.
    1
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    MCP server that enables LLMs to interact with ERPNext/Frappe sites for document CRUD, search, reports, workflows, and analytics, respecting user permissions and logging all actions.
    295
    AGPL 3.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI models to securely interact with Frappe Framework/ERPNext instances, supporting document CRUD, RPC methods, file management, workflows, reporting, and more via the Model Context Protocol.
    143
    ISC
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI assistants to interact with ERPNext data and functionality through the Model Context Protocol, including document CRUD, report running, and API method calls.
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/erpuae/synapse'

If you have feedback or need assistance with the MCP directory API, please join our Discord server