Skip to main content
Glama
README.md
# 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

B3.4/5.0

Scored across 30 tools

Disambiguation4/5

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.

Naming Consistency4/5

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.

Tool Count3/5

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.

Completeness4/5

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.

Maintenance

ActivitySlowing
ResponsivenessNo issues