appsmith-mcp
# appsmith-mcp
MCP server for a self-hosted Appsmith instance, over Appsmith's own REST API — the
same API the editor uses.
Built because no reliable Appsmith MCP server exists: there is nothing published on
npm or PyPI, nothing in the `appsmithorg` GitHub org, and the listings that surface
in search results are auto-generated stubs with no implementation behind them.
## How it authenticates
Appsmith uses Spring Security form login behind CSRF protection:
1. A `GET /api/v1/users/me` seeds an `XSRF-TOKEN` cookie.
2. `POST /api/v1/login` sends `username`/`password` form-encoded, echoing that token
in the `X-XSRF-TOKEN` header.
3. The response sets a `SESSION` cookie used for every later call.
The client logs in lazily on first use and re-logs in once automatically on a `401`,
so an expired session heals itself instead of failing a tool call.
## Setup
```sh
npm install
cp .env.example .env # then put your Appsmith password in .env
npm run check # read-only checks against the live instance
npm run check -- --scratch # also covers writes, via a throwaway app it deletes
```
`npm run check` prints a PASS/FAIL row per endpoint — run it whenever an Appsmith
upgrade might have moved routes. `--scratch` creates an `mcp-smoke-*` application,
exercises create-page / publish / delete against it, and removes it again.
## Registration
Registered at **user scope**, so it is available in every project rather than one:
```sh
claude mcp add appsmith --scope user -- \
node --env-file-if-exists=<abs-path>/.env <abs-path>/src/index.js
```
That writes to `~/.claude.json`. Paths must be absolute — a user-scope server is
launched from whatever directory the session happens to be in.
Credentials are read from `.env` via Node's `--env-file-if-exists`, so no secret
lives in the MCP config. Because `.env` pins one instance, retarget it by changing
`APPSMITH_URL`.
To scope it back to a single project, `claude mcp remove appsmith --scope user` and
put the same command in that project's `.mcp.json`.
## Tools
**Read**
| Tool | Endpoint |
| --- | --- |
| `appsmith_whoami` | `GET /api/v1/users/me` |
| `appsmith_list_workspaces` | `GET /api/v1/workspaces/home` |
| `appsmith_list_applications` | `GET /api/v1/applications/home` |
| `appsmith_list_pages` | `GET /api/v1/pages/application/{id}` |
| `appsmith_get_page` | `GET /api/v1/pages/{id}` |
| `appsmith_list_queries` | `GET /api/v1/actions` |
| `appsmith_list_js_objects` | `GET /api/v1/collections/actions` |
| `appsmith_list_datasources` | `GET /api/v1/datasources` |
| `appsmith_get_datasource_structure` | `GET /api/v1/datasources/{id}/structure` |
| `appsmith_list_plugins` | `GET /api/v1/plugins` |
| `appsmith_search_entities` | `GET /api/v1/search-entities` |
| `appsmith_export_application` | `GET /api/v1/applications/export/{id}` |
**Edit** — changing an existing app's logic
| Tool | Endpoint |
| --- | --- |
| `appsmith_update_query` | `PUT /api/v1/actions/{id}` |
| `appsmith_create_query` | `POST /api/v1/actions` |
| `appsmith_update_js_object` | `PATCH /api/v1/collections/actions/{id}` + `PUT .../body` |
| `appsmith_create_js_object` | `POST /api/v1/collections/actions` |
| `appsmith_delete_query` | `DELETE /api/v1/actions/{id}` — gated |
| `appsmith_delete_js_object` | `DELETE /api/v1/collections/actions/{id}` — gated |
| `appsmith_get_widget` | `GET /api/v1/pages/{id}` (one widget) |
| `appsmith_update_widget` | `PUT /api/v1/layouts/{layoutId}/pages/{pageId}` |
| `appsmith_clone_widget` | `PUT /api/v1/layouts/{layoutId}/pages/{pageId}` |
| `appsmith_generate_crud_page` | `POST /api/v1/pages/crud-page` |
**Write** — structure and lifecycle
| Tool | Endpoint |
| --- | --- |
| `appsmith_create_workspace` | `POST /api/v1/workspaces` |
| `appsmith_create_application` | `POST /api/v1/applications` |
| `appsmith_create_page` | `POST /api/v1/pages` |
| `appsmith_execute_query` | `POST /api/v1/actions/execute` |
| `appsmith_publish_application` | `POST /api/v1/applications/publish/{id}` |
| `appsmith_clone_application` | `POST /api/v1/applications/clone/{id}` |
| `appsmith_delete_application` | `DELETE /api/v1/applications/{id}` — gated |
| `appsmith_api_request` | any route; non-GET gated |
Editing is deliberately typed rather than left to `appsmith_api_request`: the raw tool
is gated behind `ALLOW_DESTRUCTIVE` and cannot be safely auto-approved, since it can
reach any endpoint. The typed tools each do one thing, so they can be allow-listed
individually.
`appsmith_update_query` sends only the fields you pass — the server merges the rest,
so the datasource and untouched settings survive. It deliberately does not rename:
renaming needs a refactor pass that rewrites bindings across the app, and a plain
`PUT` would leave every reference broken.
### Adding widgets
There is deliberately no "create widget from scratch" tool. Appsmith's widget defaults
live in its **frontend**, not its API — `/api/v1/widgets`, `/widget-config` and
`/configs` all 404 — and a real widget carries 24–65 properties depending on type. A
hand-written defaults catalogue would replicate frontend logic, drift on every Appsmith
upgrade, and fail silently when it did. The two supported routes instead:
- `appsmith_clone_widget` copies a widget that Appsmith itself created, so the defaults
are always correct and there is nothing to maintain. Every descendant gets a fresh
`widgetId` and a free name (`Canvas1` → `Canvas2`, `interno` → `interno1`); names only
need to be unique per page. The copy is placed below its siblings so it cannot land on
another widget. **Bindings inside the copy are not rewritten** — one that referenced
the source by name still points at the original, so the response returns
`renamedInsideCopy` for the caller to fix.
- `appsmith_generate_crud_page` calls Appsmith's own generator for a whole screen.
### Bindings
`appsmith_update_widget` maintains `dynamicBindingPathList` for you. Appsmith only
evaluates `{{ }}` on properties listed there and the server does **not** infer it: a
binding written without registration is stored verbatim and rendered as a literal
string — which, for a boolean like `isDisabled`, is always truthy. Setting a binding
adds the path; setting a literal removes it. A widget's other properties are preserved,
since the whole layout is read, patched and written back.
## Safety
- `appsmith_delete_application` and non-GET `appsmith_api_request` calls refuse to run
unless `APPSMITH_MCP_ALLOW_DESTRUCTIVE=true`.
- `appsmith_execute_query` runs against the **real datasource** — a write query writes.
It is not gated, since running queries is the point, so check what a query does
before running it.
- Large payloads (page DSLs, exports) are truncated at `APPSMITH_MCP_MAX_CHARS`.
`appsmith_get_page` returns a widget outline by default; pass `full=true` for the
raw layout. `appsmith_export_application` takes an `outputPath` to write to disk
instead of returning inline.
## Instance quirks found while building this
Two behaviours differ from what the CE controllers suggest, both handled in the code:
- `GET /applications/home` documents `workspaceId` as optional, but this instance
rejects the call without it. `appsmith_list_applications` fans out across every
workspace when no id is given.
- `POST /pages` rejects an empty `layouts` array, and `layouts: [{}]` silently creates
a page whose DSL is `null` — blank and unusable in the editor. The root
`CANVAS_WIDGET` canvas must be sent explicitly; see `src/defaults.js`.
## Maintenance
Routes were taken from the Appsmith backend controllers, not guessed:
- `controllers/ce/ApplicationControllerCE.java`
- `controllers/ce/PageControllerCE.java`
- `controllers/ce/ActionControllerCE.java`
- `controllers/ce/DatasourceControllerCE.java`
- `controllers/ce/WorkspaceControllerCE.java`
- `constants/ce/UrlCE.java`
Appsmith's API is not a documented public contract, so a major upgrade can move a
route. `npm run check` is the fast way to find out.
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 30 tools
Most tools target clearly distinct resources and actions (workspaces, applications, pages, queries, JS objects, widgets, datasources). A few overlaps exist: appsmith_list_applications vs appsmith_search_entities both find apps, and get_widget/update_widget/clone_widget vs get_page are adjacent, but descriptions clarify each tool's distinct purpose well.
Tools largely follow the appsmith_verb_noun pattern consistently (list_, create_, update_, delete_, get_, clone_, execute_, export_). Minor deviations like appsmith_whoami (noun-style) and appsmith_api_request (generic verb) break the pattern slightly, but the overall convention is coherent and predictable.
At 30 tools, this is on the heavy side for an MCP server, exceeding the typical 3-15 well-scoped range. However, Appsmith is a broad platform covering workspaces, apps, pages, queries, JS objects, widgets, and datasources, so each tool reasonably earns its place across the resource categories.
The surface covers CRUD across most resource types: workspaces (create/list), applications (create/list/clone/delete/export/publish), pages (create/get/list), queries (create/update/delete/execute), JS objects (create/update/delete), widgets (get/update/clone). Minor gaps include no delete_page or delete_datasource, and the generic appsmith_api_request fallback covers edge cases.