Reference
The tables on this page are generated from the code by pnpm docs, and CI fails
if they drift. If they say something, that is what the build does. The
activity log format below is written by hand, from apps/connector/src/activity.ts.
Environment variables
Only FM_USER and FM_PASS are required.
| Variable | What it is |
|---|---|
FM_USER, FM_PASS |
REQUIRED. The FileMaker account the connector logs in as. Needs the fmodata extended privilege to read tables, plus fmrest to expose scripts. Without them discovery returns 401. Credentials are read from the environment only. They are never written to the config file, so they stay out of backups and out of the hosted tier. |
FM_USER_<SLUG>, FM_PASS_<SLUG> |
Several FileMaker files on one connector? Each is a "profile" with its own slug, its own /mcp/<slug> endpoint and its own token. Give a profile its own account by suffixing the slug in upper snake case; anything unset falls back to FM_USER / FM_PASS above, so a single-file setup needs nothing here. profile "sales-live" -> FM_USER_SALES_LIVE / FM_PASS_SALES_LIVE The server address is set per profile in the wizard, so files on different servers (fm5.example.com and fm6.example.com) are two profiles, each with its own pair here. Changing .env means restarting the container. |
FM_HOST, FM_DATABASE |
Optional. Prefills the wizard's Connection step. The wizard's own values win, and the published config is what the MCP endpoint actually uses at runtime. Must include the scheme and no trailing slash: it is used as a URL prefix, <FM_HOST>/fmi/data/vLatest/databases/<FM_DATABASE> |
PG_USER, PG_PASSWORD, PG_USER_<SLUG>, PG_PASSWORD_<SLUG> |
Optional. A PostgreSQL database instead of, or beside, a FileMaker file. The account Ailimu logs in as, read from the environment only and never written to the config file. Give it only the rights you want Claude to have: the database's own permissions are the real limit. A connection with its own account uses the slug in upper snake case, and anything unset falls back to PG_USER / PG_PASSWORD. connection "shop" -> PG_USER_SHOP / PG_PASSWORD_SHOP |
PG_HOST, PG_PORT, PG_DATABASE |
Optional. Prefills the wizard's Connection step for a PostgreSQL connection. The wizard's own values win. PG_PORT defaults to 5432. |
ADMIN_TOKEN, MCP_TOKEN |
Optional. ADMIN_TOKEN is the setup code that creates the first owner account, and works as an owner-level bearer token for scripting /admin/api; people sign in to the wizard with their own accounts. MCP_TOKEN guards the bare /mcp endpoint. Leave blank and the connector generates them on first run, prints them, and persists them on the config volume so they survive a restart. Set them here to pin your own values. |
ACTIVITY_RETAIN_DAYS, ACTIVITY_READS_RETAIN_DAYS |
Optional. How many days the activity log keeps entries. Changes, script runs and admin actions default to 365. Claude's reads, recorded only on connections where "Record reads" is on, default to 30: they are nearly all of the volume, roughly 0.6 MB a day for normal use and 6 MB a day for heavy use. |
COMPACT_RESULTS, RESULT_MAX_KB, FIND_DEFAULT_ROWS, TEXT_CLIP_CHARS, COLUMNAR_FROM_ROWS |
Optional. How big a tool result may get. Every token of a result is paid for by the person using Claude, so by default results are lean: no indentation, empty fields left out, long text cut in a search, 25 rows unless Claude asks for more, and at most 40 KB (about 10,000 tokens) in one answer. The tools describe themselves briefly too: how to search is said once per table, not once per field. Setting COMPACT_RESULTS to off goes back to the earlier, larger format for both the answers and the tool descriptions. The other four each change one thing: the size of one answer in KB (5 to 1000), the rows a search returns when no limit is given (1 to 500), the characters kept of a long text in a search (0 keeps it all), and how many rows make a search a table (7 by default: the field names once, then the values, which roughly halves a long listing; 0 never does). Records fetched with get_ always keep their full text. A value that is not a number is ignored. |
PORT |
Optional. Defaults to 8081. |
PUBLIC_URL |
Optional. The address people open Ailimu at, e.g. http://192.168.1.20:8081 or https://ailimu.example.com. Only used to print a correct wizard link in the start-up log; inside Docker the connector cannot work it out itself. |
UPDATE_CHECK |
Optional. Once a day Ailimu asks Docker Hub which versions of its image exist, so the wizard can tell owners and editors when a newer one is out, and then reads ailimu.net/releases.json for what it brings. Neither request carries anything about your install or your data. Set to off to stop both. |
MCP_ALLOWED_ORIGINS |
Optional. Claude and other MCP clients send no Origin header and are never affected. A web page on another site that calls /mcp is refused. List the addresses of any browser-based tool that should be let through, separated by commas, e.g. http://localhost:6274 for the MCP inspector. |
TRUST_PROXY |
Optional. Set this only when Ailimu sits behind a reverse proxy, so sign-in limits and secure cookies see the browser's address rather than the proxy's. "loopback" for a proxy on the same machine, the proxy's address or subnet (e.g. 172.18.0.0/16 for one in the same Docker network), or a hop count. Leave unset otherwise: a trusted proxy header can be faked by anyone who can reach Ailimu directly. |
CONFIG_DIR |
Leave CONFIG_DIR unset. Local default is ./.ailimu-config and the Docker image sets /config itself. Setting it here would override the volume path. |
FM_TIMEOUT_SECONDS |
Optional. Seconds to wait for FileMaker to answer one request before giving up. Default 60. |
PG_TIMEOUT_SECONDS |
Optional. Seconds PostgreSQL may spend on one statement before it is stopped, so a search that cannot finish says so instead of holding a connection. Default 30. |
Credentials for several files
With more than one profile, each may have its own FileMaker account, named after the profile's slug in upper snake case, falling back to the unsuffixed pair:
FM_USER_SALES_LIVE=...
FM_PASS_SALES_LIVE=...
Each profile also has its own server address, set in the wizard. So files on
different servers, such as https://www5.example.com and https://www6.example.com
with a different user on each, are simply two profiles with their own variables:
FM_USER_SERVER5=...
FM_PASS_SERVER5=...
FM_USER_SERVER6=...
FM_PASS_SERVER6=...
FM_HOST does not choose the server for a profile. It only pre-fills the address
on a new connection. You do not need it when you use several servers.
To add another account, for example when a second FileMaker file or server needs a user with its own privileges:
- Add the profile in the wizard and note its slug, such as
sales-live. - Add
FM_USER_SALES_LIVEandFM_PASS_SALES_LIVEto.env. Hyphens in the slug become underscores. - Restart:
docker compose up -d --force-recreate.
Each account needs the fmrest extended privilege for scripts and fmodata for
tables. The Connection step tells you whether a file is using its own account or
the default one, and which variable names it looks for.
Credentials are read from the environment only. They are never written to the
config file and never handled by the wizard, which is why adding a profile with
its own account means editing .env and restarting.
Endpoints
| Method | Path | Auth | What |
|---|---|---|---|
GET |
/healthz |
none | Liveness. Returns {ok:true} and nothing about your data. |
GET |
/ |
none | Redirects to /admin, so the bare address opens the setup wizard. |
GET |
/docs |
none | This manual, for the version you are running. |
GET |
/admin |
none | The setup wizard. It asks you to sign in, or to create the owner account on first run. |
POST |
/mcp |
mcp token | MCP endpoint. Serves the only profile when the connector holds exactly one. |
POST |
/mcp/:profile |
profile token | MCP endpoint for one profile, with its own token. |
GET |
/admin/auth/me |
none | Whether to show first-run setup, sign-in, or the wizard. |
POST |
/admin/auth/setup |
none | First run only: create the owner account, with the setup code (ADMIN_TOKEN). |
POST |
/admin/auth/login |
none | Sign in with email and password. Five misses lock that email out for fifteen minutes. |
POST |
/admin/auth/logout |
none | Sign out this browser. |
GET |
/admin/auth/link/:secret |
none | Check a one-time invite or reset link without using it. |
POST |
/admin/auth/link/:secret |
none | Use a one-time link: choose a password and sign in. |
POST |
/admin/auth/restore |
none | First run only: restore a backup instead of creating an owner, with the setup code (ADMIN_TOKEN) and the backup's password. |
GET |
/admin/api/status |
viewer | Credential readiness, defaults, the profile list and who you are. |
GET |
/admin/api/profiles |
viewer | Every profile this connector serves. Tokens are left out for viewers. |
GET |
/admin/api/profiles/:slug |
viewer | One profile's published config. |
GET |
/admin/api/profiles/:slug/schema |
viewer | The cached schema, so opening a profile costs no FileMaker read. |
GET |
/admin/api/profiles/:slug/erd |
editor | The relationships between this connection's tables, for the ERD tab: each table's key fields and the links FileMaker declares or the field names suggest. Read from the cache, so no FileMaker read. |
GET |
/admin/api/profiles/:slug/claude-desktop |
editor | The Claude Desktop extension (.mcpb) for this profile, with its address and token filled in. |
PUT |
/admin/api/profiles/:slug/fill-mode |
editor | How the wizard fills in a table that has nothing exposed yet: essentials or empty. Saved without publishing. |
DELETE |
/admin/api/profiles/:slug |
editor | Delete a profile. Its endpoint stops immediately; FileMaker is untouched. |
POST |
/admin/api/databases |
editor | Files this FileMaker server exposes to the account. |
POST |
/admin/api/test-connection |
editor | Check the host, file and privileges before discovering. |
POST |
/admin/api/discover |
editor | Read the schema: tables, row counts, scripts and layouts. |
POST |
/admin/api/sample |
editor | Read a bounded row sample for named tables, to find unused columns. |
POST |
/admin/api/preview |
viewer | The tools a configuration would produce, without saving it. |
POST |
/admin/api/try |
editor | Run one read tool from an unsaved configuration against live data. |
GET |
/admin/api/config |
viewer | The current config and the tools it generates. |
PUT |
/admin/api/config |
editor | Publish. Takes effect immediately, with no restart, and records who published. |
GET |
/admin/api/activity |
editor | Search the activity log by date, keyword, connection, who and kind; outcome=error shows failures only. The first page also returns per-kind counts and the people list. format=csv downloads every match. |
GET |
/admin/api/people |
owner | Everyone with an account, and pending invite and reset links. |
PUT |
/admin/api/people/:id/access |
owner | Give a person access to one connection with their own key, change it to read-only or back, or take it away. |
POST |
/admin/api/people/:id/access/:slug/reset |
owner | A new key for one person on one connection. Their old key stops working at once. |
GET |
/admin/api/my-access |
signed in | The connections you may use with Claude. With reveal=1, your own keys for them (recorded in the activity log). |
GET |
/admin/api/my-access/:slug/claude-desktop |
signed in | Your own Claude Desktop extension for one connection, with your key inside. |
GET |
/admin/api/server-keys |
owner | The setup code (ADMIN_TOKEN) and the token for the older /mcp address. Recorded in the activity log. |
POST |
/admin/api/backup |
owner | A password-protected backup of every connection, token and account, and the activity log if asked. |
POST |
/admin/api/restore |
owner | Replace everything with a backup. Everyone is signed out. |
POST |
/admin/api/people |
owner | Invite a person with a role. Returns a one-time link to pass on; no email is sent. |
PATCH |
/admin/api/people/:id |
owner | Change someone's role. The last owner cannot be demoted. |
DELETE |
/admin/api/people/:id |
owner | Remove a person and sign them out everywhere. |
POST |
/admin/api/people/:id/link |
owner | A fresh one-time link: a password reset, or a re-sent invite. |
DELETE |
/admin/api/links/:id |
owner | Cancel a pending invite or reset link. |
POST |
/admin/api/account/password |
signed in | Change your own password. Signs you out everywhere else. |
/healthz and /docs are deliberately unauthenticated: one is a liveness probe
that reveals nothing, the other is this manual.
Tools
A table called invoices, a saved search called unpaid and a script exposed as recalculate_totals produce:
| Tool | What it does |
|---|---|
get_invoices |
One record by its identifier. Every readable field. |
find_invoices |
Search. Returns the identifier plus the searchable fields, and the total match count. |
describe_invoices |
The fields a search cannot filter on, and how this table links to others. |
update_invoices |
Change a record. Only exists when writes are enabled and read-only is off. |
search_unpaid |
One saved search: a question set up in advance, with its own conditions and blanks. Read-only, and exists even when read-only is on. |
recalculate_totals |
One exposed FileMaker script. Only exists when read-only is off. |
Limits
| Limit | Value | Why |
|---|---|---|
| Rows sampled per table | 50 | A few of the oldest rows and more of the newest, so a field added to the file later is not mistaken for an unused one. |
| Sample cell budget | 5000 | FileMaker's OData returns malformed JSON past roughly 280KB, so a wide table gets fewer rows. A 208-column table samples 24 rows. |
| Concurrent FileMaker requests | 8 | Discovery is one request per table; unbounded would hammer the server. |
| Tool result budget | 40KB | Whole rows are dropped to fit, and the tool says how many. About 10,000 tokens. Change it with RESULT_MAX_KB; the earlier format allowed 200KB. |
| Rows per search | 25 (at most 500) | What a search returns when no limit is given. Change the default with FIND_DEFAULT_ROWS. |
| Long lists as a table | 7 rows or more | A search this long is written with the field names once, then each row as a list of values. Change it with COLUMNAR_FROM_ROWS; 0 never does. |
| Long text in a search | 500 characters | A longer text is cut and says where the whole of it is: get_ returns it in full. Change it with TEXT_CLIP_CHARS. |
| Tables per sample request | 500 | A malformed request cannot turn into an unbounded scan. |
Config file
One JSON file per profile, under profiles/ on the config volume. You should
not need to edit it by hand, but it is plain JSON so you can read it and keep it
in version control if you want to.
| Field | Type | Default |
|---|---|---|
tenantId |
string | required |
slug |
string | optional |
displayName |
string | required |
connection |
object | required |
connection.host |
string | required |
connection.database |
string | required |
connection.apiVersion |
string | "vLatest" |
connection.backend |
dataapi | odata | postgres | "dataapi" |
connection.port |
number | optional |
connection.schema |
string | optional |
connection.ssl |
off | require | verify | optional |
connection.scriptDatabase |
string | optional |
readOnly |
boolean | true |
resources |
array | [] |
scripts |
array | [] |
savedSearches |
array | [] |
audit |
object | {} |
audit.enabled |
boolean | true |
audit.logWrites |
boolean | true |
audit.logReads |
boolean | false |
updatedAt |
string | optional |
updatedBy |
string | optional |
lastDiscovered |
string | optional |
fillMode |
essentials | empty | optional |
Activity log format
What the Activity screen searches is plain text on the config volume, so you can also read it on the server, back it up, or feed it to your own monitoring or log system.
/config/activity/
2026-09-28.jsonl changes, script runs and admin actions, one file per day
2026-09-27.jsonl
reads/
2026-09-28.jsonl Claude's reads, only for connections that record them
- One file per day, named by the UTC date of its entries. Each line is one
complete JSON object, so the files work with
grep,jqand any log shipper that reads JSON lines. - Old files are deleted whole once they pass
ACTIVITY_RETAIN_DAYS(365) or, forreads/,ACTIVITY_READS_RETAIN_DAYS(30). Deletion happens when the connector starts and on the first entry of each new day. - Times are UTC, in ISO 8601. The Activity screen shows them in your browser's time zone.
Fields
| Field | In | What |
|---|---|---|
ts |
every entry | When it happened, UTC. |
kind |
every entry | change, script, read or admin. |
connection |
most entries | The connection's slug, as in /mcp/<slug>. |
actor |
every entry | Claude for tool calls; a person's email for admin actions; admin token for API calls made with ADMIN_TOKEN. |
tool |
change, script, read | The MCP tool that was called. |
outcome |
change, script, read | ok or error. |
error |
failed calls | What FileMaker or Ailimu said, up to 500 characters. |
ms |
change, script, read | How long the call took, in milliseconds. |
record |
change | The ID of the record that was changed. |
fields |
change | The fields that were set, with the values sent. |
fmScript |
script | The FileMaker script's own name. |
args |
script, read | What Claude passed: script parameters, or search criteria. |
matches, returned |
read | For a search: how many records matched, and how many came back. |
bytes |
change, script, read | How much text Claude was handed for the call, in bytes. Roughly four bytes to a token, so it shows which tools cost the most. Entries written before this was added do not have it. |
action |
admin | What the person did, in words. |
person, role |
admin | Who an action on people was about, and their role. |
migrated |
imported entries | true on entries carried over from an old audit.log. |
What a read never contains: the rows that came back. A read records the question and the count, so the log does not become a second copy of your data. A change does contain the values written, which is why Activity is for editors and owners only.
One entry of each kind
A change:
{"ts":"2026-09-28T09:05:12.441Z","kind":"change","connection":"sales","actor":"Claude","tool":"update_invoices","record":"INV-70114","fields":{"payment_status":"Paid"},"outcome":"ok","ms":312}
A script run that FileMaker refused:
{"ts":"2026-09-28T09:06:40.118Z","kind":"script","connection":"sales","actor":"Claude","tool":"recalculate_totals","fmScript":"Recalculate Totals","args":{"invoice_id":"INV-70114"},"outcome":"error","error":"FileMaker script Recalculate Totals returned error 401: no records matched the request.","ms":540}
A search, on a connection that records reads:
{"ts":"2026-09-28T09:04:58.003Z","kind":"read","connection":"sales","actor":"Claude","tool":"find_invoices","args":{"payment_status":"Overdue"},"outcome":"ok","returned":50,"matches":400,"ms":188}
An admin action:
{"ts":"2026-09-28T08:59:30.772Z","kind":"admin","actor":"[email protected]","action":"published a connection","connection":"sales","readOnly":false,"logReads":true,"tables":4,"scripts":1}
Searching on the server
# Everything that touched one record
docker compose exec ailimu sh -c 'grep -h INV-70114 /config/activity/*.jsonl'
# Every failed call in the last day's file, with jq on the host
docker compose exec ailimu cat /config/activity/2026-09-28.jsonl | jq -c 'select(.outcome == "error")'
Known FileMaker behaviour
Things the FileMaker APIs do that Ailimu works around. They are listed here because they look like Ailimu bugs and are not:
- OData returns malformed JSON past roughly 280KB. The body arrives truncated mid-number. On a 208-column table, 30 rows come back intact while 40 and 50 do not, and 50 rows of 150 columns also fails: the limit tracks payload size, not row or column count. This is why samples are capped by cell count.
$count=trueinline is ignored. FileMaker answers the/$countsegment instead, and refuses that request ifAcceptnames a type.- Percent-encoded quotes, parens, commas and colons are rejected by FileMaker's OData parser, so those are sent raw in filters.
- Without
$top, up to 10,000 rows come back. Every read is bounded. - A container field is returned as the file itself, base64 inline. A single row can reach 1.4MB this way, so containers are never exposed.
- The Data API lists scripts by name only: no body, no parameters, and a
duplicated name cannot be addressed by
script=. - A table appears once per relationship path. A large file can expose 231 entity sets covering around 75 distinct tables. The wizard groups the copies and shows you the plainest name.