# Flexlists MCP server — guide for AI agents

Flexlists is a no-code database: **lists** (tables) with **rows**, **fields** (columns) and **views**
(Table, Kanban, Calendar, Gallery, Timeline, Gantt, Map, Chart, Mind map, Checklist).
This MCP server lets you read and change a user's lists with the rights of their API key or OAuth grant.

- Endpoint: `https://test.flexlists.com/mcp` (Streamable HTTP, MCP 2025-06-18; stateless, JSON responses)
- Auth: `Authorization: Bearer <API key>` (Integrations → API keys / AI agents (MCP)), or OAuth 2.1
  (discovery: `https://test.flexlists.com/.well-known/oauth-protected-resource/mcp`)
- Human docs: https://test.flexlists.com/docs/mcp

## How to work

1. Call `whoami`: who the key belongs to, its scope (whole account, one workspace, specific lists,
   or an API access configuration) and whether it is read-only.
2. `list_lists` → pick a `listId`. Every list tool takes this number.
3. `get_list_schema` before writing: rows are keyed by field **name**; Choice fields only take their options.
4. Read with `search_rows` (filter, full-text, sort, cursor) or `get_view_rows` (a view's own filters and sort).
5. Write with `create_rows` / `update_rows` (max 100 rows per call).
6. Destructive tools (`delete_rows`, `delete_list`, `delete_field`, `unshare`, `delete_group`,
   `remove_group_member`, `delete_webhook`) need `"confirm": true`. First tell the user exactly what
   goes, and only confirm after they agreed.

## Reuse first, code last

Before creating a list, view, checklist or anything else, call `find_existing` with the request in a few words. It returns
the user's existing lists / views / forms / apps / widgets / automations (what this key may see), the built-in view types and
widget blocks, gallery and list / view templates and the integration catalogue. Check in this order:

- a. the user's EXISTING lists, views, forms, apps, widgets and automations — reuse or extend them (add a field, a view, a step) instead of making a duplicate;
- b. BUILT-IN view types and widgets: the view types below (create_view on an existing list), Publish → Widget of a view, and inline widgets (create_widget with table / chart / kpi / form / map blocks);
- c. GALLERY templates and solutions, and the list / view templates (create_from_template);
- d. WORKFLOWS / automations (Flow Studio on GluedEasy) and the INTEGRATION catalogue (~700 pieces: Gmail, Slack, Stripe, …) for anything that talks to another service or runs by itself (create_automation, naming the integration);
- e. only then CUSTOM code — a custom view template, a code step, an AI-built page or application — and say in one line why a, b, c and d did not fit.

When there are two or more reasonable ways, or the request is ambiguous, offer the user 2–4 options (one line each: what they get,
effort, trade-off; mark one recommended) instead of guessing. One clear best way: just do it.

## Field types

Give every field the most specific type: Text, LongText, Markdown, HTML, Link, Email, Phone, Barcode, Integer, Decimal, Money, Percentage, Rating, Duration, Autonumber, Formula, Boolean, Choice, MultiChoice, Date, Time, DateTime, CreatedTime, ModifiedTime, CreatedBy, ModifiedBy, Image, File, Color, User, Lookup, LinkedRecord, Rollup, Count, LinkedLookup, Button. E-mail → Email, phone → Phone, website → Link,
notes → LongText, yes/no → Boolean, dates → Date, prices → Money, counts → Integer, a status with fixed values → Choice with options,
ratings → Rating, durations → Duration (seconds), barcodes → Barcode. Autonumber, CreatedTime, ModifiedTime, CreatedBy, ModifiedBy and Button are filled in by Flexlists: read only.
Types are matched case-insensitively with common synonyms (checkbox, currency, url, long text, …); an unknown type is an error, never
a silent Text. When the user names a type, use exactly that one. Fix the type of an existing field with update_field `type`.

Every type, what it is for and how to set it:

- Text — short text: names, titles, handles, codes
- LongText — notes, descriptions, bios, comments (several lines)
- Markdown — formatted text (markdown)
- HTML — HTML text
- Link — a web address (URL): website, profile link, source post
- Email — an e-mail address (checked, opens a mail)
- Phone — a phone / mobile / WhatsApp number (checked, opens a call)
- Barcode — a barcode / QR code: EAN, SKU code, ticket code (shown as a barcode). Set: "format" = "code128" (default, a barcode) or "qr" (a QR code); the value is the code
- Integer — whole numbers: counts, quantities, stock, scores
- Decimal — numbers with decimals: measurements, weights
- Money — amounts of money: price, cost, revenue, budget. Set: "currency" = the ISO code (EUR, USD, GBP, …; default EUR); values "$12.50", "€ 1.234,50", 12.5
- Percentage — percentages: discount, progress, rate. Set: values "15%" or 15 (= 15 %, never 0.15)
- Rating — a rating of 1 to N stars ("max", default 5): rating, stars, review score. Set: "max" = the number of stars (1–10, default 5); values 4, "4 stars", "★★★★", "4/5"
- Duration — a length of time: duration, time spent, run time (shown as 1:20 / "1h 20m"). Set: "format" = "h:mm" (default), "h:mm:ss" or "text"; values "2h 30m", "1:20", "90 minutes", "1.5 hours" (a bare number in text = minutes, a JSON number = seconds)
- Autonumber — a number Flexlists gives each new row: ticket number, invoice number, order no. [read only: Flexlists fills it, never send values]. Set: config: start (First number 0–1000000000, default 1)
- Formula — computed from the row's own fields [read only: Flexlists fills it, never send values]. Set: "formula" = the expression, e.g. "{Price} * {Quantity}" (or add_formula_field from a description)
- Boolean — yes / no, a checkbox: done, paid, contacted, answered, in stock. Set: values yes / no, true / false, ja / nee, sì / no, done
- Choice — ONE of a fixed set: status, stage, category, priority, type. Set: "options" = the fixed values, e.g. ["New","Contacted","Won"]
- MultiChoice — SEVERAL of a fixed set: tags, labels, skills. Set: "options" = the fixed values; a row value is a list ["a","b"] or "a, b"
- Date — a day: date added, due date, birthday, launch date. Set: values 2026-11-03, 3-11-2026, "today", "tomorrow", "next friday", "in 3 days"
- Time — a time of day. Set: values "14:30", "2:30 pm", "9am"
- DateTime — a moment: a date with a time, a timestamp. Set: values "2026-11-03 14:30", "tomorrow 3pm", "now"
- CreatedTime — when the row was made, filled in by Flexlists: created, date added (automatic) [read only: Flexlists fills it, never send values]. Set: config: dateOnly (Date only, default false)
- ModifiedTime — when the row was last changed, filled in by Flexlists [read only: Flexlists fills it, never send values]. Set: config: dateOnly (Date only, default false)
- CreatedBy — who made the row, filled in by Flexlists [read only: Flexlists fills it, never send values]
- ModifiedBy — who last changed the row, filled in by Flexlists [read only: Flexlists fills it, never send values]
- Image — pictures, photos, logos. Set: files: attach_file_to_row
- File — documents, attachments. Set: files: attach_file_to_row
- Color — a colour. Set: values "#ff8800" or a colour name
- User — a person of the workspace: assignee, owner, responsible. Set: "multiple": true for several people
- Lookup — ONE row of another list (a Link to another list that links one row). Set: "list" = the other list (its name), optional "show_field"
- LinkedRecord — rows of ANOTHER list, two-way ("Link to another list"). Set: "list" = the other list (its name), optional "show_field", "multiple", "inverse_multiple" (or add_link_field)
- Rollup — a total over linked rows: sum / average / min / max of a field of the rows a Link links [read only: Flexlists fills it, never send values]. Set: "link" = the Link field, "target_field" = the field of the linked list, "fn" = SUM / AVG / MIN / MAX / COUNT…, optional "filter" (or add_rollup_field)
- Count — the number of linked rows [read only: Flexlists fills it, never send values]. Set: "link" = the Link field, optional "filter" (or add_count_field); a plain number typed in is Integer
- LinkedLookup — the values of a field of the linked rows [read only: Flexlists fills it, never send values]. Set: "link" = the Link field, "target_field" = the field of the linked list (or add_lookup_through_link_field)
- Button — a button on each row that opens a web address ("url", {Field} = the row's value) or runs an automation [no value: people press it]. Set: "url" = the address it opens ({Field} = the row's value, e.g. "https://maps.google.com/?q={Address}") or "automationId" (runs that automation); "label" = the text on it

## Values

| Field type | Read | Write |
|---|---|---|
| Text, LongText, Markdown | string | string |
| Email, Phone | string | an e-mail address / a phone number (checked; anything else is refused) |
| Link (web address) | { url, name } | a URL |
| Integer, Decimal, Money, Percent | number | number |
| Boolean | true / false | true / false |
| Date | "2024-05-01" | "2024-05-01" |
| DateTime | ISO 8601 | ISO 8601 or epoch ms |
| Choice | label | label (case-insensitive) |
| MultiChoice | [labels] | [labels] |
| User | { id, name } | email or user id |
| LinkedRecord (Link to another list) | [{ id, label }] | row ids, { "id": 12 } or labels ("Acme"), one or an array (replaces the links; link_rows adds / removes) |
| Lookup (relation) | { id, label } | related row id, { "id": 12 } or its label ("Acme") |
| Rollup, Count, LinkedLookup | number / true|false / text / [values], by resultType | read only (computed from the linked rows) |
| Formula | number / true|false / "YYYY-MM-DD" / ISO / text / [values], by resultType | read only (computed from the row); add one with `add_formula_field` |
| Sublist | [{ id, label }] | [row ids] |
| File, Image | [{ id, name, url, size }] (url valid 1 h) | attach_file_to_row; or the files as read (by id) to keep / reorder / remove them |

`null` clears a field. Unknown field names are refused with the list of valid names. Rollup / Count / LinkedLookup fields are read only: a value sent for one is refused naming the field (get_list_schema shows `readOnly: true` and what they read in `computed`).

## Filters (`where`)

The syntax of `POST /api/v1/lists/{listId}/rows/search`: an object of conditions that must all hold.

```json
{ "Status": "Open", "Due": { "op": "lt", "value": "2024-06-01" }, "Name": { "op": "contains", "value": "acme" } }
```

ops: `eq`, `neq`, `contains`, `gt`, `gte`, `lt`, `lte`, `empty`, `notEmpty`. Keys may also be `id`,
`createdAt`, `updatedAt`. `query` adds a full-text "contains" over the text fields.

## List locks: read-modify-write across lists

When a change reads values and writes new ones based on them across lists (move stock from Warehouse to
Orders, post to a ledger and update a balance, book a slot), lock the lists first so nobody else writes in
between:

1. `lock_lists` `{ "lists": [41, 42], "ttlSeconds": 30, "label": "Move stock" }` → `token`. Status
   `queued`: call `get_lock` `{ "token": "…", "waitSeconds": 25 }` until it is `held`.
2. Read, then write with `"lockToken": "<token>"` on every write tool (`create_rows`, `update_rows`,
   `delete_rows`, `link_rows`, field tools, …).
3. **Always** `release_lock` when done — also after an error. Keep the work between lock and release short;
   use `extend_lock` only if it really takes longer than ttlSeconds.

It is write exclusion, not a transaction: reads are never blocked, your writes count at once and are not
rolled back if you stop half-way. A write to a list someone else locked fails with `list_locked` (who holds
it and `retryAfterSeconds`): wait that long and try again, or tell the user. Lock all the lists you need in
ONE `lock_lists` (1–20 lists); never wait for another lock while you hold one.

## Formulas

`add_formula_field` with a `description` lets Flexlists write and check the formula (never saved when
it does not work); with an `expression` you write it yourself in the Airtable formula language. The
language, every function and examples: https://test.flexlists.com/docs/llm/formulas.md. `explain_formula` explains one.
Formula calls that use the AI: 30 per user per hour.

## Paging and size

- `search_rows` / `get_view_rows`: max 200 rows per call; when `nextCursor` is not null, call again
  with `cursor` (and the same sort / filter).
- Every result stays under 1 MB. A bigger page comes back shorter with a `note`; continue with the cursor
  or ask for fewer `fields`.
- `aggregate_rows` (count / sum / avg / min / max, grouped by a field; dates by day…year) and
  `count_rows` answer "how many / how much" without reading rows.

## Files

- `attach_file_to_row`: from a public http(s) URL (max 25 MB, no private / internal addresses) or base64 (max 5 MB).
- `get_file`: short-lived download links (1 hour), never the bytes.
- `read_spreadsheet`, `import_spreadsheet_rows`, `create_list_from_spreadsheet`: CSV / XLSX by URL or fileId.

## Permissions

- A key never sees lists outside its scope. A read-only key gets a tool error on write tools.
- Writes need the same role as in the app (e.g. AddOnly may only add rows; schema changes need full access).
- API access configuration keys only reach their own lists, with their own permission per list and endpoint allowlist.
- Rate limit: 600 requests per minute per key (HTTP 429 + Retry-After).

## Tools

| Tool | Kind | What it does |
|---|---|---|
| `whoami` | read | The Flexlists user this key belongs to, their plan (account: plan, status, canWrite, limits), the workspaces it can see, and what the key may do: its scope (user = the whole account, workspace = one workspace, tables = specific lists, config = an API access configuration), read-only or read & write, and when it expires. |
| `list_workspaces` | read | The workspaces this key can see (id, name, whether the user owns it). |
| `find_existing` | read | Call before creating anything: what already exists or is built in for a request — the lists and views this key can see, the user's forms, apps, widgets and automations (keys with the full account / workspace scope), the built-in view types and widget blocks, list / view templates, gallery templates and the integration catalogue (Slack, Gmail, Stripe, …). |
| `list_lists` | read | The lists (tables) this key can read: listId, name, workspace and folder. |
| `get_list_schema` | read | The fields (columns) of a list: name, type, required, read-only, the options of Choice fields, for relation fields (LinkedRecord / Lookup / Sublist) the related list, its label field and the inverse field, for Rollup / Count / LinkedLookup (computed through a link, read only) what they read (computed) and their resultType, and for Formula fields their formula { expression, orderBy, direction, partitionBy }. |
| `create_list` | write | Create a new list with fields and optional first rows. |
| `rename_list` | write | Rename a list and/or change its description. |
| `delete_list` | write, destructive (confirm) | Delete a list with all its rows and views. |
| `list_folders` | read | The folders of My Lists and My Views (one tree, paths like "Sales/Q4") with the lists and views in each. |
| `create_folder` | write | Make a folder for lists and views, optionally inside another of your folders. |
| `move_to_folder` | write | Move lists (listIds) and/or views (viewIds) into one of your folders, or to the root (folder "" or "root"). |
| `list_templates` | read | Templates to start from. |
| `create_from_template` | write | Make a list (or a list + view) from a template of list_templates, with its demo rows unless demo_data is false. |
| `add_field` | write | Add a field (column) to a list. |
| `update_field` | write | Change a field: rename it, make it required or not, change its description, set the options of a Choice field (the full new list; existing options keep their colour; options rows still use are only dropped with "force": true), change a Formula (expression, orderBy / direction / partitionBy), or change its TYPE through the app's conversions (e.g. |
| `delete_field` | write, destructive (confirm) | Delete a field and its values in every row. |
| `add_rollup_field` | write | Add a computed field that sums / averages / counts / joins a field of the rows linked through a Link field (stored, kept up to date, read only). |
| `add_count_field` | write | Add a computed field with the number of rows linked through a Link field (read only), optionally only those matching a filter on fields of the LINKED list. |
| `add_lookup_through_link_field` | write | Add a computed field showing the values of a field of the rows linked through a Link field, as a list (read only), e.g. |
| `add_formula_field` | write | Add a Formula field: a value computed from the other fields of the same row (Airtable formula language; typed, read only, kept up to date). |
| `explain_formula` | read | Explain in plain words what a Formula field of a list (or a formula expression in the context of the list's fields) computes. |
| `search_rows` | read | Read rows of a list: filter (where), full-text search (query), sort, only some fields, paged with a cursor (max 200 rows per call). |
| `get_row` | read | One row by id, keyed by field name (files as short-lived download links, relations as { id, label }). |
| `create_rows` | write | Add 1 to 100 rows. |
| `update_rows` | write | Change 1 to 100 rows: each { "id": 17, "fields": { "Field name": value } } — only the given fields change. |
| `link_rows` | write | Link one row to rows of the other list through a Link (LinkedRecord) field: by row ids (target_row_ids) or by labels (target_labels, the other rows' shown values, matched ignoring case; labels that match nothing are returned in unmatched, never guessed). |
| `delete_rows` | write, destructive (confirm) | Delete 1 to 100 rows by id. |
| `count_rows` | read | How many rows match a filter (where / query); no filter = all rows. |
| `aggregate_rows` | read | Totals of a list, optionally grouped by a field (like a chart): count, sum, avg, min, max of number fields. |
| `list_views` | read | The views of a list (Table, Kanban, Calendar, Gallery, Timeline, Gantt, Map, Chart, Mind map, Checklist, …): viewId, name, type. |
| `list_view_types` | read | The view types Flexlists can make, what each is good for, and the fields ("slots") each needs with the field types they accept — use it before create_view. |
| `create_view` | write | Add a view to an existing list: KanBan, Calendar, TimeLine, Gantt, Map, Gallery, Chart, MindMap, Storyboard, StickyNotes, Spreadsheet or List (table). |
| `get_view_rows` | read | The rows of a view as the view shows them: its own filters and sort, its visible fields only. |
| `create_checklist` | write | Build a CHECKLIST (an inspection, opening routine, onboarding, audit…): three linked lists (items, runs, results) and the Checklist view. |
| `list_comments` | read | The comments (row chat) on a row, oldest first: author, text, time. |
| `add_comment` | write | Add a comment to a row (its row chat), as the key's user. |
| `get_row_history` | read | When a row was created and last changed, and its recent edits (who, when, which fields from → to) from the change log, which keeps edits for 7 days. |
| `list_snapshots` | read | The saved versions of a whole list (List history: hourly when it changed, and before every bulk action or restore), newest first: id, takenAt, reason, rows. |
| `attach_file_to_row` | write | Store a file in a File or Image field of a row (added to the files it has): from a URL (Flexlists downloads it, max 25 MB, public http/https addresses only) or as base64 content (max 5 MB, give fileName). |
| `get_file` | read | The files in a File / Image field of a row: name, size, fileId and a download link that works for 1 hour without login (the bytes are never sent inline). |
| `read_spreadsheet` | read | Look into a CSV / XLSX file (by URL or fileId): sheet names, column headers with guessed types, row count and the first rows. |
| `create_list_from_spreadsheet` | write | Make a new list from a CSV / XLSX file (by URL or fileId) and import ALL its rows on the server. |
| `import_spreadsheet_rows` | write | Append the rows of a CSV / XLSX file (by URL or fileId) to an existing list. |
| `lock_lists` | write | Get an exclusive WRITE lock on 1 to 20 lists for a read-modify-write across them (stock moves, ledgers, bookings): while you hold it nobody else can change those lists; you pass its token as lockToken to your write tools. |
| `get_lock` | read | The status of a lock (queued \| held \| released \| expired \| cancelled), its position in the queue and expiresAt. |
| `extend_lock` | write | Keep holding a lock: expiresAt = now + ttlSeconds (at most one hour after it was granted). |
| `release_lock` | write | End a lock you hold (or leave the queue): the lists are free for others again. |
| `get_sharing` | read | Who has access to a list: people (name, email, role, pending invites) and groups (name, role). |
| `share_list` | write | Give people access to a list by email. |
| `share_list_with_group` | write | Give one of your groups access to a list (everyone in it, also people added later). |
| `set_share_role` | write | Change the role of a person (email) or a group on a list. |
| `unshare` | write, destructive (confirm) | Take away a person's (email; also cancels a pending invite) or a group's access to a list. |
| `list_groups` | read | Your groups (named sets of people) with their members, pending invites and the lists each group can open. |
| `create_group` | write | Create a group and optionally add members by email. |
| `update_group` | write | Rename a group or change its description. |
| `delete_group` | write, destructive (confirm) | Delete one of your groups: its members lose the access they had through it. |
| `add_group_members` | write | Add people to a group by email (unknown addresses get an invitation). |
| `remove_group_member` | write, destructive (confirm) | Remove a person (email) from a group, or cancel their invitation. |
| `list_forms` | read | Your public forms: id, name, on/off, public link and number of submissions. |
| `get_form` | read | One form: its public URL, steps and questions, which lists it writes to (with what visitors may do there), settings and submission count. |
| `create_form` | write | Build a public FORM with AI (planned and built like the form builder does): a shareable page where visitors fill in answers that land in your lists (missing lists are created). |
| `list_form_submissions` | read | The latest submissions of one of your forms (newest first): time, answers, the rows they created, and errors. |
| `set_form_enabled` | write | Turn one of your forms on (visitors can submit) or off. |
| `list_automations` | read | Your integrations and workflows (automations): id, name, kind, on/off, trigger, last run. |
| `create_automation` | write | Start a new integration (connect a list to another service: email, Slack, a CRM, …) or workflow (automate your own data on a schedule or when rows change). |
| `set_automation_enabled` | write | Switch one of your automations on or off (switching on fails while secrets are missing). |
| `run_automation` | write | Run one of your automations now (a test run with a sample row for table-triggered ones). |
| `list_applications` | read | The applications (multi-page apps on your lists) you own or maintain, with their links, and their solutions. |
| `create_application` | write | Start a new multi-page application (a portal, a customer or employee app, …) on your lists. |
| `list_webhooks` | read | Your row-change webhooks on lists in this key's scope: id, list, events, target URL, on/off. |
| `create_webhook` | changes settings | Send row changes of a list to a URL (HTTPS POST, signed with the returned secret, shown only now). |
| `get_webhook` | read | One of your webhooks (by id from list_webhooks) with its last 20 deliveries: status, receiver status code, error, time. |
| `test_webhook` | changes settings | Send a signed `ping` to one of your webhooks now and get the status code its receiver answered (recorded in its deliveries, never retried). |
| `update_webhook` | changes settings | Change one of your webhooks: events (replaces them; row.created, row.updated, row.deleted, row.archived, row.unarchived, rows.imported, list.restored), url (checked again like a new one: https, public addresses only) and / or enabled (false switches it off and drops deliveries still waiting; true on again). |
| `delete_webhook` | changes settings, destructive (confirm) | Remove one of your webhooks (by id from list_webhooks). |
| `send_email` | write | Send an email through the workspace's WorkflowEmail integration (Integrations → Email, enabled by an owner or admin). |
| `explain_item` | read | Explain one of your automations, forms, applications or view templates: what it does, how to use it, which secrets it needs and whether they are set, how to change it. |
| `contact_support` | changes settings | File a ticket with the Flexlists support team (humans), who reply by email. |

## Resources and prompts

- Resources: `flexlists://lists` (index), `flexlists://list/{listId}/schema`, `flexlists://docs/llm` (this guide).
- Prompts: `analyze_list`, `build_tracker`, `import_data`.
