# Automata documentation > Automata is a free, open-source MCP access control layer that connects AI agents like Claude and ChatGPT to WhatsApp, Telegram, and PostgreSQL with strictly enforced chat-by-chat and table-by-table permissions. This file contains every page of the Automata docs (https://www.getautomata.app/docs) as Markdown, in reading order. A shorter index with links to product pages and guides is at https://www.getautomata.app/llms.txt. --- # Give AI useful access, not unlimited access URL: https://www.getautomata.app/docs Section: Start here > Choose a connector and follow the shortest path to your first MCP request. Automata is an open-source MCP control layer. It connects AI clients to a single messenger account, email mailbox, or PostgreSQL database, then enforces exactly which actions, chats, folders, and tables that connector can reach. > **Skip local setup** > > The fastest way to run Automata is deploying the full stack on Railway with one click. Postgres, Redis, and volumes are provisioned for you. To host it yourself, use the [single container](https://www.getautomata.app/docs/self-hosting/single-container) or [Docker Compose](https://www.getautomata.app/docs/self-hosting/docker-compose); the getting-started guide covers local development. > > [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/automata-or-whatsapp-telegram-sql-mcp?referralCode=HxrUPo&utm_medium=integration&utm_source=template&utm_campaign=generic) - [Run Automata](https://www.getautomata.app/docs/getting-started): Start the core services, create a connection, and make your first MCP request. - [Design a safe scope](https://www.getautomata.app/docs/permissions): Limit a connector to the tools and resources required for its job. ## Choose your path - [Connect WhatsApp](https://www.getautomata.app/docs/connections/whatsapp): Pair through Evolution API, import history, and enable message reads. - [Connect Telegram](https://www.getautomata.app/docs/connections/telegram): Link a personal account through the included Telegram bridge. - [Connect email](https://www.getautomata.app/docs/connections/email): Read and search one mailbox over IMAP, with optional drafts that are never sent. - [Connect PostgreSQL](https://www.getautomata.app/docs/connections/postgresql): Use a least-privilege role with guarded read and write tools. - [Self-host in one container](https://www.getautomata.app/docs/self-hosting/single-container): Run the whole stack with one docker run, or use Docker Compose for one service per container. - [Invite your team](https://www.getautomata.app/docs/organizations): Share connections through an organization with administrator and member roles. ## The five-step flow ### Step 1: Run the services you need Postgres and the web app are enough for PostgreSQL and email connectors. WhatsApp and Telegram each add their own optional service profile. ### Step 2: Create one connection A connection represents one WhatsApp account, Telegram account, email mailbox, or PostgreSQL database. ### Step 3: Mint a connector token Choose an expiry and select the action plus resource scopes. The token appears once, so copy it immediately. ### Step 4: Add the endpoint to your MCP client Use either the bearer-header endpoint or the path-token endpoint supported by your client. ### Step 5: Adjust access without reconnecting Scope edits and revocation are read on every request and take effect immediately. ## Supported connections | Connection | Transport | Read support | Write support | | --- | --- | --- | --- | | WhatsApp | Evolution API | Chats and message history | Plain-text messages | | Telegram | Included bridge | Chats, topics, messages, and search | Plain-text messages | | Email | IMAP | Folders, emails, attachments, and search | Plain-text drafts, saved but never sent, when explicitly enabled | | PostgreSQL | Native connection string | Catalog inspection and guarded queries | Guarded DML when explicitly enabled | > **One connection per connector** > > A token is permanently bound to the connection where it was created. To expose several accounts or databases, create one token for each and add each token as a separate MCP connector. Source: https://github.com/ju-li/automata-mcp/blob/main/README.md --- # Getting started URL: https://www.getautomata.app/docs/getting-started Section: Start here > Run Automata locally and connect an MCP client. Start the smallest useful Automata stack, create one connection, and hand a narrowly scoped connector token to your MCP client. > **Skip local setup** > > Want to avoid installing dependencies yourself? Deploy Automata on Railway with one click and provision Postgres, Redis, and volumes automatically. The steps below cover running Automata locally for development. To self-host on a server without building anything, use the [Docker Compose guide](https://www.getautomata.app/docs/self-hosting/docker-compose). > > [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/automata-or-whatsapp-telegram-sql-mcp?referralCode=HxrUPo&utm_medium=integration&utm_source=template&utm_campaign=generic) ## Prerequisites - Git and Docker with Docker Compose - Node.js and Corepack - `pnpm` as pinned by the source repository - A PostgreSQL connection string, or the credentials required by your chosen messenger connector > **Note** > > The source project is a Nuxt application. These commands belong in the cloned `automata-mcp` repository, not this documentation repository. ## Start the core services Copy the environment template, install dependencies, and start Postgres: ```bash cp .env.example .env pnpm install pnpm services:up ``` The template's `NUXT_DATABASE_URL` already points at the bundled Postgres. The web app creates its own `app` schema there on first boot, so there is no separate admin account to configure. Add a connector profile only when needed: ```bash pnpm services:up:whatsapp pnpm services:up:telegram ``` The WhatsApp profile adds Redis and Evolution API. The Telegram profile adds the Telegram bridge. Both share the same Postgres. ## Run the web app Nuxt runs on the host to preserve hot module replacement: ```bash pnpm dev ``` Open `http://localhost:3000`. Postgres listens on `5432`, Evolution on `8080`, and the Telegram bridge health endpoint is `http://localhost:8095/health`. > **Important** > > There is no `predev` hook. Start the required services before running the web application. ## Create a connection Sign up, which creates your [organization](https://www.getautomata.app/docs/organizations) with you as its administrator. Then select **Add a connection** and choose one kind: 1. Pair WhatsApp by scanning the QR code from **WhatsApp → Settings → Linked devices**. Tick **Import full history** first to import everything the phone holds; it is off by default, and on the hosted Free plan only recent history is imported. A WhatsApp connection not paired within an hour is deleted. 2. Link Telegram from **Telegram → Settings → Devices → Link Desktop Device**. Chats start syncing as soon as it links; on the hosted Free plan, older history is fetched back 90 days. 3. Enter an email account's IMAP server and an app password. Automata logs in before saving it. 4. Paste a PostgreSQL connection string. Automata proves the connection before saving it. Follow the connector-specific guide before using a real messenger account. ## Mint a connector token From the connection dashboard, select **New token**. Give it a label, choose an expiry, and select only the tools and resources needed by the client. > **Copy the token now** > > The raw token is shown once. Automata stores only its SHA-256 hash. A lost token cannot be recovered; replace it with a new one. ## Connect your client Use the endpoint displayed by the connection dashboard. For a local path-token client: ```text http://localhost:3000/mcp/ ``` For clients that support headers, use `http://localhost:3000/mcp` with: ```text Authorization: Bearer ``` Continue with the [MCP client guide](https://www.getautomata.app/docs/clients), then use the status or database-info tool to confirm the connection. Source: https://github.com/ju-li/automata-mcp/blob/main/README.md --- # How Automata works URL: https://www.getautomata.app/docs/how-automata-works Section: Start here > Understand connections, tokens, scopes, and the services behind each connector. Automata separates management from tool execution, then narrows each MCP request by connection kind and connector-token scope. ## Two surfaces, one server The **web UI** uses an HTTP-only session cookie, stored as a SHA-256 hash in the app's Postgres schema. People create connections, pair accounts, rotate credentials, invite teammates, and manage connector tokens there. The **MCP endpoint** uses `Authorization: Bearer ` or `/mcp/`. It never falls back to a browser session. An invalid token receives a proper `401` response with `WWW-Authenticate`; a database outage returns `503` rather than pretending the token was revoked. ## Connections and tokens A connection is exactly one WhatsApp account, Telegram account, email mailbox, or PostgreSQL database. Every connector token resolves to exactly one connection, so tools do not accept a connection argument. The connection keeps upstream credentials server-side. WhatsApp uses a per-instance Evolution token, Telegram uses a per-session bridge token, email uses the mailbox's IMAP password, and PostgreSQL uses the supplied connection string. Connector tokens are Automata credentials, not upstream API keys. ## Organizations Connections belong to an organization, and members have an administrator or member role in the management UI. Roles never widen a connector token: the token alone decides what an AI client can reach. See [Organizations and roles](https://www.getautomata.app/docs/organizations). ## Permission enforcement Every tool request passes two independent gates: 1. **Connection kind:** a PostgreSQL token never registers messenger tools. 2. **Token scope:** only explicitly enabled actions, chats, email folders, and tables are reachable. For PostgreSQL, query plans are checked before data is read or modified. For messenger searches, chat allowlists are applied in the data query rather than filtering results afterward. ## Service architecture | Service | Responsibility | Required when | | --- | --- | --- | | Web | Management UI and MCP endpoint | Always | | Postgres | Users, sessions, connections, scopes, token hashes, synced email headers, and [telemetry](https://www.getautomata.app/docs/telemetry) in the `app` schema; messenger data in `public` and `telegram` | Always | | Evolution API + Redis | WhatsApp pairing and transport | WhatsApp is enabled | | Telegram bridge | Personal-account sessions and sync | Telegram is enabled | | User database | The database reached by PostgreSQL tools | A PostgreSQL connection is added | | Mail server | The IMAP mailbox reached by email tools; bodies and attachments are fetched from it on request | An email connection is added | > **Note** > > A deployment serving only PostgreSQL connections needs the web app and Postgres. Messenger infrastructure is optional and should not be exposed unless it is used. Source: https://github.com/ju-li/automata-mcp/blob/main/README.md --- # WhatsApp URL: https://www.getautomata.app/docs/connections/whatsapp Section: Connect data sources > Pair an account through Evolution API and enable message history. New to the idea of a WhatsApp MCP? Start with the [WhatsApp MCP overview](https://www.getautomata.app/whatsapp-mcp) for what it does and how per-chat permissions work. This page covers setup. Automata provisions one Evolution API instance per WhatsApp connection. Pairing and sending use Evolution's API; message reading, search, and chat listing query its PostgreSQL datastore directly, because the Evolution API cannot search or page history efficiently. > **Use a spare number for testing** > > Pairing binds a real WhatsApp account. Repeated pair and unpair cycles or unsolicited messages can cause rate limits or a ban. Do not experiment with a primary phone number. ## What you need - Evolution API `v2.3.7`, Redis, and PostgreSQL - A generated `EVOLUTION_API_KEY` - A matching `NUXT_EVOLUTION_ADMIN_KEY` on the web service - `NUXT_EVOLUTION_DATABASE_URL` for message reads and search - A webhook URL reachable from Evolution The Evolution database URL can be the owner URL. Message reads only run fixed, parameterized `SELECT` statements in read-only sessions. A dedicated `SELECT`-only role is optional hardening: ```sql CREATE ROLE wamcp_search LOGIN PASSWORD ''; GRANT CONNECT ON DATABASE evolution TO wamcp_search; GRANT USAGE ON SCHEMA public TO wamcp_search; GRANT SELECT ON "Message" TO wamcp_search; -- Connection status: the account's own row and per-account counts. GRANT SELECT ("id", "connectionStatus", "ownerJid", "profileName", "profilePicUrl", "number", "disconnectionAt", "updatedAt") ON "Instance" TO wamcp_search; GRANT SELECT ("instanceId") ON "Chat", "Contact" TO wamcp_search; -- Group names, read from Evolution's own chat rows. GRANT SELECT ("remoteJid", "name") ON "Chat" TO wamcp_search; ``` The `"Instance"` grant is column by column on purpose: the same row holds the instance's API key, which this role never needs. The same URL also serves connection status. The dashboard, the alert sweep, and `get-connection-status` read the account's own `"Instance"` row and count only that account's rows, instead of Evolution's `fetchInstances`, which scans every account's messages on each call. A role created before these grants still works: status falls back to `fetchInstances`, cached for five minutes, and the web log says a grant is missing. A count that is only slow, because it hit the statement timeout or the account already has a read running, never falls back, since `fetchInstances` would scan every account to answer for one: the dashboard shows the last counts, or a dash. Without the `"updatedAt"` grant, [reconnect loops](https://www.getautomata.app/docs/connections/whatsapp#stuck-reconnecting) go undetected, and a [stuck session](https://www.getautomata.app/docs/connections/whatsapp#stuck-with-the-socket-dead) is reported from failed calls alone, for 30 minutes after the last one. ## Start the WhatsApp stack ```bash pnpm services:up:whatsapp pnpm dev ``` Confirm Evolution at `http://localhost:8080` and the app at `http://localhost:3000`. For a deployed stack, use the `whatsapp` profile in [Docker Compose](https://www.getautomata.app/docs/self-hosting/docker-compose) or the Evolution service in [Railway](https://www.getautomata.app/docs/self-hosting/railway). ## Pair an account Create a **WhatsApp account** connection. Tick **Import full history** first if you want everything the phone holds rather than the recent part (see [Message history](https://www.getautomata.app/docs/connections/whatsapp#message-history)). Continue to the QR code, then scan it from **WhatsApp → Settings → Linked devices → Link a device**. After the scan, the page shows **Importing your WhatsApp history** with message and chat counts climbing, then moves on to the dashboard once they stop changing for about fifteen seconds (three minutes at most). **Show dashboard now** skips the wait; the import carries on in Evolution either way. Wait for the dashboard to report the connection as open before creating a token. A scoped send is refused when Automata cannot verify a recipient because the account is disconnected. > **Pair within an hour** > > A WhatsApp connection that is not paired within one hour of being created is deleted automatically, together with its connector tokens. The pairing screen counts down the time left. A connection that was paired once and later signed out is never removed. An unpaired connection is not idle: Evolution keeps it `connecting`, prints a fresh QR code to its log every few minutes, and reconnects it at every restart. On the hosted service it also counts toward the organization's connection limit. A background task checks every ten minutes and re-checks Evolution live just before deleting, so a connection that turns out to be linked, or that it cannot check, is kept. Deletion goes through the same path as the **Delete** button. Telegram connections are not affected. ## Message history `read-messages` and `search-messages` require `NUXT_EVOLUTION_DATABASE_URL`. Without it, pairing, chat listing, and sending still work, but read tools return a missing-prerequisite response. History arrives only while a device is being linked, and how much depends on **Import full history** on the create form: - **Unticked (the default, for every plan and self-hosted):** WhatsApp hands over only the recent history it gives any newly linked browser. How far back that reaches is WhatsApp's choice. Group chats are included. - **Ticked:** WhatsApp is asked for everything the phone holds. A full import is most of the disk an account uses. Live messages from then on are all kept either way. Before scanning, keep `DATABASE_SAVE_DATA_HISTORIC=true` on Evolution, or the history burst is dropped. On the hosted Free plan the box is locked and only recent history is imported; the server refuses a full import with `402 plan_limit`. The lock applies only to the deployment's own Evolution server, never to a [bring-your-own](https://www.getautomata.app/docs/connections/whatsapp#bring-your-own-evolution) one, and deletes nothing already imported. Moving to Pro imports nothing by itself. To import full history for an existing connection, an administrator presses **Import full history** on its dashboard. That signs the device out and puts the QR code back up; the import rides in on the re-scan, and nothing already stored is lost. Nothing can ask WhatsApp for older messages after pairing, so the tools say when history is partial: `read-messages`, `search-messages`, and `get-connection-status` return `recentHistoryOnly: true` when the account was paired without full history. Things to expect from the stored history: - Group conversations are included alongside private chats. - Media is not downloaded; messages carry their text or caption. - Reactions are omitted unless a tool is called with `includeReactions: true`. - Searches take at most 10 words of up to 100 characters each, 500 characters in all. On the deployment's own message database each connection runs one read at a time with up to eight more queued; past that a read answers `429` and asks the caller to wait. - The dashboard counts conversations reported during pairing, while `list-chats` counts conversations that have stored messages, so the numbers can differ. - There are no unread counts. Evolution does not keep the signal that says a chat was read on the phone, so neither `list-chats` nor the dashboard reports read state, and the tool tells the model so. - Group names can lag behind a rename by up to a week. Names come from Evolution's own chat rows, which cost WhatsApp nothing but are written once, when the chat first arrives. Groups with no stored name, and every group once a week, are looked up one at a time, at most four per account every 15 minutes. A renamed group shows its old name until its next lookup, a group with no stored name shows the contact name or bare id until its first, and a group's member count appears only once it has been looked up. Without the `"Chat"` name grant above, names come from the lookups alone. Chats and messages are read live. - Groups the account has left are marked gone when WhatsApp refuses the lookup, keep their last name, and are not asked about again for 30 days. Only lookups nothing answered, or that WhatsApp answered with `rate-overlimit`, back the account's lookups off (one hour, doubling to a day). > **Protect the session volume** > > Never use `docker compose down -v` for a routine restart. Deleting the `evolution_instances` volume unpairs every WhatsApp account and forces new QR scans. ## Indexes on Evolution's tables Evolution indexes `"Message"` on `instanceId` only, so reads, searches, and chat listings scan the whole account. History import makes that corpus large. These indexes are optional but strongly recommended on any busy deployment. Create them as a superuser on Evolution's database; `CONCURRENTLY` keeps writes flowing while they build. **Per-chat messages by time.** Serves `read-messages` and the chat listing behind `list-chats`, the token scope picker, and the dashboard. Without it, every listing reads every message of the account, and large accounts hit the 10-second timeout. ```sql CREATE INDEX CONCURRENTLY IF NOT EXISTS message_instance_chat_ts_idx ON "Message" ("instanceId", (key->>'remoteJid'), "messageTimestamp" DESC); ``` Any index on these three columns in this order counts, whatever its name; don't build a second. The app looks for one every ten minutes, so no restart is needed. Either way, one account's chat listing is read at most once a minute and shared by every caller. **Message text for search.** Without it, `search-messages` and the dashboard's message filter read every message of the account on every search. A trigram index lets Postgres read only the messages that contain the words. It needs `pg_trgm`; find the schema it lives in (the Telegram bridge installs it in `telegram`) and qualify the operator class with it: ```sql SELECT n.nspname FROM pg_extension e JOIN pg_namespace n ON n.oid = e.extnamespace WHERE e.extname = 'pg_trgm'; ``` ```sql CREATE INDEX CONCURRENTLY IF NOT EXISTS message_body_trgm ON "Message" USING gin ((COALESCE( message->>'conversation',, message->'extendedTextMessage'->>'text',, message->'imageMessage'->>'caption',, message->'videoMessage'->>'caption',, message->'documentMessage'->>'caption',, message->'documentWithCaptionMessage'->'message'->'documentMessage'->>'caption',, message->>'speechToText',, message->'reactionMessage'->>'text',, message->'associatedChildMessage'->'message'->>'conversation',, message->'associatedChildMessage'->'message'->'extendedTextMessage'->>'text',, message->'associatedChildMessage'->'message'->'imageMessage'->>'caption',, message->'associatedChildMessage'->'message'->'videoMessage'->>'caption',, message->'associatedChildMessage'->'message'->'documentMessage'->>'caption',, message->'protocolMessage'->'editedMessage'->>'conversation',, message->'protocolMessage'->'editedMessage'->'extendedTextMessage'->>'text',, message->'protocolMessage'->'editedMessage'->'imageMessage'->>'caption',, message->'protocolMessage'->'editedMessage'->'videoMessage'->>'caption',, message->'protocolMessage'->'editedMessage'->'documentMessage'->>'caption',, message->'associatedChildMessage'->'message'->'protocolMessage'->'editedMessage'->>'conversation',, message->'associatedChildMessage'->'message'->'protocolMessage'->'editedMessage'->'extendedTextMessage'->>'text',, message->'associatedChildMessage'->'message'->'protocolMessage'->'editedMessage'->'imageMessage'->>'caption',, message->'associatedChildMessage'->'message'->'protocolMessage'->'editedMessage'->'videoMessage'->>'caption',, message->'associatedChildMessage'->'message'->'protocolMessage'->'editedMessage'->'documentMessage'->>'caption' )) telegram.gin_trgm_ops); ANALYZE "Message"; ``` Copy the expression exactly. Postgres uses the index only for a query whose expression is identical to it, so a copy that differs in any arm or in their order is never used, and searches stay slow without any error. If an app release changes how message text is read, its release notes say so; drop the index and build it again from the [upstream WhatsApp notes](https://github.com/ju-li/automata-mcp/blob/main/docs/whatsapp.md#indexes-on-evolutions-tables). `ANALYZE` gives the planner statistics on the new expression. Expect 1–2 GB of disk on a table of a few million messages. A word needs three letters or digits in a row to use the index; a search made only of shorter words or emoji still reads the whole account. **`"IsOnWhatsapp"."jidOptions"`.** With `DATABASE_SAVE_IS_ON_WHATSAPP` on, Evolution's own number lookups read that whole table. A trigram index fixes it: ```sql CREATE INDEX CONCURRENTLY IF NOT EXISTS isonwhatsapp_jidoptions_trgm ON "IsOnWhatsapp" USING gin ("jidOptions" telegram.gin_trgm_ops); ``` In both trigram indexes, replace `telegram` with the schema the query above named. If it named none, run `CREATE EXTENSION pg_trgm SCHEMA public;` and use `public.gin_trgm_ops`. A failed concurrent build leaves an invalid index; drop it with `DROP INDEX CONCURRENTLY` and build it again. **Vacuum `"Message"` more often,** so freshly imported history is marked all-visible and message counts stay index-only: ```sql ALTER TABLE "Message" SET (autovacuum_vacuum_insert_scale_factor = 0.01, autovacuum_vacuum_scale_factor = 0.05); ``` All of these live on Evolution's tables. They survive restarts, but a Prisma migration that recreates a table drops them without warning, so re-check them after bumping Evolution's image tag. ## Stuck reconnecting Sometimes two sockets log in with the same credentials and keep replacing each other. Evolution's log shows `stream errored out` dozens of times a minute for one instance, each with a `conflict` of type `replaced`, followed by a fresh `CONNECTED TO WHATSAPP`. The stored status reads `open` the whole time. Automata detects the loop from Evolution's `"Instance"` row, which is rewritten on every socket open. A connection whose `open` row keeps being freshly written for ten minutes is marked **Unstable** on the dashboard, `get-connection-status` returns `flapping: true`, and after the usual grace period everyone who can reach the connection is [emailed](https://www.getautomata.app/docs/alerts). With `NUXT_OPS_ALERT_EMAIL` set on the web service, the operator is also mailed as soon as a loop on this deployment's own Evolution server is detected. Detection needs `NUXT_EVOLUTION_DATABASE_URL` and, with a dedicated role, the `"updatedAt"` grant above. **Fix: restart the Evolution service.** On boot it reconnects every account with one socket each. Every account drops briefly; none needs a QR scan. Do it soon, because repeated logins on a real number can get it flagged by WhatsApp. **Reconnect** in the dashboard does not help, so it is offered only when the socket is `close`. Automata no longer calls Evolution's restart route, which on Baileys can itself open a second socket. ## Stuck with the socket dead The signature: the dashboard says **Connected** and `get-connection-status` answers `open`, but every send fails with "The WhatsApp connection is down", the web log shows `WhatsApp socket not connected` and `Connection Closed` on group lookups, and no new message has been stored since some point. Unlike a reconnect loop, Evolution's row is not being rewritten. Evolution's in-memory state still reads `open` over a socket that is gone. The likeliest cause is Evolution's database writes failing mid-event, as happens during a Postgres restart. Nothing inside Evolution recovers it. **Fix: restart the Evolution service.** On boot it reconnects every account whose status is `open`, so no QR scan is needed. **Reconnect** cannot help, because it acts only on a `close` socket. Restart Evolution after every Postgres restart, including ones the platform makes itself. Automata detects it from the failures themselves. A send or group lookup that Evolution answers with `Connection Closed` records the time; a later status read that finds the account `open` with no socket opened since marks it **Stuck** on the dashboard, and `get-connection-status` returns `stalled: true`. It is [alerted](https://www.getautomata.app/docs/alerts) like a reconnect loop: the operator at once when `NUXT_OPS_ALERT_EMAIL` is set, everyone who can reach the connection after the ten-minute grace. The record clears when WhatsApp next answers, when Evolution reopens the socket, or when the account next reads as not `open`. Detection needs `NUXT_EVOLUTION_DATABASE_URL`, and it is found only by a call that needs the socket: an account nobody sends from waits for its next group lookup. `Timed Out` is not counted, since a healthy socket can answer it too. ## Bring your own Evolution Self-hosted only: the hosted service does not offer this option and refuses it with `422`. Existing bring-your-own connections keep working. Select **Use my own Evolution API server** and provide both the server origin and its global key. Automata never fills a missing half from the deployment default, and it never sends one server key to another server. A bring-your-own connection also needs a database URL for that Evolution server before message read tools can work. Automata validates that the database contains a readable `"Message"` table before saving it. Give a dedicated role the same `"Instance"`, `"Chat"` (including the name grant), and `"Contact"` grants shown above. Without a database URL, read tools return an explicit error instead of empty results. Source: https://github.com/ju-li/automata-mcp/blob/main/docs/whatsapp.md --- # Telegram URL: https://www.getautomata.app/docs/connections/telegram Section: Connect data sources > Link a personal Telegram account through the bridge service. The included bridge links a personal Telegram account by QR code, stores its encrypted session, and syncs chats into a dedicated PostgreSQL schema. ## Before you begin > **Terms and consent matter** > > Telegram API and content-licensing terms restrict using platform data for artificial intelligence except where everyone in a chat has consented. Unofficial clients are monitored more closely, and violations can restrict or ban an account. Review the current Telegram terms before offering this connector. Create your own API credentials at [my.telegram.org](https://my.telegram.org) under **API development tools**. Do not reuse published example credentials; Telegram refuses them. The credentials are optional, but set both or neither. With neither, the bridge still starts, `/health` reports `"telegram": "unconfigured"`, and **Add a connection** explains what to set. Setting both later and restarting the bridge turns Telegram on. ## Configure the bridge Set these values in `.env`: ```bash TELEGRAM_API_ID= TELEGRAM_API_HASH= TELEGRAM_BRIDGE_ADMIN_KEY= TELEGRAM_SESSION_ENCRYPTION_KEY= NUXT_TELEGRAM_URL=http://localhost:8095 NUXT_TELEGRAM_ADMIN_KEY= NUXT_TELEGRAM_DATABASE_URL=postgresql://@localhost:5432/evolution ``` `NUXT_TELEGRAM_DATABASE_URL` can use the owner credential; a restricted reader role is optional (see [Where the data lives](https://www.getautomata.app/docs/connections/telegram#where-the-data-lives)). Until `NUXT_TELEGRAM_URL` and `NUXT_TELEGRAM_ADMIN_KEY` are set, creating a Telegram connection is refused with a message that no Telegram bridge is available. The bridge itself also needs `TELEGRAM_BRIDGE_DATABASE_URL` and refuses to start without it; the development compose file sets it for you. It listens on `TELEGRAM_BRIDGE_PORT`, else `PORT`, else `8095`. On platforms that inject `PORT`, such as Railway, set `TELEGRAM_BRIDGE_PORT` so `NUXT_TELEGRAM_URL` keeps matching. Start the bridge profile and web app: ```bash pnpm services:up:telegram pnpm dev ``` > **Back up the encryption key** > > Every stored session is sealed with `TELEGRAM_SESSION_ENCRYPTION_KEY`. Losing or changing it unlinks every account. A database backup and this key together grant account access. ## Link and sync Create a **Telegram account** connection, choose **Show QR code**, and scan from **Telegram → Settings → Devices → Link Desktop Device**. Two-step verification, when enabled, is sent to Telegram and is not stored by Automata. ## Sync behavior The bridge syncs private chats, bots, groups, supergroups, forum topics, and channels. Live changes continue in real time while a deliberately paced backfill fetches older history. - Backfill stops at `TELEGRAM_BACKFILL_MAX_PER_CHAT` or `TELEGRAM_BACKFILL_DAYS` (365 by default). The day limit caps what is stored, not only how far backfill walks: a chat whose newest message is older than the limit gets no message from the dialog list either. Live messages and the catch-up after a restart are never cut. - On the hosted Free plan, older history is fetched back 90 days. The limit is decided when the account is linked and stored on the session: an account linked under Pro keeps its longer history after a move to Free, and one linked under Free fetches further back only once it is unlinked and linked again. A bring-your-own bridge is never capped. `get-telegram-status` reports the limit in force as `sync.historyDays`. - Broadcast channels are skipped unless `TELEGRAM_BACKFILL_BROADCAST=true`. - Secret chats are device-only and never visible. - Unlinking from Automata deletes synced chats; ending the device session in Telegram keeps them for relinking. **A very busy account is caught up rather than queued.** When live updates arrive faster than they are written (more than 5,000 waiting, or the oldest waiting two minutes), the bridge stops holding channel and supergroup updates in memory and fetches them from Telegram again once it has caught up. Nothing is lost; where Telegram will not go back far enough, the chat is marked as having a gap. Private chats and basic groups are never skipped. `GET /health` on the bridge reports the backlog under `sync`; a backlog that never drains means the database cannot keep up. A client that stops reconnecting on its own, because Telegram was unreachable for longer than the client's retries or stopped answering for two minutes, is rebuilt by the bridge without a restart. The log says `client stopped reconnecting (…); rebuilding`, then `connected again`. ## Where the data lives The bridge uses the same Postgres as Evolution and the web app, in its own `telegram` schema, which it creates on first start. It connects with the owner credential. To give the app less, create a LOGIN-only role, set `TELEGRAM_READER_ROLE` to its name on the bridge, and use that role in `NUXT_TELEGRAM_DATABASE_URL`. The bridge re-grants column-level `SELECT` at every start, never on sessions, access hashes, or phone numbers. The bridge migrates its schema at start, in one transaction. Migration `0003` adds an index on `telegram.messages` and holds a write lock on that table while it builds, so sync writes wait for it. With millions of messages, build it by hand beforehand, without the lock, and the migration skips it: ```sql CREATE INDEX CONCURRENTLY IF NOT EXISTS messages_live_by_session ON telegram.messages (session_id) WHERE deleted_at IS NULL; ``` If that build fails, it leaves an invalid index under the same name. Drop it and build it again before you deploy. ## One bridge process Run one bridge process. Two processes competing for one Telegram session can invalidate it, so keep one replica and no deployment overlap. A second process waits on the session lock and takes over once the first exits. If the bridge's container has a memory limit, set `NODE_OPTIONS=--max-old-space-size=` to about three quarters of it, so Node stops with a heap error in the log rather than the container being killed without one. ## Telegram test servers Set `TELEGRAM_TEST_SERVERS=true` to use Telegram's separate test environment. Test-server accounts are distinct from real accounts, which makes this the safest way to try the connector end to end. ## Bring your own bridge Self-hosted only, like a bring-your-own Evolution server: the hosted service does not offer the option and refuses it. Select **Use my own Telegram bridge** and provide its URL, administrator key, and optionally a database URL for message reads. User-supplied origins pass the same outbound host guard as Evolution servers. Source: https://github.com/ju-li/automata-mcp/blob/main/docs/telegram.md --- # PostgreSQL URL: https://www.getautomata.app/docs/connections/postgresql Section: Connect data sources > Connect a database with a least-privilege role and guarded tools. Automata accepts a PostgreSQL connection string, proves it before saving, and exposes catalog, query, and optional write tools through a connector token. ## Create a database role The database role is the real security boundary. Start with only the schemas and tables the connector requires: ```sql CREATE ROLE automata_reader LOGIN PASSWORD ''; GRANT CONNECT ON DATABASE app TO automata_reader; GRANT USAGE ON SCHEMA public TO automata_reader; GRANT SELECT ON public.customers, public.invoices TO automata_reader; ``` > **Security warning** > > Do not connect as a superuser or database owner. Tool scopes protect against model mistakes, but they are not a boundary against a determined caller holding a leaked token. ## Add the connection Create a **PostgreSQL database** connection and paste the DSN: ```text postgresql://automata_reader:@db.example.com:5432/app?sslmode=require ``` Automata connects before saving. The string must name exactly one host (`host1,host2` is refused), and an `@` or `#` in the user name or password must be percent-encoded (`%40`, `%23`). Rotating the connection string later does not invalidate connector tokens because tokens identify the connection, not the current credential. ## Read and write access New PostgreSQL tokens start with read tools enabled and `run-statement` disabled. For controlled writes, grant the exact `INSERT`, `UPDATE`, or `DELETE` privileges to the role, then explicitly add `run-statement` to the token. ## Query safeguards Every statement is restricted to one extended-protocol statement and checked with `EXPLAIN (FORMAT JSON, VERBOSE)` before execution. - `run-query` accepts `SELECT`, `WITH`, `VALUES`, or `TABLE` in a read-only transaction. - `run-statement` accepts only `INSERT`, `UPDATE`, `DELETE`, or `MERGE`. - DDL, `TRUNCATE`, `COPY`, and `CREATE TABLE AS` are refused. - Functions outside `pg_catalog`, `SECURITY DEFINER` functions, and dangerous built-ins are refused. - Writes over `maxRows` roll back in full. - `UPDATE` and `DELETE` without `WHERE` require `allowWholeTable: true`. - Every table in the plan must be in the token's table allowlist, or the statement is refused before it runs. - `run-query` takes `timeoutMs`, 10 seconds by default and at most 120 seconds; `run-statement` defaults to 15 seconds, at most 60. A query whose client disconnects is cancelled on the server. - Results are capped at 8 MiB per page, including a write's `RETURNING` rows. Past it the call answers `413` and a write rolls back. Cells are clipped at 2,000 characters unless `fullValues: true`. ### What plan checks cannot see The plan describes the statement, not everything it may trigger. Triggers and the bodies of user-defined functions can do more than the plan shows, and a superuser can bypass normal database restrictions. This is why the role, not the token scope, is the real boundary. ## Private network targets The outbound host guard rejects private and loopback destinations by default. Set `NUXT_ALLOW_PRIVATE_TARGETS=true` only for local or single-tenant self-hosting where private connectivity is intentional. Automata always refuses targets that resolve to its own database (`NUXT_DATABASE_URL`), Evolution API, Evolution database, Telegram bridge, or Telegram bridge database, regardless of that setting. The guard runs whenever a connection is opened, and the approved address is pinned so the name cannot be re-resolved elsewhere. The app warns when the connecting role is a superuser. ## Supabase Use the **Session pooler** connection string from **Connect** in the Supabase dashboard, such as `postgresql://postgres.:@aws-0-.pooler.supabase.com:5432/postgres`. Supabase's direct hosts (`db..supabase.co`) resolve only to IPv6, so a deployment without outbound IPv6, such as Railway, fails with `ENETUNREACH`. The create form warns about a direct host but does not refuse it. - Use the Session pooler, not the Transaction pooler on port `6543`, which has not been tested with Automata. - The pooler's user name carries the project ref: `postgres.`, or `automata_reader.` for your own role. - Supabase's IPv4 add-on is the alternative if you want to keep the direct host. Source: https://github.com/ju-li/automata-mcp/blob/main/docs/postgres.md --- # Email (IMAP) URL: https://www.getautomata.app/docs/connections/email Section: Connect data sources > Connect one mailbox over IMAP for reading, search, and unsent drafts. An email connection gives an AI client read access to one mailbox over IMAP: it can list folders, list and search emails, read an email's text, and fetch its attachments. When a token allows it, the client can also save a plain-text draft in the mailbox's Drafts folder for you to send yourself. It cannot send, delete, move, or mark mail. Email needs no extra service: Postgres and the web app are enough. ## Add the connection Choose **Email (IMAP)** under **Add a connection** and fill in the server, port, security, username, and password. Presets fill in the server for Gmail, iCloud, Fastmail, Zoho, and Yahoo. Automata logs in with the credentials before saving anything, so a wrong password fails the create instead of leaving a connection that never syncs. - **Use an app password.** Gmail (with 2-Step Verification on), iCloud, Fastmail, Yahoo, and Zoho issue passwords meant for exactly this, and you can revoke one without changing your main password. - **Microsoft 365 and Outlook.com cannot be connected.** Microsoft no longer accepts passwords over IMAP and requires OAuth, which this connection does not support. - **There is no unencrypted option.** Choose **TLS** (usually port `993`) or **STARTTLS** (usually `143`). STARTTLS is enforced: a server that does not upgrade the connection is refused rather than sent the password in clear. Certificates are verified against the host name. **Change credentials** on the connection's page replaces them, checked the same way. Every connector token keeps working. If the new login is a different mailbox, everything synced from the old one is deleted first. ## What is stored Only headers are synced. Bodies and attachments stay on your mail server and are fetched when a tool asks for them. | Data | Where it comes from | Stored by Automata | | --- | --- | --- | | Folders, sender, recipients, subject, date, read and flagged state, threading ids | Synced every two minutes | Yes, about 1 KB per email | | Attachment names, types, and sizes | Synced from the message structure | Yes | | Email body | Fetched when `read-email` asks | No | | Attachment content | Fetched when `get-email-attachment` asks | No, neither in the database nor on disk | | Searching inside email text | Passed to the mail server's own search | No | Storage stays small however much mail you have. Folders are opened read-only and fetched with `BODY.PEEK`, so **reading an email does not mark it read** in your mail client. Synced headers live in the app database (`app.mail_folders`, `app.mail_messages`, `app.mail_sync_state`) and are deleted with the connection. Like every connection credential, the mailbox password is stored in clear in the app database and its backups. ## Sync behavior - **Every folder except All Mail, Junk, and Trash** is synced. Gmail's All Mail would hold every message a second time. Skipped folders still appear in `list-mail-folders` with `synced: false`, as do folders that cannot hold mail, such as a parent `Archive` above `Archive/2024`. - **New mail** arrives within two minutes, along with changes to read and flagged state. On servers with CONDSTORE the server reports what changed; on others the newest 500 emails per folder are re-checked each run. - **Deleted mail** is removed at least every ten minutes. - **Older mail is backfilled** newest first, 500 emails per folder per run, up to `NUXT_IMAP_BACKFILL_DAYS` (default 365) back and `NUXT_IMAP_BACKFILL_MAX` (default 50,000) emails per folder. A folder that stopped at a limit says so, and the tools tell the client that reaching the end of a listing means "oldest synced email", not "oldest email". - **A folder the server renumbers** (its UIDVALIDITY changes, as when a folder is deleted and recreated) is synced again from scratch. An email id issued before that asks the client to list the folder again instead of returning a different message. - **One email in several folders**, as with Gmail labels, is listed once with every folder it is in. - **An email whose headers cannot be stored**, because of malformed text the database refuses, is listed by date with no subject, sender, or attachments instead of stalling the rest of the folder. Its body is still readable. The dashboard shows when the sync last succeeded, how far each folder has got, and why it is failing if it is. **A refused password stops the sync** until the credentials are changed or **Sync now** is pressed, since retrying a refused login every two minutes is how providers lock accounts. Listings say when the sync is behind, so a client does not present a stale inbox as current. The mail server's replies are size-capped, and connecting plus logging in must finish within 60 seconds. A server that exceeds either fails that run without affecting other connections. The sync runs in the web process and is safe with several replicas: each mailbox is claimed with a lease in the database, so only one replica syncs it at a time. A process that stops mid-sync holds the lease for up to ten minutes, during which **Sync now** reports that a sync is already running. ## Tools and limits | Tool | What it does | | --- | --- | | `get-mailbox-status` | Which mailbox, whether the sync works, when it last ran, and which folders are still backfilling or stopped at a limit | | `list-mail-folders` | Folders in scope, INBOX first, with synced and unread counts | | `list-emails` | One page of headers, newest first, from one folder or every synced folder in scope | | `search-emails` | `from`, `to`, and `subject` against synced headers, and `text` through the mail server's search, combined with AND | | `read-email` | Headers, the body as plain text, and the attachment list | | `get-email-attachment` | One attachment, whole: images as images, text files as text, anything else as an embedded file | | `create-email-draft` | **Write.** Saves an unsent plain-text draft in the Drafts folder | Parameters for each tool are in the [MCP tools reference](https://www.getautomata.app/docs/reference/mcp-tools#email-imap-tools). - **Body search depends on your server.** Most IMAP servers match substrings; Gmail matches whole words with its own search syntax. A search covers at most 20 folders within 25 seconds, and any folder it did not reach is named in the result. - **Bodies are fetched up to 2 MiB.** A longer body is cut there and `read-email` says so. - **Attachments come back whole**, text files included. - **Attachments are capped** at `NUXT_IMAP_ATTACHMENT_MAX_BYTES` (default 10 MiB). A larger file is refused with its size before anything is downloaded, and the cap is enforced again on the bytes received. Only parts the synced message lists as attachments can be fetched. ## Drafts `create-email-draft` writes one message into the mailbox's Drafts folder, flagged as a draft and as read, and does nothing else. **It cannot send mail**: the connection has no SMTP credentials. The draft waits in your mail client until you send, edit, or delete it. - **Off by default.** A new email token starts with the six read tools allowed and `create-email-draft` not. - **The folder is not the client's choice.** It is the folder your server marks as Drafts, or failing that the one folder named "Drafts". A mailbox with neither, or with more than one, is refused rather than guessed at. - **Plain text only, no attachments.** The sender is the login address, so a connection whose username is not an email address cannot save drafts. - **Replies are threaded.** Given the id of the email being answered, the draft carries its `In-Reply-To` and `References` headers, the subject defaults to `Re: …`, and the recipient defaults to the original sender (or its recipients, when you sent the original). The original text is not quoted unless the client includes it. - The draft appears in `list-emails` after the next sync; saving one starts a sync straight away. ## Folder scope An email token is scoped by **folder**, the same way WhatsApp and Telegram tokens are scoped by chat. A token limited to `Clients` lists, searches, and reads only emails filed there: an email that is also in INBOX is reachable through its `Clients` copy only, and asking for a folder outside scope is refused by name. Saving drafts needs the Drafts folder in the allowlist. See [Permissions](https://www.getautomata.app/docs/permissions) for the rest of a token's scope. ## Private network targets The mail server is an address a user chose, so it passes the same outbound guard as a [PostgreSQL connection string](https://www.getautomata.app/docs/connections/postgresql#private-network-targets). Private and loopback addresses are refused unless `NUXT_ALLOW_PRIVATE_TARGETS=true`, this deployment's own backends are always refused, and the address approved is the one dialled. A cached connection's host is re-checked every minute. ## Settings All three are optional and set on the web service. | Variable | Default | Purpose | | --- | --- | --- | | `NUXT_IMAP_BACKFILL_DAYS` | `365` | How far back the header sync reaches, per folder | | `NUXT_IMAP_BACKFILL_MAX` | `50000` | Most emails synced per folder | | `NUXT_IMAP_ATTACHMENT_MAX_BYTES` | `10485760` | Largest attachment `get-email-attachment` fetches | Source: https://github.com/ju-li/automata-mcp/blob/main/docs/imap.md --- # Tokens and permissions URL: https://www.getautomata.app/docs/permissions Section: Control access > Grant only the actions, chats, and tables an AI client needs. A connector token combines an identity, an expiry, one connection, and independent action plus resource scopes. ## Token lifecycle An administrator mints tokens from the connection's dashboard, for themselves or for a member who can reach the connection. Choose a clear label and an expiry of 30 days, 90 days (the default), one year, or never. Automata shows the raw token once and stores only a SHA-256 hash with last-used, expiry, revocation, and scope data. Treat the token like a password. Use one token per client or automation so access can be revoked independently. Each token is held by one person. Anything that takes away that person's access to the connection, such as removing them from the organization, unassigning the connection, or demoting an administrator, revokes the tokens they held on it, and the dashboard asks first. See [Organizations and roles](https://www.getautomata.app/docs/organizations). ## Action scopes A token can expose all tools for its connection kind or a chosen subset. Disabled tools are not registered for the MCP request, and direct attempts to call them fail. WhatsApp and Telegram tokens start with every tool. PostgreSQL and email tokens start read-only: running database statements and saving email drafts must be explicitly enabled. ## Resource allowlists **WhatsApp and Telegram chats:** list tools return only allowed chats. Read and send tools refuse an out-of-scope chat. Unrestricted search is narrowed in the query itself. **Email folders:** listings and searches cover only allowed folders. Reading an email or fetching an attachment from another folder is refused, and saving a draft needs the Drafts folder in the allowlist. **PostgreSQL tables:** catalog tools hide out-of-scope tables. Query and statement tools inspect the query plan before execution and refuse any referenced table outside the allowlist. Names are case-sensitive, and allowing a partitioned table includes its partitions. > **Note** > > Action and resource scopes are independent. A send tool can be enabled while still restricting it to a small chat allowlist. ## Edit, expire, or revoke Scope is loaded on every request. Editing it takes effect immediately without changing the token value or reconnecting the client. Expiry and revocation also take effect immediately, even when the underlying account or database is unavailable. A lost token is replaced, not recovered. ## See which clients use a token A token is a bearer credential, so the same one can sit in Claude.ai, Cursor, and a script at once. Under each token's name, the token list shows the MCP clients that have connected with it, as they name themselves when a session starts, for example `claude-ai 0.1.0 · Cursor 1.7`. Click the line for each client's version, protocol version, user agent, when it was first seen, its last session, and how many sessions it has started. **Last session** is when a client last connected; **Last used** is still the last tool call. The newest 20 clients are kept per token. A client whose name first appears more than a day after the token's first client gets a **New client** label for two weeks. If you don't recognize it, issue the token a new secret. The label is only a hint: a client can call itself anything, so a token without the label isn't proven safe, and no access decision depends on these names. A member sees this for their own tokens, an administrator for every token in the organization. ## Telemetry When an organization's telemetry is on, each tool call made with a token is stored for 30 days with the reason the AI client gave for it. Click a token's **Last used** date to see them. Telemetry is required only on the hosted Free plan; a self-hosted deployment never sends it to us. See [Telemetry](https://www.getautomata.app/docs/telemetry). ## Security boundary Token scopes are strong model guardrails and prevent accidental cross-resource access. They do not replace upstream permissions: - Use a least-privilege PostgreSQL role. - Keep Evolution and Telegram administrator keys server-side. - Use separate connector tokens for unrelated clients. - Revoke tokens that appear in logs, chat, or source control. Source: https://github.com/ju-li/automata-mcp/blob/main/README.md --- # Telemetry URL: https://www.getautomata.app/docs/telemetry Section: Control access > What tool-call telemetry records, and why it never applies to self-hosted deployments. > **Self-hosted deployments never send telemetry to us** > > Telemetry is required only on the **hosted Free plan** at [mcp.getautomata.app](https://mcp.getautomata.app). A self-hosted deployment never sends us tool calls, reasons, usage, or anything else, and never requires telemetry. It is off by default. If an administrator chooses to turn it on, the records stay in that deployment's own database and nowhere else. Telemetry is a 30-day history of the tool calls made with your connector tokens, with the one-line reason the AI client gave for each. It lets an organization see what its connectors are actually used for. ## Who it applies to | Where Automata runs | Telemetry | Where records are stored | | --- | --- | --- | | Hosted, **Free** plan | Always on; cannot be turned off | The hosted service's database | | Hosted, **Pro** plan | Off by default; an administrator can turn it on | The hosted service's database | | **Self-hosted** | Off by default; an administrator can turn it on | Your own database only. Nothing is sent to us | Moving a hosted organization from Free to Pro hands the choice back to its administrators, and telemetry goes back to off unless an administrator turned it on. A Pro subscription that lapses puts the organization back on Free, and telemetry is required again. ## What is recorded While telemetry is on, each tool call is stored with: - the tool's name, for example `search-messages`; - whether the call worked; - the reason the AI client gave, clipped to 300 characters, or nothing if it gave none; - the client's user agent; - the connector token it was made with, and when. Tool arguments, results, message content, and query rows are never recorded. ## The reason argument Every tool takes an optional `reason` argument: one sentence from the model on why it is making the call, for example *"User asked to find invoices from a vendor received last week and total them."* MCP never sends the user's prompt to the server, so this is the closest an organization can get to knowing what its connectors are used for. It is the model's own account, not the prompt. Automata removes `reason` before the tool runs, so it never changes what a tool does. With telemetry off, it is discarded. ## Who can see it Click a connector token's **Last used** date to see its calls, newest first. - A member sees the calls made with their own tokens. - An administrator sees the calls made with every token in the organization. Everyone in the organization can see on the **Organization** page whether telemetry is on, because their calls are recorded too. ## What it is never used for A reason can mention what someone asked about, and a prompt-injected message can make the model write anything there. So reasons are: - kept only in the app database, and shown as plain text; - never written to logs, analytics, or account events; - never returned to an AI client over MCP; - never used for access decisions, limits, or billing. ## Turning it on or off An administrator changes it on the **Organization** page with the **Collect telemetry** box under **Telemetry**. On the hosted Free plan the box is locked on; upgrade to Pro to turn it off. Turning it off stops collection immediately. Calls already stored are deleted when their 30 days are up. ## Related - [See which clients use a token](https://www.getautomata.app/docs/permissions#see-which-clients-use-a-token) lists the MCP clients that connected with each token. It is separate from telemetry and always on. - [Privacy Policy](https://www.getautomata.app/privacy) covers how the hosted service handles this data. Source: https://github.com/ju-li/automata-mcp/blob/main/apps/web/server/utils/tool-calls.ts --- # Organizations and roles URL: https://www.getautomata.app/docs/organizations Section: Control access > Invite teammates and understand who can manage connections and tokens. Connections belong to an organization, so a team can share accounts and databases without sharing a login. ## Organizations Signing up creates your organization, and you become its administrator. Connections, members, and invitations all live inside it. On a self-hosted install, sign-up without an invitation is closed after the first account unless the operator opens it or hands out a sign-up code. See [Who can sign up](https://www.getautomata.app/docs/self-hosting/configuration#who-can-sign-up). Each person manages their own name, email, password, and [two-factor authentication](https://www.getautomata.app/docs/two-factor) from the **Profile** page. A forgotten password is reset with **Forgot password?** on the sign-in page, which mails a link and a code valid for 30 minutes; an administrator can send the same mail to a member without seeing either. See [Password reset](https://www.getautomata.app/docs/self-hosting/configuration#password-reset). When SMTP is configured, a new email address must be confirmed before it replaces the old one. Every connector token is still bound to exactly one connection, regardless of who created it. ## Invite teammates Administrators invite people from the **Organization** page. Each invitation names an email address and produces a link that only an account with that address can accept: - With SMTP configured, **Send invite email** mails the link to the recipient, and **Resend email** on a pending invitation mails a new link that replaces the old one. - Without SMTP, the link is still created and can be copied and sent by hand. Invitation links work whatever the sign-up mode. With email verification on, the invited person still confirms their address with a mailed code, because holding a forwarded link is not the same as owning the inbox, and must finish the signup in the browser where they started it. See [Configuration](https://www.getautomata.app/docs/self-hosting/configuration#alerts-and-mail) for the mail settings. ## Roles | Role | Can do | | --- | --- | | Administrator | Manage members and invitations, send a member a password reset, create, assign, and remove connections, mint and edit connector tokens for anyone who can reach a connection, turn [telemetry](https://www.getautomata.app/docs/telemetry) on or off, and see every token's clients and tool calls | | Member | Use the connections assigned to them, see only their own connector tokens, revoke a token or rotate its secret but never widen its scope, and see those tokens' clients and tool calls | Roles control the management UI only. They never widen a connector token: a token reaches exactly the actions, chats, and tables in its own scope. ## Offboarding Removing a member takes away their access to the dashboard and revokes every connector token they held. Unassigning a connection from a member, or demoting an administrator who has no assignment on a connection, likewise revokes their tokens on it. The dashboard asks before doing so. Removal does not delete the account: the person keeps their login, outside the organization. See [Tokens and permissions](https://www.getautomata.app/docs/permissions#token-lifecycle). Source: https://github.com/ju-li/automata-mcp/blob/main/docs/using-automata.md --- # Two-factor authentication URL: https://www.getautomata.app/docs/two-factor Section: Control access > Protect web sign-in with authenticator apps, passkeys, and recovery codes. Anyone can turn on a second sign-in step for their own account under **Profile → Two-factor authentication**. They can use either or both of: - **An authenticator app.** Google Authenticator, 1Password, Authy, or any app that reads an `otpauth://` QR code: 6 digits, 30 seconds, SHA-1. - **Passkeys and security keys.** WebAuthn covers platform passkeys such as Touch ID, Windows Hello, or a phone, and hardware keys such as a YubiKey. Adding the first factor also mints **ten recovery codes**. They are shown once, and each signs in once. Regenerating them from the same dialog invalidates every old code. It is opt-in per person. An organization administrator cannot require it, and cannot see or reset another member's factors. ## What it covers Only the web sign-in. **Connector tokens are not affected.** An MCP token is a credential in its own right, and Claude cannot answer a second-factor prompt. To cut off a leaked token, rotate or revoke it from its connection's page. See [Tokens and permissions](https://www.getautomata.app/docs/permissions#edit-expire-or-revoke). A passkey is only a second factor. You still type your password first; there is no passwordless sign-in. ## How sign-in works ### Step 1: Password first Email and password, as before. If they are right and the account has a factor, no session starts yet. The browser gets a five-minute, HTTP-only cookie scoped to the login route. An account has one pending sign-in at a time; entering the password again cancels the earlier one. ### Step 2: Then a factor The person answers with an authenticator code, a passkey, or a recovery code. A right answer starts the normal session. Each answer uses one of five attempts as it arrives, before it is checked, so sending many at once gets no more than five. After that, the password has to be entered again. Each authenticator code works once, so a code seen over someone's shoulder cannot be replayed. Throttles refuse further attempts for a while after ten failures in fifteen minutes: per account for the second step, and per email and per client address for the password. They are kept in memory, per process. Adding or removing a factor, or regenerating recovery codes, asks for the current password. That check, also used to change your email or password, has its own per-account throttle of ten wrong answers in fifteen minutes, so a stolen session cannot guess the password there. Turning two-factor on signs out every other session of that account; removing the last factor deletes the recovery codes. An authenticator app must be confirmed with a code within 15 minutes of starting setup, in the same session, and after five wrong codes setup starts over. ## Passkeys and the app URL A passkey is bound to a domain. Automata takes that domain from `NUXT_PUBLIC_APP_URL`, never from the request, so: - **`NUXT_PUBLIC_APP_URL` must be the address people actually open.** If it is wrong, the browser refuses the passkey prompt. If it is unset, passkeys are hidden and the settings say why. Authenticator apps still work. - **Moving to another domain makes every registered passkey unusable.** Authenticator apps and recovery codes are unaffected. Before a domain move, make sure people have one of those, or warn them to re-add their passkeys. - Browsers allow WebAuthn only on HTTPS or `localhost`. Plain HTTP on a LAN address does not offer passkeys. ## Recovering a lost factor There is no self-service or administrator reset. A [password reset](https://www.getautomata.app/docs/self-hosting/configuration#password-reset) does not help: it changes only the password, and the next sign-in still asks for the factor. An operator with database access can turn two-factor off for one account; the person can then sign in with their password and enroll again: ```sql BEGIN; DELETE FROM app.totp_credentials WHERE user_id = (SELECT id FROM app.users WHERE email = 'person@example.com'); DELETE FROM app.webauthn_credentials WHERE user_id = (SELECT id FROM app.users WHERE email = 'person@example.com'); DELETE FROM app.recovery_codes WHERE user_id = (SELECT id FROM app.users WHERE email = 'person@example.com'); COMMIT; ``` > **Verify who is asking** > > This removes protection the person chose to turn on. Confirm their identity through another channel before running it. ## Storage | Table | Holds | | --- | --- | | `totp_credentials` | The authenticator secret, the last step a code was accepted for, and `confirmed_at`; until confirmed, the session that started setup and the wrong-code count | | `webauthn_credentials` | Credential ID, public key, signature counter, transports, name, and `last_used_at` | | `recovery_codes` | SHA-256 of each code and `used_at` | | `mfa_challenges` | Pending sign-ins and passkey registrations, short-lived | Authenticator secrets are not encrypted, like the connection credentials in the `app` schema. Someone holding a database backup can generate codes, but still needs the password, which is stored only as an scrypt hash. Passkey public keys are not secret. Source: https://github.com/ju-li/automata-mcp/blob/main/docs/two-factor.md --- # MCP clients URL: https://www.getautomata.app/docs/clients Section: Control access > Use header-based or path-token authentication from supported MCP clients. Automata serves remote MCP over HTTP and supports both bearer-header and path-token authentication. ## Step-by-step guides Prefer a full walkthrough? These guides cover pairing WhatsApp, choosing chats, and adding Automata to a specific AI client. - [Connect Claude to WhatsApp](https://www.getautomata.app/blog/how-to-connect-whatsapp-to-claude): Claude.ai, Claude Desktop, and Claude Code, with a custom connector. - [Connect ChatGPT to WhatsApp](https://www.getautomata.app/blog/how-to-connect-chatgpt-to-whatsapp): Use ChatGPT with your own WhatsApp chats, using a path-token URL. - [Connect Grok Bot to WhatsApp](https://www.getautomata.app/blog/how-to-connect-grok-bot-to-whatsapp): Add Automata as a custom MCP server with a bearer API key. - [Connect Meta Muse to WhatsApp](https://www.getautomata.app/blog/how-to-connect-meta-muse-to-whatsapp): Let Muse read and reply to your chats, not just receive your messages. - [Connect Cursor to WhatsApp](https://www.getautomata.app/blog/how-to-connect-cursor-to-whatsapp): Add the server to mcp.json and use WhatsApp from the editor. ## Choose an endpoint format Prefer the bearer-header endpoint when the client supports custom headers: ```text Endpoint: https://your-automata.example.com/mcp Authorization: Bearer ``` Use a path token only for clients that cannot attach an authorization header: ```text https://your-automata.example.com/mcp/ ``` > **Important** > > URLs are commonly logged by clients and proxies. Automata redacts path tokens from its own logs, but header authentication still reduces accidental exposure across the rest of the request path. ## Claude.ai In Claude.ai, open **Settings → Customize → Connectors → Add → Add custom connector**. Enter the name and connector URL shown in the Automata connection dashboard. Use the endpoint format supported by the current connector form. For a full walkthrough, see [how to connect Claude to WhatsApp](https://www.getautomata.app/blog/how-to-connect-whatsapp-to-claude). ## Claude Code For a path-token endpoint: ```bash claude mcp add --transport http automata \ https://your-automata.example.com/mcp/ ``` Use a distinct connector name for each Automata connection, such as `support-whatsapp` or `reporting-db`. ## Bearer-header clients Configure the MCP URL as `/mcp` and add: ```text Authorization: Bearer ``` Automata does not use a browser cookie for MCP authentication. Missing, invalid, expired, or revoked connector tokens receive `401`. ## MCP Inspector Launch the official inspector from the source repository: ```bash pnpm dlx @modelcontextprotocol/inspector ``` Point it to `http://localhost:3000/mcp/`, or use `http://localhost:3000/mcp` with the bearer header. Begin with a read-only status tool before testing write access. Source: https://github.com/ju-li/automata-mcp/blob/main/README.md --- # Single container URL: https://www.getautomata.app/docs/self-hosting/single-container Section: Self-host and operate > Run the whole stack with one docker run and one volume. The all-in-one image runs the whole stack in one container: Postgres, Redis, Evolution API, the web app, and the Telegram bridge share one image and one volume, and every secret is generated on first boot. ## Run it ```bash docker run -d --name automata --restart unless-stopped --stop-timeout 30 \ -p 3000:3000 -v automata_data:/data \ -e NUXT_PUBLIC_APP_URL=https://automata.example.com \ ghcr.io/ju-li/automata-mcp-aio:1 ``` Open the app on port `3000` and sign up. The first account creates an organization and becomes its administrator. First boot creates the database and runs Evolution's and the app's migrations; `docker logs -f automata` shows each service starting. `NUXT_PUBLIC_APP_URL` is the address people will open, and every connector URL is built from it. Left out, it defaults to `http://localhost:3000`, which is fine for trying it and wrong for anything Claude has to reach. `--stop-timeout 30` gives the services time to stop in order: the web app, Evolution, and the bridge first, Postgres last. Docker's default of 10 seconds can cut that short. ## What is inside | Process | Version | Listens on (inside the container) | | --- | --- | --- | | Web app | This release | `3000`, or `$PORT`. The only port to publish | | Postgres | 16 | Loopback, port `5432` | | Redis | 8.2.9 | `127.0.0.1:6379` | | Evolution API | v2.3.7 | `18080` | | Telegram bridge | This release | `18095` | The image is built on Evolution's own image, so Evolution runs exactly as it ships. [s6-overlay](https://github.com/just-containers/s6-overlay) starts the rest in order, restarts what crashes, and stops the container if a start-up step fails. The image is large, about 2 GB, most of it Evolution's. `PORT` may not be one of the internal ports above; the container refuses to start and says so. ## Everything lives in /data | Path | Holds | | --- | --- | | `/data/postgres` | The database: every user, connection, stored credential, and synced message | | `/data/redis` | Evolution's cache | | `/data/evolution` | Evolution's session files | | `/data/secrets` | The generated secrets, one file each | On first boot the container generates `POSTGRES_PASSWORD`, `WEBHOOK_SECRET`, `EVOLUTION_API_KEY`, `TELEGRAM_BRIDGE_ADMIN_KEY`, and `TELEGRAM_SESSION_ENCRYPTION_KEY` into `/data/secrets`. An environment variable with one of those names overrides the stored file, which is left as it was. Set them only if you manage secrets elsewhere, keep them URL-safe (`openssl rand -hex 32`), and never change `TELEGRAM_SESSION_ENCRYPTION_KEY` after the first boot. > **Back up the whole volume** > > Losing `TELEGRAM_SESSION_ENCRYPTION_KEY` unlinks every Telegram account. Losing the volume loses everything, and every WhatsApp number needs its QR code scanned again. `docker volume rm automata_data` is the single-container equivalent of `docker compose down -v`. `docker rm` on its own is safe. ## Options Everything is optional. The names are the ones `.env.selfhost.example` uses, so a filled-in copy works as `docker run --env-file .env …`; blank values are ignored. | Variable | For | | --- | --- | | `NUXT_PUBLIC_APP_URL` | The public address, as above | | `TELEGRAM_API_ID`, `TELEGRAM_API_HASH` | Telegram, from `my.telegram.org`. Both or neither. Without them the app tells an administrator what to set. See [Telegram](https://www.getautomata.app/docs/connections/telegram) | | `SMTP_HOST`, `SMTP_PORT`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `SMTP_TLS`, `MAIL_FROM`, `MAIL_FROM_NAME` | [Connection alerts](https://www.getautomata.app/docs/alerts), invitation emails, email verification, and [password reset](https://www.getautomata.app/docs/self-hosting/configuration#password-reset). Without `SMTP_HOST`, verification is off (unless `EMAIL_VERIFICATION=required`) and passwords cannot be reset from the app | | `OPS_ALERT_EMAIL` | Where reconnect loops and stuck sessions on this container's Evolution are reported. See [Connection alerts](https://www.getautomata.app/docs/alerts). Needs SMTP | | `NUXT_ALLOW_PRIVATE_TARGETS` | `true` lets connections reach private and loopback addresses. See below | | `SIGNUP`, `SIGNUP_CODE` | Who can create an account. See [Who can sign up](https://www.getautomata.app/docs/self-hosting/configuration#who-can-sign-up) | | `EMAIL_VERIFICATION` | `auto`, `required`, or `off`. See [Email verification](https://www.getautomata.app/docs/self-hosting/configuration#email-verification) | | `MCP_RATE_TOKEN_PER_MINUTE`, `MCP_RATE_ORG_PER_MINUTE`, `MCP_RATE_EXPENSIVE_PER_MINUTE`, `REDIS_URL` | [MCP rate limits](https://www.getautomata.app/docs/self-hosting/configuration#mcp-rate-limits) and where their counts live | | `PORT` | The web app's port inside the container. Defaults to `3000` | > **Private targets reach more here** > > Every backend runs on the container's loopback. The app still refuses its own Postgres, Evolution, and bridge by address and port, but not Redis on `127.0.0.1:6379`, so turn on `NUXT_ALLOW_PRIVATE_TARGETS` only where everyone with an account is trusted. ## Updating ```bash docker pull ghcr.io/ju-li/automata-mcp-aio:1 docker rm -f automata docker run -d --name automata … # the same command as before ``` Removing the container and running it again with the same volume is safe. Migrations run at boot and only go forward, so moving to a new major is a deliberate edit of the tag. `:1` gets a release only once it has been tested and promoted; `:beta` gets each one as soon as it is published, for anyone who wants to try it early. See [Releases](https://www.getautomata.app/docs/self-hosting/releases). ## When to use Compose instead Choose [Docker Compose](https://www.getautomata.app/docs/self-hosting/docker-compose) when you want to restart, upgrade, or back up one service without the others, put Postgres on its own disk, or leave out WhatsApp or Telegram entirely. The single container always runs all of them. Source: https://github.com/ju-li/automata-mcp/blob/main/docs/self-hosting.md --- # Docker Compose URL: https://www.getautomata.app/docs/self-hosting/docker-compose Section: Self-host and operate > Self-host the published images on any Docker host. Run Automata on any machine with Docker using the published images, one container per service. Unlike the [local development stack](https://www.getautomata.app/docs/self-hosting/local), the web app runs in a container too, so nothing is built or installed on the host. For a single `docker run` with everything in one image, see [Single container](https://www.getautomata.app/docs/self-hosting/single-container). ## Start it ```bash git clone https://github.com/ju-li/automata-mcp.git && cd automata-mcp cp .env.selfhost.example .env # fill in the generated secrets and your public URL docker compose up -d ``` Open the app at `NUXT_PUBLIC_APP_URL` and sign up. The first account becomes the administrator; sign-up is invite-only after that unless you change `SIGNUP`. See [Who can sign up](https://www.getautomata.app/docs/self-hosting/configuration#who-can-sign-up). The web service listens on `WEB_PORT`; put a TLS-terminating reverse proxy in front of it before exposing it to the internet. Add connector profiles only when you need them: ```bash docker compose --profile whatsapp up -d docker compose --profile telegram up -d ``` A PostgreSQL-only deployment needs just the default services. ## What it runs | Service | Image | Started by | | --- | --- | --- | | Postgres | Official Postgres image | Always | | Web | `ghcr.io/ju-li/automata-mcp-web:1` | Always | | Redis and Evolution API | `redis:8.2.9-alpine`, `evoapicloud/evolution-api:v2.3.7` | `whatsapp` profile | | Telegram bridge | `ghcr.io/ju-li/automata-mcp-telegram-bridge:1` | `telegram` profile | Every secret in `.env.selfhost.example` must be generated before first start. Mail is optional: with the SMTP host empty, connection alerts and invitation emails are not delivered, although an invitation link can still be copied by hand. Email verification also stays off, and passwords cannot be reset from the app. See [Connection alerts](https://www.getautomata.app/docs/alerts) and [Password reset](https://www.getautomata.app/docs/self-hosting/configuration#password-reset). Set `OPS_ALERT_EMAIL` to have reconnect loops and stuck sessions on your Evolution server reported to you. The `.env` also takes the optional [MCP rate limits](https://www.getautomata.app/docs/self-hosting/configuration#mcp-rate-limits), `MCP_RATE_TOKEN_PER_MINUTE`, `MCP_RATE_ORG_PER_MINUTE`, and `MCP_RATE_EXPENSIVE_PER_MINUTE`, and `REDIS_URL` to share their counts across web replicas. If you point `REDIS_URL` at the bundled `redis` service, use a database of its own, such as `redis://redis:6379/1`. ## Updating The images are pinned to the major tag `:1`, so a pull stays within compatible releases: ```bash docker compose pull docker compose up -d ``` A release reaches `:1` only once it has been tested and promoted. To run each release as soon as it is published, use `:beta` instead. The web service applies its own schema migrations at startup. Migrations only move forward, so take a Postgres backup before a major upgrade or before trying a beta. See [Releases](https://www.getautomata.app/docs/self-hosting/releases) for tags and versioning. ## Protect pairings and data > **Never use down -v on a real deployment** > > `docker compose down -v` deletes volumes, including the Evolution instances volume that holds every WhatsApp pairing and the Postgres volume that holds every user, connection, and token. Use `docker compose down` for a routine stop. Back up Postgres regularly and restrict access to the backups, because they hold every stored connection credential. Keep `TELEGRAM_SESSION_ENCRYPTION_KEY` somewhere other than the backup, and never rotate it casually: changing it unlinks every Telegram account. ## The app database The web app creates and migrates its own `app` schema in the bundled Postgres on first boot, so there is no separate admin account or migration step. It holds users, sessions, organizations, connections, invitations, connector-token hashes, the MCP clients seen on each token, [telemetry](https://www.getautomata.app/docs/telemetry) tool calls (kept 30 days), last-known WhatsApp group names, pending email verifications and password resets, synced email headers (never bodies), and [two-factor](https://www.getautomata.app/docs/two-factor) credentials. Connection credentials in this schema are not encrypted at rest. Treat the database, its volume, and its backups as holding every connected account. ## Coming from PocketBase Earlier versions stored users and connections in PocketBase. Deploy commit `2e1872e` first; it imports every user, password, connection, and token into Postgres on first boot. Once the import is done, remove the PocketBase service and its volume. Source: https://github.com/ju-li/automata-mcp/blob/main/docs/self-hosting.md --- # Local deployment URL: https://www.getautomata.app/docs/self-hosting/local Section: Self-host and operate > Run the core, WhatsApp, or Telegram service profiles locally. The development compose file runs supporting services only. Nuxt stays on the host for hot module replacement and listens on port `3000`. ## Service profiles | Command | Services | Use for | | --- | --- | --- | | `pnpm services:up` | Postgres | PostgreSQL connectors | | `pnpm services:up:whatsapp` | Postgres, Redis, Evolution | WhatsApp connectors | | `pnpm services:up:telegram` | Postgres, Telegram bridge | Telegram connectors | | `pnpm services:down` | All enabled profiles | Routine shutdown | | `pnpm services:logs` | All enabled profiles | Combined service logs | | `pnpm services:ps` | All enabled profiles | Service status | After bringing up services, run `pnpm dev` on the host. > **Note** > > This stack is for development: Nuxt runs on the host and the images are not used. To run Automata on a server without building anything, use [Docker Compose](https://www.getautomata.app/docs/self-hosting/docker-compose). ## One Postgres, three schemas | Schema | Owner | Holds | | --- | --- | --- | | `app` | Web | Users, sessions, organizations, connections, invitations, token hashes | | `public` | Evolution | WhatsApp instances, chats, and messages | | `telegram` | Telegram bridge | Encrypted sessions and synced chats | The web app applies its migrations at startup under an advisory lock. To start over locally, drop the schema; the next boot recreates it: ```bash psql postgres://evolution:change-me@localhost:5432/evolution -c 'DROP SCHEMA app CASCADE' ``` ## Local topology | From | To | Address | | --- | --- | --- | | Nuxt | Postgres (app and messages) | `localhost:5432` | | Nuxt | Evolution | `http://localhost:8080` | | Nuxt | Telegram bridge | `http://localhost:8095` | | Evolution | Nuxt webhook | `http://host.docker.internal:3000/api/webhook/evolution` | | Telegram bridge | Nuxt webhook | `http://host.docker.internal:3000/api/webhook/telegram` | ## Ports and webhooks Postgres `5432`, Redis `6379`, Evolution `8080`, and the Telegram bridge `8095` bind to loopback in the development compose file. Nuxt listens on `0.0.0.0` so containers can reach its webhook. Set `WEBHOOK_URL` and `NUXT_WEBHOOK_URL` to the same Evolution callback. Set `NUXT_WEBHOOK_SECRET` so per-instance webhook requests carry and verify `x-webhook-secret`. ## Testing mail locally Run a mail catcher and point `NUXT_SMTP_HOST` at it: ```bash docker run -d --name wamcp-mailpit -p 1025:1025 -p 8025:8025 axllent/mailpit:v1.21 # .env: NUXT_SMTP_HOST=localhost, NUXT_SMTP_PORT=1025, NUXT_MAIL_FROM=alerts@automata.test ``` Read sent mail at `http://localhost:8025`. See [Connection alerts](https://www.getautomata.app/docs/alerts) for how detection works. Trigger the alert sweep by hand with `curl -X POST http://localhost:3000/_nitro/tasks/alerts:sweep`. ## Linux firewall On Linux with `ufw`, container-to-host webhook traffic enters the host input chain and may time out. The compose subnet is pinned so it can be narrowly allowed: ```bash sudo ufw allow from 172.31.250.0/24 to 172.17.0.1 \ port 3000 proto tcp comment 'automata webhooks' ``` Verify from the Evolution container: ```bash docker compose -f docker-compose.dev.yml exec evolution \ wget -T 5 -qO- --post-data='{"event":"ping"}' \ --header='content-type: application/json' \ http://host.docker.internal:3000/api/webhook/evolution ``` A response of `{"ok":true}` confirms the path. If you set `NUXT_WEBHOOK_SECRET`, add `--header='x-webhook-secret: '`, or the answer is a `401`, which still proves the path is open. A timeout usually means the firewall rule or subnet is wrong. > **Important** > > On an untrusted network, binding Nuxt to `0.0.0.0` can expose port `3000` if the host firewall does not block LAN traffic. Source: https://github.com/ju-li/automata-mcp/blob/main/docker-compose.dev.yml --- # Railway deployment URL: https://www.getautomata.app/docs/self-hosting/railway Section: Self-host and operate > Deploy each service with the right roots, volumes, ports, and replica count. A complete deployment uses up to five services. Provision only the connector infrastructure you need. [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/automata-or-whatsapp-telegram-sql-mcp?referralCode=HxrUPo&utm_medium=integration&utm_source=template&utm_campaign=generic) Railway's template provisions the service matrix below automatically, including volumes. You still need to fill in the environment variables described under [Environment wiring](https://www.getautomata.app/docs/self-hosting/railway#environment-wiring). ## Service matrix | Service | Source | Target port | Persistent volume | | --- | --- | --- | --- | | Postgres | Railway template | Managed | Managed | | Redis *(WhatsApp only)* | Railway template | Managed | Managed | | Evolution *(WhatsApp only)* | `evoapicloud/evolution-api:v2.3.7` | `8080` | `/evolution/instances` | | Telegram bridge *(Telegram only)* | `ghcr.io/ju-li/automata-mcp-telegram-bridge:1` | `8095` | None | | Web | `ghcr.io/ju-li/automata-mcp-web:1` | Railway assigned | None | Postgres is always required because it holds the app's own tables. A deployment serving only PostgreSQL connections needs just Postgres and web. ## Published images The web app and Telegram bridge are published to GHCR on every release for `linux/amd64`. A release is first tagged `X.Y.Z`, `beta`, and `sha-`; once tested, it is promoted to `X.Y`, `X`, and `latest`. See [Releases](https://www.getautomata.app/docs/self-hosting/releases) for upgrade guidance. > **Pin the major tag** > > Point services at `:1`, not `:latest`. The web service migrates its schema at boot and migrations only move forward, so a major release should never reach a deployment by accident on its next restart. To run unreleased code, build from the repository instead. Keep the root directory at `/` and set the Dockerfile path to `apps/web/Dockerfile` or `/apps/telegram-bridge/Dockerfile`. A root directory of `/apps/telegram-bridge` fails with `"/pnpm-workspace.yaml": not found`. ## Volumes and ports Attach `/evolution/instances` to Evolution in its service settings. Railway rejects Dockerfile `VOLUME` instructions, so no image declares one and nothing warns you if it is missing. The Telegram bridge needs no volume; its sessions and chats live in Postgres. Railway injects `PORT` into every service, so pin the internal ports explicitly: ```text evolution: SERVER_PORT=8080 telegram-bridge: TELEGRAM_BRIDGE_PORT=8095 web: Railway-assigned public PORT ``` > **The Evolution volume is mandatory** > > Without it, every deploy unpairs all WhatsApp accounts. Users, connections, and tokens live in Postgres, so back that up too: its backups hold every stored credential. ## Environment wiring Use Railway references such as `${{Service.VAR}}` for shared secrets and database URLs, so rotated values propagate. ```text NUXT_DATABASE_URL=${{Postgres.DATABASE_URL}} NUXT_PUBLIC_APP_URL=https:// # WhatsApp only NUXT_EVOLUTION_URL=http://evolution.railway.internal:8080 NUXT_EVOLUTION_ADMIN_KEY=${{evolution.AUTHENTICATION_API_KEY}} NUXT_EVOLUTION_DATABASE_URL=${{Postgres.DATABASE_URL}} NUXT_WEBHOOK_URL=https:///api/webhook/evolution NUXT_WEBHOOK_SECRET= # Telegram only NUXT_TELEGRAM_URL=http://.railway.internal:8095 NUXT_TELEGRAM_ADMIN_KEY=${{telegram-bridge.TELEGRAM_BRIDGE_ADMIN_KEY}} NUXT_TELEGRAM_DATABASE_URL=${{Postgres.DATABASE_URL}} ``` ## Evolution and bridge variables - Set Evolution `WEBHOOK_GLOBAL_ENABLED=false`. Automata registers webhooks per connection, and a global webhook would deliver every event twice. - Keep the `DATABASE_SAVE_DATA_*` flags on, including `DATABASE_SAVE_DATA_HISTORIC`, or message tools return nothing. - On the bridge, set `TELEGRAM_BRIDGE_DATABASE_URL=${{Postgres.DATABASE_URL}}` (it refuses to start without it), `TELEGRAM_BRIDGE_ADMIN_KEY`, and `TELEGRAM_SESSION_ENCRYPTION_KEY`. - `TELEGRAM_API_ID` and `TELEGRAM_API_HASH` may stay empty until you need Telegram. Setting both later redeploys the bridge and enables it. - `NUXT_TELEGRAM_WEBHOOK_URL` is not needed; the bridge posts to `NUXT_PUBLIC_APP_URL` + `/api/webhook/telegram`. - If the bridge has a memory limit, set `NODE_OPTIONS=--max-old-space-size=` to about three quarters of it, so Node stops with a heap error in the log rather than the container being killed without one. ### Web mail and network settings - Configure `NUXT_SMTP_*` and `NUXT_MAIL_FROM` on web to deliver [alerts](https://www.getautomata.app/docs/alerts), invitations, verification codes, and password resets. Without SMTP, **Forgot password?** is unavailable; see [Password reset](https://www.getautomata.app/docs/self-hosting/configuration#password-reset). - Set `NUXT_OPS_ALERT_EMAIL` to your own address to be mailed at once when a WhatsApp connection on this Evolution falls into a reconnect loop or gets stuck with its socket dead. See [Connection alerts](https://www.getautomata.app/docs/alerts). - Leave `NUXT_ALLOW_PRIVATE_TARGETS` unset. - [MCP rate limits](https://www.getautomata.app/docs/self-hosting/configuration#mcp-rate-limits) are on by default. To share them across web replicas, set `NUXT_REDIS_URL` to a Redis database number of its own, such as `redis://…/1`; Evolution's cache uses `/6`. - Sign-up is invite-only unless you say otherwise: the first account gets through and everyone after joins by invitation. Set `NUXT_SIGNUP_CODE` to let holders of that code create their own organization (the first account then needs it too), or `NUXT_SIGNUP=open` to let anyone who finds the domain sign up. See [Who can sign up](https://www.getautomata.app/docs/self-hosting/configuration#who-can-sign-up). - Once SMTP is configured, new signups and email changes are verified by email. See [Email verification](https://www.getautomata.app/docs/self-hosting/configuration#email-verification). Keep the Telegram bridge and web app at one replica, and leave `RAILWAY_DEPLOYMENT_OVERLAP_SECONDS` unset on the bridge. Multiple bridge processes compete for sessions; multiple web replicas run duplicate alert sweeps. ## Postgres sizing Railway's Postgres template starts with stock settings sized for a small machine: `shared_buffers` 128 MB and `max_connections` 500. The hosted service runs with these instead, set once as a superuser (`railway connect Postgres`): ```sql ALTER SYSTEM SET shared_buffers = '2GB'; ALTER SYSTEM SET effective_cache_size = '6GB'; ALTER SYSTEM SET max_connections = 150; ``` `ALTER SYSTEM` writes `postgresql.auto.conf` on the service's volume, so the settings survive redeploys. `shared_buffers` and `max_connections` take effect only at the next restart of the Postgres service; check with `SHOW` afterwards, and undo one with `ALTER SYSTEM RESET ;` and a restart. - **`max_connections` 150, not 100.** Count the connection budget before lowering it. Each process's pool ceiling: web 18, Telegram bridge 11, and Evolution Prisma's default of `2 × vCPU + 1`. Add each PostgreSQL connection pointed at this database (up to 3 each), double whichever service is mid-deploy, and multiply by replicas. - **`work_mem` stays at 16 MB.** Temp files come from full scans of `"Message"`; the [Evolution indexes](https://www.getautomata.app/docs/connections/whatsapp#indexes-on-evolutions-tables) fix those, a larger `work_mem` would not. - **The memory graph is mostly file cache.** Railway counts the kernel's cache of the data files as the container's memory, so the graph follows the size of `"Message"`, not what Postgres needs. That cache is reclaimable. A memory limit on the service, or reading less of `"Message"`, brings it down. > **Restart Evolution after restarting Postgres** > > Evolution's writes fail while Postgres is down, which can leave a WhatsApp session [stuck with its socket dead](https://www.getautomata.app/docs/connections/whatsapp#stuck-with-the-socket-dead): it reads connected, but nothing is sent or received. Restart the Evolution service once Postgres is back; every account reconnects without a QR scan. This includes restarts Railway makes itself and the one `shared_buffers` needs. ## First-run checks 1. Confirm web logs include `[app-db] applied migrations`. No manual schema step is needed. 2. Open the web domain and create an account. The first account becomes the administrator; invite everyone else from the Organization page. 3. Create one connection and call a read-only status tool. 4. Confirm connector URLs use the correct public origin. ## Coming from PocketBase Earlier versions stored users and connections in PocketBase. Deploy commit `2e1872e` first; it imports every user, password, connection, and token into Postgres on first boot. Once that import is done, delete the PocketBase service and its volume. Source: https://github.com/ju-li/automata-mcp/blob/main/README.md --- # Configuration URL: https://www.getautomata.app/docs/self-hosting/configuration Section: Self-host and operate > Reference the environment variables used by each service. Use the source `.env.example` for the complete comments and defaults. This page identifies the major boundaries and values required by each deployment shape. ## Core application | Variable | Required | Purpose | | --- | --- | --- | | `NUXT_DATABASE_URL` | Yes | Postgres owner URL. The app creates and migrates its own `app` schema here at boot | | `NUXT_PUBLIC_APP_URL` | Yes | Public origin used to build connector URLs, links in invitation, verification, password-reset, and alert emails, and the passkey relying-party ID | | `NUXT_ALLOW_PRIVATE_TARGETS` | No | Permit user-supplied private targets in trusted single-tenant installs. Defaults to off | | `NUXT_SIGNUP` | No | `open` or `invite-only`. Defaults to invite-only. See [Who can sign up](https://www.getautomata.app/docs/self-hosting/configuration#who-can-sign-up) | | `NUXT_SIGNUP_CODE` | No | Invite-only mode: a shared code that lets its holder create their own organization | | `NUXT_EMAIL_VERIFICATION` | No | `auto`, `required`, or `off`. Defaults to `auto`. See [Email verification](https://www.getautomata.app/docs/self-hosting/configuration#email-verification) | | `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` | Local compose | Values for the bundled Postgres container | The `app` schema holds users, sessions, organizations, connections, invitations, connector-token hashes, and [two-factor](https://www.getautomata.app/docs/two-factor) credentials. Connection credentials in it are not encrypted, so treat this database and its backups as holding every connected account. ## WhatsApp Everything in this section is optional. Without a default Evolution server, users can still create WhatsApp connections by supplying their own server URL and key. | Variable | Required when enabled | Purpose | | --- | --- | --- | | `EVOLUTION_API_KEY` | For bundled Evolution | Global Evolution administrator key | | `NUXT_EVOLUTION_URL` | For a default server | Evolution origin reached by web | | `NUXT_EVOLUTION_ADMIN_KEY` | For a default server | Must match `EVOLUTION_API_KEY` | | `NUXT_EVOLUTION_DATABASE_URL` | For reads and search | The Evolution Postgres. Also used for the chat listing, connection status, and reconnect-loop and stuck-session detection. The owner URL works; a SELECT-only role with the [documented grants](https://www.getautomata.app/docs/connections/whatsapp#what-you-need) is optional hardening | | `WEBHOOK_URL` | Local compose | Compose fallback webhook URL | | `NUXT_WEBHOOK_URL` | Yes for fast Evolution alerts | Per-connection webhook registered by web | | `NUXT_WEBHOOK_SECRET` | Strongly recommended | Shared `x-webhook-secret` value, also used by Telegram webhooks | Keep Evolution's `DATABASE_SAVE_DATA_HISTORIC=true`, or the history sent at pairing is dropped. Full history is imported only for connections created with **Import full history** ticked; see [Message history](https://www.getautomata.app/docs/connections/whatsapp#message-history). On a busy deployment, create the optional [indexes on Evolution's tables](https://www.getautomata.app/docs/connections/whatsapp#indexes-on-evolutions-tables). ## Telegram | Variable | Required when enabled | Purpose | | --- | --- | --- | | `TELEGRAM_API_ID`, `TELEGRAM_API_HASH` | Both or neither | Credentials from `my.telegram.org`. With neither, the bridge starts and reports Telegram as unconfigured | | `TELEGRAM_BRIDGE_ADMIN_KEY` | Yes | Creates and deletes bridge sessions | | `TELEGRAM_SESSION_ENCRYPTION_KEY` | Yes | Seals stored account sessions | | `TELEGRAM_BRIDGE_DATABASE_URL` | Yes | Same Postgres as Evolution; the bridge keeps to its `telegram` schema | | `TELEGRAM_READER_ROLE` | No | A LOGIN-only role the bridge grants column-level `SELECT` to at every start | | `NUXT_TELEGRAM_URL` | For a default bridge | Bridge origin reached by web | | `NUXT_TELEGRAM_ADMIN_KEY` | For a default bridge | Matches the bridge administrator key | | `NUXT_TELEGRAM_DATABASE_URL` | For reads and search | Read access to the `telegram` schema | | `NUXT_TELEGRAM_WEBHOOK_URL` | Local development | Bridge-to-web callback; production derives it from the app URL | | `TELEGRAM_BRIDGE_PORT` | No | The bridge's port. Unset, it follows `PORT`, then defaults to `8095`. Pin it on platforms that inject `PORT` | | `TELEGRAM_TEST_SERVERS` | No | Use Telegram's separate test environment | Backfill is tuned by `TELEGRAM_BACKFILL_MAX_PER_CHAT`, `TELEGRAM_BACKFILL_DAYS`, `TELEGRAM_BACKFILL_BROADCAST`, and `TELEGRAM_BACKFILL_PAGE_DELAY_MS`. ## Email Email connections need no extra service. These optional variables on the web service tune them; see [Email (IMAP)](https://www.getautomata.app/docs/connections/email#settings). | Variable | Default | Purpose | | --- | --- | --- | | `NUXT_IMAP_BACKFILL_DAYS` | `365` | How far back the header sync reaches, per folder | | `NUXT_IMAP_BACKFILL_MAX` | `50000` | Most emails synced per folder | | `NUXT_IMAP_ATTACHMENT_MAX_BYTES` | `10485760` | Largest attachment `get-email-attachment` fetches | ## Alerts and mail The web server sends [connection alerts](https://www.getautomata.app/docs/alerts), [invitation emails](https://www.getautomata.app/docs/organizations), [verification codes](https://www.getautomata.app/docs/self-hosting/configuration#email-verification), and [password resets](https://www.getautomata.app/docs/self-hosting/configuration#password-reset) over SMTP. Configure `NUXT_SMTP_HOST`, `NUXT_SMTP_PORT`, `NUXT_SMTP_USERNAME`, `NUXT_SMTP_PASSWORD`, `NUXT_SMTP_TLS`, `NUXT_MAIL_FROM`, and `NUXT_MAIL_FROM_NAME`. `NUXT_SMTP_TLS=false` uses STARTTLS on port `587`; set it to `true` for implicit TLS on `465`. With `NUXT_SMTP_HOST` empty, alerts are computed and stay queued, invitation links can still be copied by hand, email verification stays off unless set to `required`, and passwords cannot be reset from the app. `NUXT_MAIL_FROM` must be an address the provider accepts. `NUXT_OPS_ALERT_EMAIL` is optional: the address mailed when a WhatsApp connection on this deployment's own Evolution server falls into a [reconnect loop](https://www.getautomata.app/docs/connections/whatsapp#stuck-reconnecting) or is [stuck with its socket dead](https://www.getautomata.app/docs/connections/whatsapp#stuck-with-the-socket-dead), since only whoever runs Evolution can fix either. It needs SMTP. ## MCP rate limits Every `/mcp` request counts against per-minute budgets, so one runaway agent loop cannot hammer the Evolution database, a connected Postgres, or a mail server for everyone else. | Variable | Default | Budget | | --- | --- | --- | | `NUXT_MCP_RATE_TOKEN_PER_MINUTE` | `120` | Every request, per connector token | | `NUXT_MCP_RATE_ORG_PER_MINUTE` | `600` | Every request, per organization, all its tokens together | | `NUXT_MCP_RATE_EXPENSIVE_PER_MINUTE` | `30` | Costly tools only, per connection: `search-messages`, `search-telegram-messages`, `search-emails` with `text`, and `run-query` without `explain` | Each budget can be spent all at once and refills continuously. `0` turns that budget off. A request over any budget is refused whole and costs nothing: it gets `429` with a `Retry-After` header and a JSON-RPC error naming the budget. The token stays valid. The web log gets one line per budget per minute, with ids only. **Where the counts live.** By default they are kept in each web process, which is right for a single replica. With several replicas each counts on its own, so the effective budget multiplies. To share the counts, set `NUXT_REDIS_URL`. It also shares two short-lived caches: the WhatsApp chat listing (one minute) and each WhatsApp connection's history setting (five minutes). Nothing requires it. | `NUXT_REDIS_URL` | Behavior | | --- | --- | | Unset | Counted and cached in-process; one line at boot says so | | Set, unreachable | In-process for as long as it is down, logged at most once a minute. Requests are never refused because of it | | Set, reachable | Shared across replicas and kept across redeploys | If you point it at a Redis that something else uses, such as Compose's `redis` service, which Evolution caches into, give it a database number of its own, for example `redis://redis:6379/1`; Evolution's cache uses `/6`. The app writes only keys starting with `automata:`, every one with an expiry of at most five minutes, and never runs `FLUSHALL`, `FLUSHDB`, `CONFIG`, or `KEYS`. It never picks up Evolution's `CACHE_REDIS_URI` on its own. ## Who can sign up Each self-signup creates a new organization, and its administrator can start connections on your Evolution server and Telegram bridge. So by default nobody can sign up without an invitation: | `NUXT_SIGNUP` | `NUXT_SIGNUP_CODE` | Who can sign up without an invitation | | --- | --- | --- | | Unset or `invite-only` | Unset | Only the first account on an empty database, which becomes the administrator | | Unset or `invite-only` | Set | Anyone who enters the code, the first account included | | `open` | Ignored | Anyone who finds the URL | Invitation links work in every mode, and neither variable is required: a new install with both unset lets you sign up first, then you invite everyone else from the Organization page. An unrecognized `NUXT_SIGNUP` value is read as invite-only, so a typo cannot open sign-up. Until that first signup, a fresh install on a public address belongs to whoever opens it first. If that window matters, set `NUXT_SIGNUP_CODE` before starting the stack (`openssl rand -base64 24`) and give the code only to people who should get their own organization. Wrong codes are throttled per client address. > **Never combine open sign-up with private targets** > > With `NUXT_SIGNUP=open` and `NUXT_ALLOW_PRIVATE_TARGETS=true`, any stranger who signs up can paste a database or Evolution address such as `postgres://…@192.168.1.20:5432/…`, and the server connects to it for them. Keep private targets for deployments where everyone with an account is trusted, and keep sign-up closed there. ## Email verification Without verification, anyone can register `someone@yourcompany.com` before its owner does, and the owner can then neither sign up nor accept an invitation sent to that address. With it, signing up (with or without an invitation) and changing an account's email both send a six-digit code and a link, and nothing is created or changed until one comes back. An unconfirmed signup holds no claim on the address and expires after 30 minutes. | `NUXT_EMAIL_VERIFICATION` | Behavior | | --- | --- | | Unset or `auto` | On when `NUXT_SMTP_HOST` is set, off otherwise | | `required` | Always on. Without SMTP, signups and email changes are refused | | `off` | Never. The password alone proves an email change | Any other value is read as `required`. The link is built from `NUXT_PUBLIC_APP_URL` only, never from the request, so a forged `Host` header cannot redirect it; without that URL the email carries only the code. A link only proves the address: it signs in only the browser that started the signup. Existing accounts are not asked to verify. ## Password reset **Forgot password?** on the sign-in page mails a link and a six-digit code, and an administrator can send the same mail to a member from the Organization page without ever seeing either. Both expire after 30 minutes and work once; a pending reset is kept in `app.password_resets`, one per account. Like the verification link, the reset link is built from `NUXT_PUBLIC_APP_URL` only. A reset signs every browser out of the account, still asks for the second factor at the next sign-in, and leaves connector tokens untouched. It needs SMTP. Without `NUXT_SMTP_HOST`, the page says passwords cannot be reset and the administrator button is hidden; the operator resets a password in the database instead. Hash the new password with the web image's own Node: ```bash docker compose exec web node -e "const c=require('node:crypto');const s=c.randomBytes(16);c.scrypt(process.argv[1],s,64,{N:32768,r:8,p:1,maxmem:67108864},(e,k)=>console.log(['scrypt',32768,8,1,s.toString('base64'),k.toString('base64')].join('\$')))" 'the-new-password' ``` In the single container, run the same with `docker exec /opt/node22/bin/node -e …`. Then store it, ending the account's sessions and pending sign-ins (write the address in lower case): ```sql BEGIN; UPDATE app.users SET password_hash = '', updated = now() WHERE email = 'person@example.com'; DELETE FROM app.sessions WHERE user_id = (SELECT id FROM app.users WHERE email = 'person@example.com'); DELETE FROM app.mfa_challenges WHERE purpose = 'login' AND user_id = (SELECT id FROM app.users WHERE email = 'person@example.com'); COMMIT; ``` ## Docker Compose The [Docker Compose deployment](https://www.getautomata.app/docs/self-hosting/docker-compose) reads a single `.env` created from `.env.selfhost.example`. It sets `WEB_PORT` for the published web port, `NUXT_PUBLIC_APP_URL`, the Telegram credentials, and every secret that must be generated before first start. Everything else uses short names that `docker-compose.yml` maps onto the `NUXT_*` variables above: `SIGNUP`, `SIGNUP_CODE`, `EMAIL_VERIFICATION`, `ALLOW_PRIVATE_TARGETS`, `WEBHOOK_SECRET`, `SMTP_*`, `MAIL_FROM`, `MAIL_FROM_NAME`, `OPS_ALERT_EMAIL`, `MCP_RATE_TOKEN_PER_MINUTE`, `MCP_RATE_ORG_PER_MINUTE`, `MCP_RATE_EXPENSIVE_PER_MINUTE`, and `REDIS_URL`. The [single container](https://www.getautomata.app/docs/self-hosting/single-container) reads the same names with `--env-file`, and generates any blank secret on first boot. Unlike local development, the web app runs in a container, so `NUXT_DATABASE_URL` points at the bundled Postgres service rather than `localhost`. ## Production settings - Do not ship `.env`; use platform variables and secret references. - Set `NUXT_WEBHOOK_SECRET`. - Keep `NUXT_ALLOW_PRIVATE_TARGETS` unset for shared deployments. - Leave sign-up invite-only unless strangers should be able to create organizations. - Set `NUXT_PUBLIC_APP_URL` to the exact address people open; email verification links and passkeys depend on it. - Keep the web service at one replica so the five-minute alert sweep runs once. If you run more for MCP traffic anyway, set `NUXT_REDIS_URL` so rate limits are shared. - Restart Evolution after every Postgres restart; failed writes can leave a WhatsApp session [stuck with its socket dead](https://www.getautomata.app/docs/connections/whatsapp#stuck-with-the-socket-dead). - Keep Evolution global keys, bridge keys, and database URLs out of client code and logs. - Back up Postgres, and restrict access to those backups. - Preserve `TELEGRAM_SESSION_ENCRYPTION_KEY` outside the deployment platform. > **Security warning** > > `NUXT_EVOLUTION_ADMIN_KEY` can create, inspect, and delete every WhatsApp instance on the default Evolution server. Treat it as one of the highest-impact credentials in the deployment. Source: https://github.com/ju-li/automata-mcp/blob/main/.env.example --- # Releases URL: https://www.getautomata.app/docs/self-hosting/releases Section: Self-host and operate > Image tags, version pinning, and safe upgrades. Three container images are published to GitHub Container Registry on every release. The current release on `:1` is `1.3.1`; `1.3.2` is the first release published to `:beta` first. ## Image tags | Image | Purpose | Used by | | --- | --- | --- | | `ghcr.io/ju-li/automata-mcp-web` | Management UI and MCP endpoint | Docker Compose, Railway | | `ghcr.io/ju-li/automata-mcp-telegram-bridge` | Telegram bridge | Docker Compose, Railway | | `ghcr.io/ju-li/automata-mcp-aio` | Everything in one container | [Single container](https://www.getautomata.app/docs/self-hosting/single-container) | The all-in-one image builds the web app and the bridge from the same commit as the other two, on top of Evolution's pinned image, so one version names the same code in all three. Every image is built for `linux/amd64`. Evolution API is not an Automata image; pin it to `evoapicloud/evolution-api:v2.3.7`. A release reaches its tags in two steps: | Tags | When | | --- | --- | | `X.Y.Z`, `beta`, `sha-` | As soon as the release is published. The GitHub Release is marked as a prerelease | | `X.Y`, `X`, `latest` | Once the release has been tested and promoted. The GitHub Release is then marked latest, and the MCP Registry listing is updated | Promotion retags the images that were tested rather than rebuilding them, so `:1` runs exactly the digest that ran as `:beta`. When two releases are waiting as betas, `beta` follows the newer one. ## Pick a tag > **Pin the major tag** > > Point services at `:1`, not `:latest`. The web service migrates its schema at boot and migrations only move forward, so a major release should never reach a deployment by accident on its next restart. Use `X.Y.Z` when you need an exact, reproducible version, and `sha-` to run a specific commit. Use `:beta` to run each release as soon as it is published, before it reaches `:1`. To run code that has not been released, build from the repository with the root directory at `/` and the Dockerfile at `apps/web/Dockerfile`, `apps/telegram-bridge/Dockerfile`, or `docker/aio/Dockerfile`. ## Upgrading and rollback 1. Back up Postgres, or the whole `/data` volume for the single container. 2. Pull the new image and restart the service. 3. Confirm the web logs include `[app-db] applied migrations`. Because migrations only move forward, never roll back by running an older image against an upgraded database. Restore Postgres from the backup taken before the upgrade instead. The same applies to a beta: a deployment that ran one cannot simply go back to the previous image, so try betas on a deployment whose database you can restore, or that you are willing to fix forward. Source: https://github.com/ju-li/automata-mcp/blob/main/docs/releases.md --- # Security URL: https://www.getautomata.app/docs/self-hosting/security Section: Self-host and operate > Harden credentials, network access, connector tokens, and backups. Automata reduces the authority given to an AI client, but operators still control the upstream credentials and infrastructure that define the ultimate boundary. ## Credential boundaries Keep browser sessions and MCP authentication separate. The web UI uses an HTTP-only session cookie stored only as a SHA-256 hash; MCP requests use connector tokens only. Web sign-in is hardened in three ways: - **Sign-up is invite-only by default.** Every self-signup creates a new organization administrator, so leave `NUXT_SIGNUP` unset or set a `NUXT_SIGNUP_CODE`. See [Who can sign up](https://www.getautomata.app/docs/self-hosting/configuration#who-can-sign-up). - **Email addresses are verified** once SMTP is configured, so nobody can register an address they do not own ahead of its owner. See [Email verification](https://www.getautomata.app/docs/self-hosting/configuration#email-verification). - **Failed sign-ins are throttled** per email and per client address, the current-password check behind password, email, and two-factor changes is throttled per account, and users can add [two-factor authentication](https://www.getautomata.app/docs/two-factor) with an authenticator app, passkey, or security key. Evolution global keys provision instances, while normal WhatsApp calls use per-instance tokens. The Telegram administrator key provisions sessions, while normal calls use per-session credentials. Do not add a fallback from a missing per-connection key to a global key. ## Connector tokens Raw connector tokens are shown once and stored only as SHA-256 hashes. Automata redacts `/mcp/` paths and authorization headers from its own error paths, but operators should still prefer bearer headers and ensure proxy logs do not capture secrets. Issue separate tokens per client, choose expiries, scope narrowly, and revoke any token exposed in chat, support tickets, logs, or source control. ## Database roles Use least-privilege roles for user database connections. Evolution message reads run only fixed, parameterized SELECTs in read-only sessions, so the owner URL works there; a SELECT-only role is optional hardening. Tool plan checks cannot see everything triggers or user-defined functions may do, and a superuser can bypass normal database restrictions. Grant only required schemas, tables, and statement types. Keep `run-statement` off unless the workflow truly requires writes. ## Network protections The outbound host guard blocks private and loopback targets by default and always blocks Automata internal services. It compares canonical addresses, so an IPv4-mapped IPv6 spelling or another loopback address on an internal service's port does not slip past. Database and mail connections dial the address it approved. Leave `NUXT_ALLOW_PRIVATE_TARGETS` off for shared environments, and never pair it with open sign-up. In the [single container](https://www.getautomata.app/docs/self-hosting/single-container), every backend is on loopback and Redis is not on the refused list, so turning it on there reaches more. Request bodies are capped before they are buffered: 64 KiB for most `/api` routes, 1 MiB for token creation and scope edits, 256 KiB for webhooks, and 2 MiB for `/mcp`, checked only after the token authenticates. A larger body is refused with `413`. Set `NUXT_WEBHOOK_SECRET`. Evolution does not sign webhook payloads itself; Automata registers per-instance webhooks carrying this shared header, then treats each delivery as a prompt to re-read live state rather than trusting payload state. Serve the web app only over HTTPS. The Docker Compose stack publishes plain HTTP on `WEB_PORT`, so place a TLS-terminating reverse proxy in front of it before exposing it to the internet. Do not expose Evolution, Redis, Postgres, or bridge administration publicly unless protected by an additional trusted network boundary. ## Storage and backups Connection credentials in the `app` schema (Evolution keys, bridge keys, and user connection strings) never leave the server, but they are not encrypted at rest. Treat `NUXT_DATABASE_URL` and every Postgres backup as holding every connected account, and restrict backup access accordingly. Telegram sessions are encrypted with `TELEGRAM_SESSION_ENCRYPTION_KEY`. Store the key separately from database backups and preserve it through deploys. Restrict access to the Evolution instance volume because it holds active WhatsApp pairings. > **Backups contain authority** > > A backup plus the relevant encryption or administrator keys may grant access to connected accounts and databases. Apply the same retention, access-control, and incident-response policy used for production secrets. Source: https://github.com/ju-li/automata-mcp/blob/main/README.md --- # Connection alerts URL: https://www.getautomata.app/docs/alerts Section: Self-host and operate > Get emailed when a messenger connection disconnects. A paired messenger account can silently drop. Automata emails everyone who can reach a connection when it disconnects, and again when it recovers, so you find out before an AI client starts failing. That is every administrator of the organization plus the members the connection is assigned to, each mailed separately. ## How alerts are detected Two mechanisms work together: - **Webhooks** give fast alerts. Automata registers a per-connection webhook with Evolution and the Telegram bridge. Each delivery is treated as a prompt to re-read live state, not as trusted state. - **A five-minute sweep** runs inside the web process and re-reads every connection. It is not just a backstop: a socket that dies and is retried emits no webhook at all, and a Telegram bridge that is down cannot send one, so the sweep is the only thing that sees those. Nothing is mailed until a connection has been down for **10 minutes**, in any state. A drop that recovers inside that window mails nobody, so a short blip no longer produces an outage email followed seconds later by a recovery email. Because the sweep runs every five minutes, a real outage is reported 10 to 15 minutes after it starts. A WhatsApp **reconnect loop** counts as an outage too, even though nearly every live read finds the account `open`. Two sockets on one set of credentials keep replacing each other, and neither the webhook nor the live state shows it. The sweep reads when Evolution last wrote the account's `"Instance"` row, which happens on every socket open. Ten minutes of fresh writes on an `open` row marks the connection unstable, and the 10-minute grace period runs from there, so the email goes out 20 to 25 minutes into the loop. It says restarting the Evolution service is the fix, since **Reconnect** cannot help. See [Stuck reconnecting](https://www.getautomata.app/docs/connections/whatsapp#stuck-reconnecting). So does a WhatsApp account whose **socket is dead under a live `open`**: every read says connected, but a send or group lookup failed with `Connection Closed` and Evolution has not reopened the socket since. The connection is marked stuck from the next read, and the 10-minute grace period runs from there. The email says the Evolution service needs restarting, never **Reconnect**. A webhook delivery cannot start it. See [Stuck with the socket dead](https://www.getautomata.app/docs/connections/whatsapp#stuck-with-the-socket-dead). Only whoever runs the Evolution service can end a reconnect loop or a stuck session, so the operator can be told directly. Set `NUXT_OPS_ALERT_EMAIL` on the web service and that address is mailed as soon as a connection on the deployment's own Evolution server is marked unstable or stuck, with no grace period, and once more when that ends. The mail names only ids. Connections on a server the organization supplied are not reported. A connection that has never been paired is skipped. An alert is recorded only after the email is actually sent. A broken SMTP configuration delays alerts instead of dropping them, and a deployment with no SMTP host computes alerts without delivering them. ## Configure mail Set the `NUXT_SMTP_*` variables and `NUXT_MAIL_FROM` on the web service. Use `NUXT_SMTP_TLS=false` for STARTTLS on port `587`, and `true` for implicit TLS on `465`. The sender must be an address your provider accepts. The full list is in [Configuration](https://www.getautomata.app/docs/self-hosting/configuration#alerts-and-mail). The same mail settings deliver [organization invitations](https://www.getautomata.app/docs/organizations) and account emails such as verification codes and password resets. Without SMTP, **Send invite email** answers with an error and leaves the link on screen to copy; **Resend email** has already replaced the link, so it shows the new one to copy, and the old one no longer works. ## Operational rules > **Run one web replica** > > The sweep runs in-process, so several web replicas send duplicate alert emails. Keep the web service at one replica. - Set `NUXT_WEBHOOK_SECRET` so webhook requests carry and verify the shared `x-webhook-secret` header. - Make sure `NUXT_WEBHOOK_URL` is reachable from Evolution. Without it no per-connection webhook is registered, and alerts arrive only as fast as the sweep. The Telegram bridge posts to `NUXT_TELEGRAM_WEBHOOK_URL` when it is set, and otherwise derives its callback from `NUXT_PUBLIC_APP_URL`. - Set `NUXT_PUBLIC_APP_URL` so each alert links to its connection. ## Test it locally Run a mail catcher, then trigger the sweep by hand. The steps are in [Local deployment](https://www.getautomata.app/docs/self-hosting/local#testing-mail-locally). For missing or duplicate emails, see [Troubleshooting](https://www.getautomata.app/docs/troubleshooting#alerts). Source: https://github.com/ju-li/automata-mcp/blob/main/docs/alerts.md --- # Troubleshooting URL: https://www.getautomata.app/docs/troubleshooting Section: Self-host and operate > Diagnose pairing, history, webhook, networking, and volume problems. Start with a status tool, service health endpoint, and `pnpm services:logs`. Avoid deleting volumes or relinking accounts until the failure mode is clear. ## Connection status **WhatsApp says connected but tools fail:** call `get-connection-status`. A stored state can outlive a dead socket; reconnect from the dashboard when live state is closed. **WhatsApp shows Unstable, or `get-connection-status` returns `flapping: true`:** two sockets are replacing each other in a reconnect loop. Restart the Evolution service; **Reconnect** in the dashboard does not help. See [Stuck reconnecting](https://www.getautomata.app/docs/connections/whatsapp#stuck-reconnecting). **WhatsApp shows Stuck, or `get-connection-status` returns `stalled: true`:** the account reads `open` but its socket is dead, so sends fail and no new messages arrive. Restart the Evolution service; no QR scan is needed and **Reconnect** does not help. This often follows a Postgres restart, so restart Evolution after every one. See [Stuck with the socket dead](https://www.getautomata.app/docs/connections/whatsapp#stuck-with-the-socket-dead). **A WhatsApp connection disappeared:** a connection not paired within an hour of being created is deleted with its tokens. Create it again and scan the QR code before the countdown runs out. See [Pair an account](https://www.getautomata.app/docs/connections/whatsapp#pair-an-account). **Telegram reports another process:** ensure the bridge has one replica and deployment overlap is disabled. A second process waits on the session lock and should take over after the first exits. **Telegram session ended:** link by QR again. Synced chats are retained when the session ends from Telegram Devices, but an explicit Automata unlink deletes them. ## Message history **WhatsApp reads return a missing setting:** configure `NUXT_EVOLUTION_DATABASE_URL`, or add the database URL to a bring-your-own connection. If you use a dedicated role, confirm it can select from `"Message"`. **Creating a Telegram connection is refused:** the message says no Telegram bridge is available. Set `NUXT_TELEGRAM_URL` and `NUXT_TELEGRAM_ADMIN_KEY` on the web service, or supply your own bridge. **Telegram is not offered:** set both `TELEGRAM_API_ID` and `TELEGRAM_API_HASH` on the bridge and restart it. `/health` reports `"telegram": "unconfigured"` until both are set. **WhatsApp history is incomplete, or tools return `recentHistoryOnly: true`:** the connection was created without **Import full history**, which is off by default, so only recent history was imported. History arrives only during a fresh device link: ensure `DATABASE_SAVE_DATA_HISTORIC=true`, then press **Import full history** on the dashboard and rescan. The phone still decides the ultimate history depth. **Import full history is locked:** on the hosted Free plan only recent history is imported. Upgrade to Pro, then press **Import full history** on the connection's dashboard; upgrading imports nothing by itself. **Telegram older history stops early:** check `TELEGRAM_BACKFILL_MAX_PER_CHAT`, `TELEGRAM_BACKFILL_DAYS`, and broadcast settings. Slow backfill is intentional. On the hosted Free plan, history is fetched back 90 days; an account linked under Free fetches further back only once it is unlinked and linked again under Pro. `get-telegram-status` shows the limit in force as `sync.historyDays`. **Chat counts differ:** the dashboard counts conversations reported during pairing, while `list-chats` counts conversations with stored messages. Use `hasMore` to detect another result page. **A WhatsApp send is refused as a LID:** the digits passed are a LID, WhatsApp's privacy alias for a person, not a phone number. Sending to them as a number would reach whoever owns that number, so Automata refuses. Reply with the chat's full `jid`, ending in `@lid`, exactly as `list-chats` returns it. **A WhatsApp group shows an old name or a bare id:** names come from Evolution's chat rows, which are not updated on a rename, and are refreshed by slow one-at-a-time lookups, every group at least once a week. A renamed group shows its old name, and a group with no stored name its bare id, until its next lookup. **WhatsApp chat listings or searches are slow or time out:** create the optional indexes in [Indexes on Evolution's tables](https://www.getautomata.app/docs/connections/whatsapp#indexes-on-evolutions-tables). Without them every listing and search reads the whole account. **The web log says a grant is missing:** a dedicated Evolution database role needs the column grants on `"Instance"`, the `"instanceId"` grants on `"Chat"` and `"Contact"`, and the `"remoteJid"`/`"name"` grant on `"Chat"` shown in the [WhatsApp setup notes](https://www.getautomata.app/docs/connections/whatsapp#what-you-need). Status still works without them, through a slower fallback, but reconnect loops go undetected, and a connection read through that role is never removed for being left unpaired. ## MCP requests **A client gets `429` with `Retry-After`:** a per-minute [rate limit](https://www.getautomata.app/docs/reference/mcp-tools#rate-limits) ran out, and the JSON-RPC error names which one: 120 requests a minute per token, 600 per organization, or 30 costly calls (searches, `run-query`) per connection. The token stays valid; wait and retry. Self-hosters can raise or turn off each budget; see [MCP rate limits](https://www.getautomata.app/docs/self-hosting/configuration#mcp-rate-limits). **Rate limits are looser than configured:** with several web replicas and no `NUXT_REDIS_URL`, each replica counts on its own. Set `NUXT_REDIS_URL` to share the counts. ## Networking and webhooks **Webhook silently times out on Linux:** add the narrow `ufw` rule from the [local deployment guide](https://www.getautomata.app/docs/self-hosting/local) and verify the container-to-host request. **Private PostgreSQL target is refused:** set `NUXT_ALLOW_PRIVATE_TARGETS=true` only for a trusted local or single-tenant environment. Automata internal targets remain blocked. **Bring-your-own server fails validation:** provide both URL and administrator key, verify DNS and TLS, and confirm the origin is allowed by the outbound host guard. ## Railway and volumes **Private hostname resolves but connection is refused:** confirm `SERVER_PORT=8080` and `TELEGRAM_BRIDGE_PORT=8095`, then make internal URLs match. **Build cannot find `pnpm-workspace.yaml`:** use the published `:1` images, or set the web and bridge root directory to `/` and the Dockerfile path separately. **All WhatsApp accounts unpaired after deploy:** verify `/evolution/instances` is attached and persistent. If the volume was deleted, each account must be scanned again. **Web fails to start after an upgrade:** check for `[app-db]` errors. Migrations only move forward, so pin images to the major tag and restore Postgres from a backup rather than downgrading. See [Releases](https://www.getautomata.app/docs/self-hosting/releases). **Users or tokens vanished:** check that `NUXT_DATABASE_URL` points at the same Postgres as before. Coming from PocketBase, deploy commit `2e1872e` first so the import runs. ## Alerts See [Connection alerts](https://www.getautomata.app/docs/alerts) for how detection works. **No disconnect email arrives:** configure `NUXT_SMTP_*` and `NUXT_MAIL_FROM` on the web service, use a sender address your provider accepts, and check for `[mailer]` errors. Alerts are recorded only after successful delivery. **Duplicate alert emails:** keep the web service at one replica because the five-minute sweep runs in-process. **Alerts arrive 10 to 15 minutes after a drop:** expected. Every outage serves a 10-minute grace period so short blips that recover on their own mail nobody, and the sweep runs every five minutes. ## Sign-in and sign-up **"Sign-up is closed" on a new install:** someone already created the first account, or `NUXT_SIGNUP_CODE` is set and must be entered. Ask an administrator for an invitation. See [Who can sign up](https://www.getautomata.app/docs/self-hosting/configuration#who-can-sign-up). **No verification email:** check the `NUXT_SMTP_*` settings and `[mailer]` logs. With `NUXT_EMAIL_VERIFICATION=required` and no SMTP host, signups are refused with `503`. **Verification email has a code but no link:** set `NUXT_PUBLIC_APP_URL`. The link is never built from the request host. **"Passwords cannot be reset here" after Forgot password?:** the deployment has no SMTP host. Set the `NUXT_SMTP_*` variables, or reset the password in the database as described in [Password reset](https://www.getautomata.app/docs/self-hosting/configuration#password-reset). With SMTP, the mailed link and code expire after 30 minutes and work once. **Signed out everywhere after a password reset:** expected. A reset ends every session of the account, and two-factor is still asked for at the next sign-in. Connector tokens keep working. **Passkeys are missing or the browser refuses them:** `NUXT_PUBLIC_APP_URL` must be the exact HTTPS address people open. See [Two-factor authentication](https://www.getautomata.app/docs/two-factor#passkeys-and-the-app-url). > **Important** > > Do not use `docker compose down -v` while troubleshooting. The `-v` flag deletes pairing and application state volumes. Source: https://github.com/ju-li/automata-mcp/blob/main/README.md --- # MCP tools URL: https://www.getautomata.app/docs/reference/mcp-tools Section: Reference > All 22 tools, grouped by connector with scope and safety behavior. Automata exposes five tools each for WhatsApp, Telegram, and PostgreSQL connections, and seven for email (IMAP) connections. A token sees only tools matching both its connection kind and action scope. Every tool also takes an optional `reason` argument: one sentence from the model on why it is making the call. Automata removes it before the tool runs, so it never changes what a tool does. When the organization has [telemetry](https://www.getautomata.app/docs/telemetry) on, the reason is stored with the call. ## WhatsApp tools ### `get-connection-status` (read) Reports whether the account is paired and whether live state is `open`. `flapping: true` means the account reads `open` but keeps reconnecting: reads may miss recent messages and sends may fail. The dashboard shows it as **Unstable**. `stalled: true` means the account reads `open` but its socket was found dead: sends fail and new messages are not arriving. The dashboard shows it as **Stuck**. Both are fixed by restarting the Evolution service, not by **Reconnect**. See [Stuck reconnecting](https://www.getautomata.app/docs/connections/whatsapp#stuck-reconnecting) and [Stuck with the socket dead](https://www.getautomata.app/docs/connections/whatsapp#stuck-with-the-socket-dead). `recentHistoryOnly: true` means the account's history import was partial; see [Message history](https://www.getautomata.app/docs/connections/whatsapp#message-history). ### `list-chats` (read) Lists in-scope conversations by recent activity and returns the `jid` used by other tools. The response reports `hasMore` when another page exists. The listing is read live, so a message that has reached the dashboard appears on the next call. It carries no read or unread status, because WhatsApp's read state is not available to it. Timestamps are UTC. A `jid` ending in `@lid` is a LID, WhatsApp's privacy alias for a person: its digits are not a phone number. A LID chat carries `phoneNumber` only when WhatsApp has revealed the number on a live message. A person stored under both a LID and a phone number is listed once, under the LID, with both ids in `jids`; `linkedOutOfScope` means part of that conversation is under an id the token may not read. ### `read-messages` (read) Reads one in-scope chat newest first. Accepts 1–200 items per page with a default of 50, optional `since` and `until`, and returns `hasMore`, `nextPage`, `covered`, and `totalMatching`. Given either id of a person linked across a LID and a phone number, it reads both as one timeline and lists them in `jids`. `recentHistoryOnly: true` means the account imported only recent history at pairing, so the oldest stored message is not necessarily where the chat began; a `note` says so once the read reaches it. ### `search-messages` (read) Finds messages containing every query word across allowed chats or one `jid`. Supports a date range and `fromMe`; returns up to 100 results with a default of 20. A query takes at most 10 words of at most 100 characters each, 500 characters in all. `recentHistoryOnly: true`, with a `note`, means the account imported only recent history, so finding nothing older is not proof nothing older exists. ### `send-text-message` (write) Sends plain text after chat scope and live recipient verification. To reply in an existing chat, pass its full `jid` as `number`, exactly as `list-chats` or `read-messages` returned it, including `…@lid` chats. For someone with no chat yet, pass a phone number in international format with the country code, digits only, with no `+`, spaces, or dashes (for example `5511999999999`). Bare LID digits passed as a phone number are refused, with the full `jid` to use instead. If the token has a send disclaimer, it is appended to every message and named in `disclaimerAppended`. A refusal comes back with WhatsApp's reason: a number that is not on WhatsApp, a connection that is down, or the message WhatsApp gave. WhatsApp read and search omit reactions unless `includeReactions: true`; the response reports `reactionsExcluded`. ## Telegram tools ### `get-telegram-status` (read) Reports linked and online state, the signed-in identity, and synchronization progress. `sync.historyDays` is how far back older history is fetched for this account: 90 days on the hosted Free plan, otherwise the deployment's setting. ### `list-telegram-chats` (read) Lists in-scope private chats, groups, supergroups, and channels by recent activity. Returns the `chatId` used by other tools. ### `read-telegram-messages` (read) Reads one chat newest first with the WhatsApp-style paging envelope. An optional `topicId` selects a forum topic, and the response says when older history was never synced. ### `search-telegram-messages` (read) Finds every query word in message text and captions across allowed chats or one `chatId`. Returns up to 100 results with a default of 20, with the same query limits as `search-messages`. ### `send-telegram-message` (write) Sends plain text to an allowed `chatId` or username; pass exactly one of the two. Telegram's 4,096-character limit includes any send disclaimer, which is named in `disclaimerAppended`. ## Email (IMAP) tools Email tools read one mailbox. Headers are synced every few minutes; bodies and attachments are fetched from the mail server only when asked for and never stored. None of these tools can send, delete, move, or mark mail, and reading an email does not mark it read. Setup, sync behavior, and settings are in [Email (IMAP)](https://www.getautomata.app/docs/connections/email). ### `get-mailbox-status` (read) Reports which mailbox the token reads, whether its sync is working, when it last synced, and which folders still hold unsynced older mail. ### `list-mail-folders` (read) Lists the folders the token may read, INBOX first, with synced and unread counts. All Mail, Junk, and Trash are listed with `synced: false` and cannot be listed or searched. ### `list-emails` (read) Lists one page of emails newest first, without bodies, from one `folder` or every synced folder in scope. Filters by `since`, `until`, `unread`, `flagged`, and `hasAttachments`. Accepts 1–100 items per page with a default of 25 and returns `hasMore`, `nextPage`, `covered`, and `totalMatching`. ### `search-emails` (read) Finds emails by `from`, `to`, `subject`, and `text`, combined with AND, optionally within one `folder` and a date range. `from`, `to`, and `subject` match synced headers; `text` (at least two characters) is passed to the mail server's own search, so matching varies by provider. ### `read-email` (read) Returns one email's headers, its body as plain text (HTML is converted), and its attachment list. Long bodies come back in pieces of `maxChars` (default 20,000, at most 100,000), continued with `offset`. ### `get-email-attachment` (read) Fetches one attachment by `part`, always the whole file. Images come back as images, text files as text, and other files, PDFs included, as embedded files. Attachments over the size limit, 10 MiB by default, are refused with their size. ### `create-email-draft` (write) Saves a plain-text draft in the mailbox's Drafts folder for the owner to review and send. It never sends. Pass `replyToEmailId` to thread a reply under an email; the subject and recipient then default from the original. A new email token starts without this tool. ## PostgreSQL tools ### `get-database-info` (read) Returns server version, database name, connecting role, and token table scope. ### `list-tables` (read) Lists tables in scope with schema and name filters. Accepts 1–500 entries per page and defaults to 100. ### `describe-table` (read) Returns columns, keys, constraints, indexes, and comments for one in-scope table. ### `run-query` (read) Runs one `SELECT`, `WITH`, `VALUES`, or `TABLE` statement in a read-only transaction. Results come in pages of `maxRows` rows (at most 1,000, default 200), walked with `page`. Cells are clipped at 2,000 characters unless `fullValues` is set. A page larger than 8 MiB is refused rather than cut. Timeout defaults to 10 seconds and is capped at 120, and a query is cancelled on the database if the client disconnects first. Pass `explain: true` to get the plan the query would run instead of its rows. The query is planned, not executed, so this is the cheap way to find out why a query timed out or is slow. Row figures in a plan are planner estimates, never counts. ### `run-statement` (write · destructive) Runs one `INSERT`, `UPDATE`, `DELETE`, or `MERGE`. A change over `maxRows` rolls back in full; the default is 100. A whole-table update or delete requires `allowWholeTable: true`. `RETURNING` rows over 8 MiB roll the statement back. Timeout defaults to 15 seconds and is capped at 60. ## Limits and scope behavior | Boundary | Enforcement | | --- | --- | | Connection kind | Tools for other connector kinds are never registered | | Action scope | Disabled tools cannot be called | | Chat and folder scope | Lists, reads, search, and sends operate only on allowed chats or email folders | | Table scope | Catalog queries and query plans reject tables outside the allowlist | | Query shape | One statement; read and write forms are separated | | Write size | Oversized writes roll back as one transaction | | Request size | MCP request bodies over 2 MiB are refused with `413` | | Request rate | Per-minute budgets per token, organization, and connection; see [Rate limits](https://www.getautomata.app/docs/reference/mcp-tools#rate-limits) | | Availability | Missing message databases disable dependent read tools with an explicit error | ## Rate limits Requests are otherwise unlimited, but every `/mcp` request counts against per-minute budgets, so one runaway agent loop cannot slow the shared databases and mail servers for everyone else. The budgets are the same on every plan. | Budget | Default | | --- | --- | | Per connector token, every request | 120 a minute | | Per organization, all its tokens together | 600 a minute | | Per connection, costly tools only: `search-messages`, `search-telegram-messages`, `search-emails` with `text`, and `run-query` without `explain` | 30 a minute | Each budget can be spent all at once and refills continuously. A request over any of them is refused whole with `429`, a `Retry-After` header, and a JSON-RPC error naming the budget that ran out. It never gets `401`, so the client keeps the token: wait and retry. Self-hosters can change or turn off each budget; see [MCP rate limits](https://www.getautomata.app/docs/self-hosting/configuration#mcp-rate-limits). > **Important** > > Tool annotations help clients understand read-only and destructive behavior, but the server-side gates and upstream credentials are the controls that enforce it. Source: https://github.com/ju-li/automata-mcp/blob/main/apps/web/server/mcp/tools --- # Development URL: https://www.getautomata.app/docs/development Section: Contribute > Repository layout, scripts, auth paths, and adding an MCP tool. Automata is a pnpm workspace with a Nuxt web app, a Telegram bridge, and a cloud service used only by the hosted offering. This page orients you before changing the source. ## Repository layout ```text apps/ web/ Nuxt app: management UI and MCP endpoint telegram-bridge/ Personal-account Telegram bridge cloud/ Hosted offering only: plans, Stripe, account events docker/aio/ All-in-one image: Dockerfile and s6 service tree docs/ Source documentation docker-compose.dev.yml Supporting services for local development docker-compose.yml Full self-host stack from published images ``` Start the supporting services from the [local deployment guide](https://www.getautomata.app/docs/self-hosting/local), then run `pnpm dev` on the host so hot module replacement works. ## Scripts | Command | Purpose | | --- | --- | | `pnpm dev` | Run the Nuxt app on the host | | `pnpm services:up`, `:whatsapp`, `:telegram` | Start service profiles | | `pnpm services:down`, `services:logs`, `services:ps` | Stop, inspect, and tail services | | `pnpm typecheck` | Type-check the web app | | `pnpm bridge:typecheck` | Type-check the Telegram bridge, which `pnpm typecheck` does not cover | | `pnpm test:emails` | Check the account-email templates against their snapshots | There is no `predev` hook, so start the services your connector needs before `pnpm dev`. ## Authentication paths The two surfaces never share credentials: - Management routes require the HTTP-only session cookie, stored as a SHA-256 hash in the `app` schema. A row in `app.sessions` always means fully signed in: a sign-in still owed a [second factor](https://www.getautomata.app/docs/two-factor) lives in `app.mfa_challenges` until it is answered. - The MCP endpoint accepts only a connector token, from `Authorization: Bearer` or the `/mcp/` path, and never falls back to a session. Invalid tokens receive `401` with `WWW-Authenticate`. A database outage returns `503`, so a transient failure is never mistaken for a revoked token. See [How Automata works](https://www.getautomata.app/docs/how-automata-works). Request bodies are capped before they are buffered (`server/utils/body-limit.ts`): 64 KiB for `/api`, 1 MiB for token creation and scope edits, 256 KiB for webhooks, and 2 MiB for `/mcp` after the token authenticates. A new route that needs a larger body needs its own entry there, or it answers `413`. ## Add an MCP tool Tools live under `apps/web/server/mcp/tools`, grouped by connection kind. 1. Define the tool in the folder for its kind, with a read, write, or destructive annotation. Don't declare a `reason` argument: every tool gets an optional one for [telemetry](https://www.getautomata.app/docs/telemetry), removed before your handler runs, and a tool that declares its own fails at load. 2. Register it for that kind only. Other kinds must never see it. 3. Resolve the connection from the token, never from a tool argument. 4. Enforce action scope and any chat or table allowlist on the server, before touching upstream data. 5. List it in `lhm.plugin.json`, or the release check refuses the release: regenerate the list with `pnpm listings:tools /.well-known/mcp/server-card.json` against a running server. 6. Document it in the [MCP tools reference](https://www.getautomata.app/docs/reference/mcp-tools). > **Note** > > Tool annotations only describe behavior to clients. The server-side gates are what enforce it, so new tools must pass through the same scope checks as existing ones. Source: https://github.com/ju-li/automata-mcp/blob/main/docs/development.md