Elaichi

Governed AI agents for the software your company already runs.

От сообщества: Добавлен пользователем или импортирован; проверьте владельца перед подключениемРаботаетБез входаГлобальныйБесплатноТолько чтение

Что умеет

  • Elaichi Organization Get: Read this organization’s own record: id, name, slug, plan and the non-secret `settings` object. Returns that object directly — no envelope, no cursors. Billing identifiers an
  • Elaichi Member List: List the people in this organization. Each row carries `user_id` (a `usr_…` id), status, join date, the roles and teams that person holds, and `user`: { id, name, email } — the na
  • Elaichi Member Get: Read one membership by USER id. Returns the record directly — user_id, status, via, joined_at, roles[], teams[], plus `user`: { id, name, email } — and errors when that user is not

Какие данные видит

Нужен ли аккаунт

Не нужен: сервер работает без входа

Governed AI agents for the software your company already runs. 400+ connectors, one endpoint.

Your client connects to one thing: Elaichi. Your applications are connected inside Elaichi, not inside the client — so adding a new app later doesn't mean touching the client again.

Because every call travels through Elaichi rather than around it:

  • Access follows the person. Two people on the same team connect the same endpoint and see different tools, resolved against their own permissions on every call.
  • Restricted tools are never offered. A tool your organization has restricted isn't advertised to the model at all, so it can't be talked into calling it.
  • The call is attributed. Tool calls land in an append-only log, including the identifier of the record they changed.
  • Revoking is one action. Disconnect an app in Elaichi and it's gone from every client, with no hunting through anyone's settings.

Connecting: OAuth 2.1 with PKCE and dynamic client registration. There's no API key, client ID or secret to paste — the client registers itself on first contact, you sign in, and you pick what it may do on a consent screen.

Docs: https://elaichi.ai/docs

Как подключить

Как подключить Elaichi к Claude

Адрес сервера, который нужно вставить в настройках Claude:

https://gateway--elaichi.run.tools

  1. Откройте Claude (сайт claude.ai или приложение для компьютера).
  2. Перейдите в Настройки → Коннекторы (в английском интерфейсе: Settings → Connectors).
  3. Нажмите Добавить пользовательский коннектор (Add custom connector).
  4. Вставьте адрес сервера, скопированный ниже, в поле URL удалённого MCP-сервера и нажмите Добавить.
  5. В чате нажмите + → Коннекторы и включите новый коннектор.

Пользовательские коннекторы доступны на тарифах Free, Pro, Max, Team и Enterprise. На Free можно добавить только один. В Team и Enterprise коннектор сначала добавляет владелец организации в Настройки организации → Коннекторы.

Авторизация

После нажатия Добавить откроется окно входа в сервис. Войдите под своим аккаунтом и подтвердите доступ. Пароль Claude не видит.

Как подключить Elaichi к ChatGPT

Адрес сервера, который нужно вставить в настройках ChatGPT:

https://gateway--elaichi.run.tools

  1. Откройте ChatGPT в браузере (chatgpt.com). Нужен тариф Plus, Pro, Business, Enterprise или Edu.
  2. Один раз включите режим разработчика: Настройки → Приложения → Дополнительные настройки → Режим разработчика (Settings → Apps → Advanced settings → Developer mode).
  3. Откройте Настройки → Коннекторы и нажмите Создать (Create).
  4. Заполните форму: Название (любое), Описание (одна фраза, что делает сервис), URL MCP-сервера (скопируйте ниже).
  5. В поле Аутентификация выберите OAuth, если сервис требует вход, иначе Нет. Нажмите Создать.
  6. В новом чате откройте + → Приложения/Коннекторы и включите коннектор.

Название раздела у OpenAI менялось (Connectors → Apps/Plugins). Если пункт называется иначе, ищите «режим разработчика» в настройках. В рабочих пространствах Business/Enterprise администратор должен разрешить пользовательские коннекторы.

Авторизация

При первом использовании ChatGPT откроет окно входа в сервис. Войдите и подтвердите доступ.

Как подключить Elaichi к Cursor

Адрес сервера, который нужно вставить в настройках Cursor:

https://gateway--elaichi.run.tools

Быстрый способ: нажмите кнопку Открыть в Cursor ниже, Cursor сам предложит добавить сервер.

Вручную:

  1. В Cursor откройте Settings → Cursor Settings → MCP и нажмите Add new global MCP server.
  2. Вставьте JSON, скопированный ниже, в открывшийся файл ~/.cursor/mcp.json (для одного проекта: .cursor/mcp.json в корне проекта).
  3. Сохраните файл. Сервер появится в списке MCP; при необходимости пройдите авторизацию по кнопке рядом с ним.

Авторизация

Если сервис требует вход, рядом с сервером в списке MCP появится кнопка авторизации.

Список инструментов сервера (52)

Технические названия из tools/list. Нужны только разработчикам.

elaichi__organization__getRead this organization’s own record: id, name, slug, plan and the non-secret `settings` object. Returns that object directly — no envelope, no cursors. Billing identifiers and trial fields are never included. Takes no arguments; the organization is fixed by the caller’s credentials, so there is no way to read a different one.
elaichi__member__listList the people in this organization. Each row carries `user_id` (a `usr_…` id), status, join date, the roles and teams that person holds, and `user`: { id, name, email } — the name and email live outside the membership record and are attached here, so call people by name rather than reading a `usr_…` id back to the user. Going the other way, `q` matches name and email case-insensitively across the whole organization and filters before paging, so "find Bob" is one call, not a walk through every page. Every member.* operation, plus team.add_member and team.remove_member, takes the `user_id` you read here. Returns { result: [...], nextCursor, prevCursor } — camelCase here, unlike the REST API’s next_cursor. Page by sending nextCursor back as `cursor` until it is null.
elaichi__member__getRead one membership by USER id. Returns the record directly — user_id, status, via, joined_at, roles[], teams[], plus `user`: { id, name, email } — and errors when that user is not a member of this organization. To find someone by name in the first place, use member.list with `q`. Read this before member.set_roles, which replaces the role list wholesale.
elaichi__member__set_rolesSet the single role one member holds. A member holds exactly ONE role in an organization, so this is a WHOLESALE REPLACE: `role_ids` must be an array of exactly one role id, and that role becomes their entire access. There is no way to add a role alongside an existing one — "also give Bob the admin role" means Bob stops being whatever he was and becomes an admin, so confirm that with the user before calling. If they need a mix of two roles’ permissions, the answer is a custom role holding both (role.create), not two assignments. Resolve role ids with role.list first. Takes effect on the member’s next request. You cannot assign a role that carries a permission you do not hold yourself, and you cannot re-role a member who holds permissions you lack. Returns the updated membership, including `user`: { id, name, email }, so you can confirm out loud whose role changed. To change what a role means for everyone who holds it, use role.update instead; to remove the person from the organization entirely, member.delete. NOT CALLABLE FROM ANY AI SURFACE, in-app agent or MCP: handing somebody a role needs step-up reauthentication (confirming it is really you), which neither surface can ask for, so every call from here is refused. Say what needs doing and tell the user to do it in Settings → People in the Elaichi app.
elaichi__member__offboardingPreview what removing a member would break, without changing anything: their personal connections and, per connection, which toolboxes reference it and whether that reference reaches beyond the member. Any connection marked `needs_resolution` will make member.delete refuse. Separately, `delegated_entries` lists entries on OTHER members’ toolboxes whose pin currently rides on THIS member’s own "use" grant, not their ownership (docs/access-model.md §6) — non-blocking, and member.delete never refuses over it, but say so: once the member is gone those entries go unmet for everyone until somebody with their own "use" on the connection re-pins them; there is no operation that does that automatically. `synthetic_tools` lists the multi-step tools they own, as metadata only (name, description, step_count — never the steps, which are private to their author): those cannot be shared or transferred, so every one of them is deleted when the member is removed, and their presence makes member.delete refuse outright — say what will be lost before proposing a removal. `toolboxes` and `templates` list what they own of each, with a bounded `shared_with` rollup (counts plus at most five named grantees, never the whole grant list) and an `entry_count`: only a resource’s OWNER can transfer or delete it, so one left behind when its owner goes becomes unmanageable by everybody in the organization forever — a shared one keeps working for its grantees with nobody able to change it, a private one simply becomes unreachable. Their presence also makes member.delete refuse. Returns { user_id, user: { id, name, email }, connections: [{ connection, referenced_by: [...], needs_resolution }], synthetic_tools: [{ synthetic_tool: { id, name, description, step_count, ... }, referenced_by: [...], needs_resolution }], toolboxes: [{ resource_type, id, name, entry_count, private, shared_with: { team_count, user_count, total, preview }, needs_resolution }], templates: [ ...same shape ], delegated_entries: [
elaichi__team__listList the organization’s teams: id (`team_…`), name, description and member count. `q` filters by team name before paging. Who belongs to a team is not in these rows — read one with team.get. Returns { result: [...], nextCursor, prevCursor } — camelCase here, unlike the REST API’s next_cursor. Page by sending nextCursor back as `cursor` until it is null.
elaichi__team__getRead one team. Returns the record directly — no envelope, no cursors — or null when the id is unknown. The record carries `member_count`/`admin_count` and a BOUNDED preview of the roster (`member_preview_user_ids`, `admin_preview_user_ids` — at most five ids each), never the whole membership: a team can hold thousands of people. Alongside the preview, `members` repeats those same few as { id, name, email }, so you can name them without a second call; a previewed id with no `members` entry has left the organization. There is no operation for the full roster — say what `member_count` is rather than implying the preview is everyone.
elaichi__team__createCreate an empty team. Only `name` is required. Returns the created team including its new `team_…` id. Nobody is added by this call — add people one at a time with team.add_member.
elaichi__team__updateRename a team or change its description. MERGE, not replace: fields you omit keep their current values (unlike member.set_roles, role.update’s `permissions` and toolbox.set_entries, which all replace wholesale). Membership is untouched — use team.add_member / team.remove_member for that. Returns the updated team record, the same shape team.get returns.
elaichi__team__add_memberAdd an existing organization member to a team. This does not invite anyone: the person must already be a member (member.list). Anything shared with the team — toolboxes, connections — becomes available to them immediately. Returns the TEAM record, in the same shape team.get returns and with membership already re-read, so `member_count` includes the person you just added; it does not return a membership record, because there is no such thing.
elaichi__team__set_member_adminAppoint or remove a TEAM administrator — the per-team counterpart of the org-wide team:manage permission. `is_admin: true` gives this person add/remove-member, rename and describe rights over THIS team only; `is_admin: false` demotes them back to an ordinary member with no such rights. The person must already be on the team (team.add_member first) — this does not add them, and it 404s if they are not already a member. Refuses if you try to appoint YOURSELF (self-promotion) — the same self-escalation guard team.add_member enforces; demoting yourself is allowed. Appointing a team admin does not widen what they can see or use — it only lets them manage who else is on this one team — and it never reaches team.delete, which stays org-wide-team:manage-only regardless of who administers the team. This operation itself needs org-wide team:manage to call, the same narrower gate its REST route enforces: unlike team.add_member/team.remove_member, an existing team administrator with no org-wide team:manage cannot use this op to promote or demote anyone on their own team. Returns the TEAM record, the same shape team.get returns.
elaichi__role__listList the organization’s roles: id (`role_…`), name, description and the exact permission strings each one grants. This is the lookup you need before member.set_roles, and the way to see what a role currently means before role.update. Returns { result: [...], nextCursor, prevCursor } — camelCase here, unlike the REST API’s next_cursor. Page by sending nextCursor back as `cursor` until it is null.
elaichi__role__getRead one role by id, including its complete permission list. Read this before role.update, whose `permissions` field replaces that list wholesale. Returns the record directly, or null when the id is unknown — an unknown id is not an error here, so null means "no role has that id", never "the call failed".
elaichi__permission__listThe complete catalog of permission strings a role can grant, each with a human label and description. This is the ONLY legal source of values for role.create and role.update — names not in this list are rejected. Returns { result: [{ name, label, description, group }] } — `group` is the heading the settings UI files that permission under, useful for grouping them in an answer — and nothing else: no nextCursor, no prevCursor, and limit/cursor are ignored because the whole catalog is one short response.
elaichi__connector__listBrowse the catalog of third-party providers this organization can connect to (Slack, HubSpot, Google Calendar, …). A connector is a provider DEFINITION, not an account: nothing here is authenticated and nothing here is usable until someone creates a connection from it, so this cannot answer "what is our org connected to" — that is connection.list. Each row carries `slug` (the connector’s key everywhere else in this catalog — connectors have no id), label, category, description and `byoa` (true when the org must supply its own OAuth app first). No row carries a tool count: that lives on connector.get, one connector at a time, so "which connector has the most tools" cannot be answered from this list and must not be guessed from it. Every row carries `restricted` and, when it is true, `restricted_by` ("role" or "user") — governance has blocked that connector FOR YOU. A blocked connector is still listed, deliberately: presence here is not permission to use it, and absence means the provider genuinely is not in this catalog. Say the true one. When `restricted` is true, tell the person the connector exists and that they are blocked, and offer access_request.create with resource_type "connector" and the slug as `tool` — never claim no such connector exists, and never attempt connection.create or connector.list_tools on it, both of which refuse. `restricted_by` says only WHOSE rule layer blocked it, their role or their account; it never names the rule, who wrote it, or what else it covers, and neither may you. `search` and `category` are applied by the catalog before paging, so "is there a Slack connector" is one call rather than a walk through every page; `category` matches exactly, so take its value from the `category` of a row you have already seen rather than inventing one. Returns { result: [...], nextCursor } — no prevCursor on this one, so you can page forward but never back. `limit` defaults to 50 here, NOT to the whole catalog, and the catalog is far larger than on
elaichi__connector__getRead one connector definition by slug: label, category, description, supported auth formats, `tool_count` and whether it needs a customer-supplied OAuth app. No credentials and no tool schemas — for the tools themselves use connector.list_tools. It also returns `restricted`: true when governance has blocked this whole connector for YOU. Read it before anything else, because connector.list_tools refuses a blocked connector outright and this is how you learn it without provoking the refusal. `restricted: false` does NOT mean "nothing is restricted for me" — it is the connector-level signal only, and a caller with individual tools blocked still sees false. `tool_count` is the PROVIDER’S total and is never filtered: it counts what the connector offers, not what you may run. The two differ whenever a restriction applies, and that difference is not yours to compute or report — do not subtract the rows connector.list_tools returned from `tool_count` and describe the remainder, because the size of what governance hid from you is an inference this API never states. Returns the object directly, and errors when the slug is unknown.
elaichi__connector__list_toolsList the tools a provider exposes that YOU are allowed to run, with each tool’s full JSON Schema. It is the connector’s own catalogue — no account and no credential is involved, so it answers "what could Elaichi do with X" before anything is connected — but it is NOT account-independent and NOT unfiltered: the organization’s restrictions for the CALLER are applied first, so two members can get different answers for the same slug, and neither is the provider’s full catalogue. If governance has blocked the whole connector for you this REFUSES with a permission error rather than answering an empty list — an empty list would be a false statement about the connector, where a refusal is a true one about you; call connector.get and read `restricted` to find that out without provoking it. Restrictions are per-caller, not per-connection, so for the same slug this returns the SAME set connection.list_tools does — reach for that one when you hold a `conn_…` id or the question is about a real account, and this one when all you have is a slug. They refuse identically too — neither answers an empty list for a blocked connector — so an empty `tools` from either one means the provider genuinely has nothing to list. Returns { slug, tools: [...], nextCursor, prevCursor } — the rows are under `tools`, NOT under `result`, so this is not the standard list envelope. `limit` defaults to 200, which is every tool of almost every connector in one call; a non-null `nextCursor` means this connector has more, and you reach them by sending it back as `cursor` until it comes back null. Never report a page whose nextCursor is not null as the connector’s complete tool list — connector.get’s `tool_count` is the count. Every row carries a full JSON Schema, so prefer a narrow question to paging the lot.
elaichi__connection__listList the authenticated third-party accounts the caller can use — their own, plus anything explicitly shared with them or their teams. A connection with NO shares is private to its owner and is never listed for anyone else, whatever permission they hold — this cannot answer "what has Priya connected" and must not be reported as if it could; only its owner can. There is no org-wide listing: an org owner or admin sees exactly this same set, never every connection in the organization, whatever permission they hold. A connection is one account of one connector with a credential held in the vault; this is what "are we connected to Slack" means. Each row carries a `conn_…` id, name, connector_slug, `owner_user_id` with an `owner`: { id, name, email } beside it — say who owns a shared connection by name, never by id — and status: "active" is usable, "pending" means nobody ever finished the provider login, "needs_reauth" means it broke and connection.reconnect is the repair. Every row also carries `access_summary`, a BOUNDED who-else-can-reach-it rollup — { org_level, team_count, user_count, total, preview } — never a full grant list however many grants exist, so answer from its counts ("shared with 3 people and 1 team") and send the user to the connection’s Manage access tab when they need every name. There is no `shares` key on a row. Every row also carries `access_via` — how the CALLER reaches that connection: "owner", "direct" (shared with them by name), "team" (with `access_via_team`: { id, name } naming the team) or "org" (shared organization-wide). It is about the caller alone, never about anyone else, so it is present on every row including ones where `access_summary` is withheld — and it answers "why can I see this" without disclosing who else can. Credentials are never included. Narrow before paging rather than walking every page: `connector_slug` answers "do we have Slack" exactly (one slug, or several) and is the right choice when the caller names a provider, `ow
elaichi__connection__getRead one connection by id: id, name, display_label, account_label, connector_slug, status, and its owner — `owner_user_id` plus `owner`: { id, name, email } — never the credential. It carries NO grant information: there is no `shares` key and no `access_summary` on this response at all, so when the question is who else can reach the connection, use connection.list, whose every row carries `access_summary`. The standard use is confirming that `status` became "active" after a user opened a connect_url from connection.create or connection.reconnect — this is the ONLY operation that answers that, because connection.list_tools ignores status entirely. Returns the object directly and errors when the id is unknown or not visible to the caller — visible meaning owned or shared with them, and ONLY that: a connection:view/:manage permission does not widen this, so an admin auditing the org cannot read a connection nobody gave them access to.
elaichi__connection__list_toolsList the tools one authenticated account is ALLOWED to run: the connector’s catalog filtered by the restrictions in force for this caller. It does not look at the connection’s status — a "pending", "needs_reauth" or "disconnected" connection returns exactly the same list as a working one while actually running none of them — so this can never confirm that a connection authenticated, and a non-empty list here is not evidence that it did. Use connection.get and read `status` for that. Use this whenever you hold a `conn_…` id, and always when assembling toolbox entries, because an entry naming a restricted tool is rejected. It is no longer the only restriction-filtered view: connector.list_tools applies the same per-caller clamp, so for the same connector the two return the same set — what this one adds is that you name the ACCOUNT rather than the slug. If governance has blocked the whole connector for you, this REFUSES with a permission error rather than answering an empty list — the same as connector.list_tools, and the same as the REST route behind it. So an empty `tools` here means the provider genuinely has nothing to list, never "blocked for you"; read connector.get’s `restricted` to state the block without provoking the refusal. The connection itself stays describable either way — connection.get is not gated — so you can always explain WHY the listing refused. Returns { connection_id, connector_slug, tools: [...], nextCursor, prevCursor } — the rows are under `tools`, NOT under `result`, so this is not the standard list envelope. The restriction filter is applied BEFORE paging, so every row on every page is one this caller may actually run and the cursor never counts rows it would have hidden. `limit` defaults to 200, which is the whole allowed set for almost every provider; a non-null `nextCursor` means there are more, and you page by sending it back as `cursor` until it is null.
elaichi__connection__createStart connecting a third-party account. Creates the connection with status "pending" and returns { connection, connect_url }. Elaichi cannot finish the provider’s login on the user’s behalf — `connect_url` is a one-time connect session a HUMAN opens in a browser, and the connection only becomes "active" once they have. Never ask for, accept, or repeat a credential in chat — no operation here takes one. When you already know which app the user means, resolve connector_slug with connector.list first; whether you may omit it otherwise depends on the surface, see below. `shares` defaults to empty, which makes the connection private to the caller (needing only connection:create); passing any entries shares it with a person, a team, or the whole organization in the same call and additionally requires the connection:share permission — refused, not silently dropped, without it. Prefer leaving `shares` empty unless the user actually asked for a shared account; access can always be granted afterwards from the “Manage access” action on the Connections page in the Elaichi web app, which this catalog does not expose as its own operation. There is no Connect button and no connector picker here, so connector_slug is required — resolve it with connector.list before calling. Handing the link over is your job. The result repeats connect_url on its own line in a second content block. Put that URL in your reply, as a clickable link if you can, and tell the user to open it. Do not wait for them in this turn: end your reply, and call connection.get later to see whether status became "active".
elaichi__connection__reconnectMint a fresh connect_url for an existing connection whose status is "needs_reauth" (or still "pending"), and return it — the connection repairs itself once a HUMAN opens that one-time connect session and finishes the provider’s login, which Elaichi cannot do for them. Touches no credential and changes nothing else about the connection, so toolbox entries pinned to it keep working afterwards. Needs "edit" access to the connection, its ownership, or connection:manage over a SHARED one — connection:manage never reaches another member’s private connection (one with no shares). Returns { connection, connect_url }, the same shape connection.create returns. There is no Connect button here, so handing the repair link over is your job. The result repeats connect_url on its own line in a second content block. Put that URL in your reply, as a clickable link if you can, and tell the user to open it. Do not wait for them in this turn: end your reply, and call connection.get later to see whether status became "active".
elaichi__connection__renameChange a connection’s display name. Nothing else moves: the credential, its shares, status and every toolbox entry pinned to it are untouched. Returns the updated connection.
elaichi__connection__shareGrant a person, a team, or the whole organization access to a connection at one of three levels: "view" (see that it exists), "use" (also bind it into their own toolbox entries and run its tools), or "edit" (also rename it and start a reconnect) — even "edit" never lets the grantee transfer or delete the connection; that stays with the owner or connection:manage. `grantee_id` is required when grantee_type is "user" or "team" and must be omitted for "org" — the schema cannot express that conditional, so check it yourself. Re-sharing to the same grantee updates the level rather than adding a second grant; to take access away use connection.unshare, which identifies the grant the same way. This is the ONLY non-destructive way to give someone else access to your connection — connection.transfer instead REASSIGNS OWNERSHIP, which removes it from your own connection.list the moment it completes, so never reach for transfer when the user asked to "share", "give access to", or "let someone use" a connection. Returns the ACL entry plus { connection_id }; for a "user" share the entry also carries `grantee`: { id, name, email }, so you can confirm by name who was granted access.
elaichi__template__listList template DEFINITIONS visible to the caller: their own, plus every one explicitly shared with them, with one of their teams, or with the whole organization — and nothing else. No permission widens it. template:view and template:manage add nothing here, and this cannot answer "how many templates does the organization have". Unlike connections and toolboxes, templates carry no org-wide oversight path at all — not here, not in the REST API, not in the Elaichi web app: an org owner/admin sees a template only when they own it or it has been shared with them, whatever permission they hold. A template is a group of tools with frozen/default params; it NEVER references a connection, so it is not itself runnable — stamp a toolbox from one with toolbox.create (template_id). Ids are `tpl_…`. Returns { result: [...], nextCursor, prevCursor } — camelCase here, unlike the REST API’s next_cursor. Unlike the other lists in this catalog `limit` defaults to 200 rather than 50, so one call is the entire set for almost every organization. It is still a PAGE: a non-null `nextCursor` means there are more rows, and the only way to reach them is to send it back as `cursor` until it comes back null. A page whose nextCursor is not null is never a total.
elaichi__template__getRead one template definition and its entries. Returns a NESTED object — { template: { id, name, description, … }, entries: [...], access } — so the name is `result.template.name`, never `result.name`, and the entry list is `result.entries`; `access` is the caller’s own level ("owner" | "edit" | "use" | "view") — "use" is what authorizes toolbox.create with this id as template_id. Entries here never carry connection_id: a template pins nothing. Errors when the id is unknown or the caller cannot see it — visible meaning owned by them, shared with them or one of their teams, or shared org-wide, and ONLY that: template:view/:manage does not widen it, there is no `visibility` parameter here or on `GET /template/:id`, and an admin auditing the organization reads nothing they were not given. Unlike role.get, team.get and the governance reads, it never answers null, so there is no null to interpret. Always read this before template.set_entries, which replaces `entries` wholesale.
elaichi__template__createCreate a template: a named, shareable group of tool references with NO connection attached — other people (or the caller) later stamp their own connections into a toolbox from it (toolbox.create with template_id). `entries` is optional — create an empty one and fill it later with template.set_entries. Each proxy entry needs connector_slug + tool_name (never connection_id — that only exists after stamping), and `frozen_params` pins argument values callers cannot override. Take tool names from connection.list_tools, which is already filtered by the restrictions that apply here: entries are validated as a set, so one bad name rejects the whole call. `shares` grants access at creation time — see template.share for the level ladder; sharing a template exposes only tool NAMES and pinned parameters, never a connection, so (unlike toolbox.create) there is nothing to warn about. Returns the created template including its new `tpl_…` id.
elaichi__template__updateUpdate a template’s name or description (not entries — use template.set_entries). MERGE: fields you omit keep their current values. `description` takes a string, or null to clear it; any other type is rejected rather than silently wiping the field. Returns the updated { template, entries } detail — the same NESTED shape template.get returns, so the name is `result.template.name`, never `result.name`.
elaichi__template__set_entriesReplace a template’s entries: a WHOLESALE REPLACE, never a merge (name and description are untouched — those belong to template.update). The array you send becomes the template’s entire entry list, so "add the Slack tool" means reading template.get, resending its existing entries and appending yours; sending only the new entry deletes everything else. Each proxy entry needs connector_slug + tool_name — never connection_id, which a template cannot carry; `frozen_params` and `enabled` are optional. Validate names against connection.list_tools first — the set is validated as a whole, so one bad entry rejects the call and nothing changes. Toolboxes already stamped from this template are UNTOUCHED by this call (stamping copies entries once, it is never a live link). Returns the updated { template, entries } detail.
elaichi__template__shareGrant a person, a team, or the whole organization access to a template at one of three levels: "view" (see it), "use" (also stamp a toolbox from it), "edit" (also change it). `grantee_id` is required when grantee_type is "user" or "team" and must be omitted for "org" — the schema cannot express that conditional, so check it yourself. Re-sharing to the same grantee updates the level rather than adding a second grant; to take access away use template.unshare, which identifies the grant the same way. A template never references a connection, so sharing one never exposes anybody’s credential — contrast toolbox.share, whose response carries a `delegation` summary. Returns the ACL entry plus { template_id }; for a "user" share the entry also carries `grantee`: { id, name, email }.
elaichi__toolbox__listList the toolboxes the caller can see or run: their own, plus every one explicitly shared with them, with one of their teams, or with the whole organization — and nothing else. No permission widens it. toolbox:view and toolbox:manage add nothing here: unlike connections/templates/connectors, toolboxes have NO org-wide oversight listing at all, neither here nor `GET /toolbox` in the REST API (no `visibility` parameter exists for them), so this cannot answer "how many toolboxes does the organization have" and must not be reported as if it could — a caller who owns none and has been shared none gets an empty list however privileged they are. Always includes the virtual toolbox `global:{the caller’s usr_… id}` ("All tools"), which holds every tool they can run across every active connection and is the right default for execution; plus one `connection:{conn_…}` toolbox per active connection; plus every stored `tbx_…` toolbox. Ids here are therefore usually NOT `tbx_…` — pass whatever `id` you read back verbatim to toolbox.get and toolbox.execute. Only the stored `tbx_…` rows have grants and a lifecycle (toolbox.update/set_entries/share/unshare/delete); the `global:`/`connection:` ids are recomputed from scratch on every call and refuse mutation. A connection needing reauthentication contributes no tools; a stored toolbox whose entry has no connection pinned reports it in `needs_connection_count`. Returns { result: [...], nextCursor, prevCursor } — camelCase here, unlike the REST API’s next_cursor. Unlike the other lists in this catalog `limit` defaults to 200 rather than 50, so one call is the entire set for almost every organization. It is still a PAGE: a non-null `nextCursor` means there are more rows, and the only way to reach them is to send it back as `cursor` until it comes back null. A page whose nextCursor is not null is never a total.
elaichi__toolbox__getResolve one toolbox: its own record plus, when the caller’s access reaches "use", exactly what it can execute right now after overrides, restrictions and delegation are applied. Returns a NESTED object — { toolbox: { id, name, description, … }, entries: [...], access, delegation, tools?: [{ name, description, input_schema, … }] } — so the name is `result.toolbox.name`, never `result.name`. Entries pinned to a connection carry `delegated_by_user_id` (whose "use" grant the pin rides on) and, when resolvable, `unmet`/`tool_available`/`delegation_ok`/`connection_status`. `delegation` is the bounded summary of what this toolbox RUNS ON — answer "what does it run on?" from it rather than by counting `entries[].connection_id`, which misses every synthetic entry, since those pin nothing of their own and their tool’s steps hold the connections. `tools` is present only when `access` reaches "use" — a view-only grantee sees WHAT is configured, not the resolved, runnable surface. Errors when the id is unknown or the caller cannot see it — visible meaning owned by them, shared with them or one of their teams, or shared org-wide, and ONLY that: toolbox:view/:manage does not widen it, there is no `visibility` parameter here or on `GET /toolbox/:id`, and an admin auditing the organization reads nothing they were not given. Call this before toolbox.execute to get the exact `tool_name` and its argument schema — tool names are provider-specific and must not be guessed — and before toolbox.set_entries, which replaces `entries` wholesale.
elaichi__toolbox__createCreate a toolbox: a connection-bound, shareable bundle of tool references. Two ways to build one, combinable: pass `template_id` to STAMP it (copies that template’s entries at creation time — never a live link; needs "use" access to the template) with `connection_map` filling each stamped proxy entry’s connector from one of the caller’s own usable connections (a connector the map does not name is left "needs connection", not rejected); and/or pass `entries` for hand-written ones beyond whatever stamping copied, which get full validation including connection_id. `name` defaults to the template’s name when stamping and is otherwise required. `shares` grants access at creation time — see toolbox.share for the level ladder; sharing DELEGATES, so every response carries a `delegation` summary — { connection_count, delegator_count, visible: { connection_count, private_connection_count, preview } } — saying what this toolbox runs on: read it out before you share, not after. Returns { toolbox, entries, delegation } with the new `tbx_…` id. Not a prerequisite for running tools the caller already has — for that, toolbox.execute against `global:{their usr_… id}`.
elaichi__toolbox__updateUpdate a toolbox’s name or description (not entries — use toolbox.set_entries). MERGE: fields you omit keep their current values. `description` takes a string, or null to clear it; any other type is rejected rather than silently wiping the field. Only a STORED id may be updated — the `global:…`/`connection:…` dynamic ids from toolbox.list are recomputed from scratch on every call and are refused here. Returns the updated { toolbox, entries } detail — the same NESTED shape toolbox.get returns, so the name is `result.toolbox.name`, never `result.name`.
elaichi__toolbox__set_entriesReplace a toolbox’s entries: a WHOLESALE REPLACE, never a merge (name and description are untouched — those belong to toolbox.update). The array you send becomes the toolbox’s entire entry list, so "add the Slack tool" means reading toolbox.get, resending its existing entries and appending yours; sending only the new entry deletes everything else. Each proxy entry needs connector_slug + tool_name; pin connection_id for multi-connection toolboxes, which must be one the ACTING caller can use — they become its delegator (docs/access-model.md §6). An incoming entry whose (connection_id, tool_name) pair matches a PRE-EXISTING one keeps that row’s original delegator rather than re-stamping the caller, so merely re-ordering entries never silently reassigns whose authority a pin rides on. `frozen_params` and `enabled` are optional. Validate names against connection.list_tools first — the set is validated as a whole, so one bad entry rejects the call and nothing changes. Only a STORED id may be updated — dynamic ids from toolbox.list are refused. Returns the updated { toolbox, entries } detail.
elaichi__toolbox__shareGrant a person, a team, or the whole organization access to a toolbox at one of three levels: "view" (see it), "use" (also RUN its tools), "edit" (also change it). `grantee_id` is required when grantee_type is "user" or "team" and must be omitted for "org" — the schema cannot express that conditional, so check it yourself. Re-sharing to the same grantee updates the level rather than adding a second grant; to take access away use toolbox.unshare, which identifies the grant the same way. "use" DELEGATES: the grantee runs tools through each entry’s pinned connection under that entry’s DELEGATOR’s own authority (the editor who pinned it), never their own — and that reaches connections pinned directly AND connections reached through a synthetic entry’s steps. The response repeats the toolbox’s `delegation` summary, a bounded rollup of what the grantee can now run through: say what it discloses in your own words rather than pasting ids. Returns the ACL entry plus { toolbox_id, delegation }; for a "user" share the entry also carries `grantee`: { id, name, email }, so you can confirm by name who was granted access (a "team" or "org" share has no `grantee`).
elaichi__toolbox__executeRun one real third-party action through a connection this toolbox reaches — send the Slack message, create the HubSpot contact, read the calendar — with the organization’s restrictions and the approval gate applied. This is the operation that reaches outside Elaichi: it does whatever the underlying provider tool does, and the provider may make it irreversible, so confirm the target and the arguments before calling it. `toolbox_id` comes from toolbox.list, where `global:{the caller’s usr_… id}` holds every tool they can run and is the right default; `tool_name` must match a `name` returned by toolbox.get for that same id exactly; `arguments` must satisfy that tool’s own input schema (send {} when it takes none). A STORED toolbox the caller does not own but reaches only via a "use" share still runs — that is delegation: the call executes through the pinning editor’s connection authority, not the caller’s own, and if that authority is later revoked the tool simply disappears from toolbox.get, unmet, rather than failing here. Returns the provider’s raw response, whose shape is defined by the provider, not by Elaichi. Use synthetic_tool.execute instead for a saved multi-step Elaichi tool. The same tools are usually advertised here individually under their own `{account}__{tool}` names too, or behind search_tools and execute_tool once there are many of them. Those reach the same accounts at the same cost — but they are a SEPARATE namespace, not aliases, so pick one path and stay inside it. This operation’s `tool_name` must be a name toolbox.get returned for this same toolbox_id; passing an advertised `{account}__{tool}` name, or one search_tools handed you, fails with a bare "Tool not found: <name>" that says nothing about the namespace being wrong. To use the other paths, call the advertised tool directly by its own name, or pass a search_tools result to execute_tool.
elaichi__synthetic_tool__listList synthetic tools: multi-step tools defined inside Elaichi — a named input schema over a chain of connector calls — as opposed to the provider tools that come from a connection. Ids are `syn_…`. Reading and executing are two different gates here, so never infer one from the other. READING: OWNER-SCOPED, and no permission widens it. A synthetic tool carries no ACL — there is nothing to be "shared with me" — so this is exactly the caller’s own tools; toolbox:view and toolbox:manage add nothing, here or on `GET /synthetic-tool`, so this cannot answer "how many synthetic tools does the organization have" and must not be reported as if it could. A tool missing from this list may therefore mean "not yours" rather than "does not exist" — and synthetic_tool.get answers a tool you may not read with a 404 for the same reason. EXECUTING is no wider: only the tool’s OWNER may run one, because a synthetic tool’s steps run over the owner’s own connections with no re-check at execution. Returns { result: [...], nextCursor, prevCursor } — camelCase here, unlike the REST API’s next_cursor. Unlike the other lists in this catalog `limit` defaults to 200 rather than 50, so one call is the entire set for almost every organization. It is still a PAGE: a non-null `nextCursor` means there are more rows, and the only way to reach them is to send it back as `cursor` until it comes back null. A page whose nextCursor is not null is never a total.
elaichi__synthetic_tool__getRead one synthetic tool, including the `input_schema` its caller must satisfy and the steps it runs. Returns the record directly. OWNER-ONLY: a caller who does not own the tool gets a 404, not a permission error, even holding toolbox:view or toolbox:manage — not being allowed to see a tool and the tool not existing are deliberately indistinguishable, so never report a 404 here as proof the id is wrong. There is no `visibility` parameter on this operation or on `GET /synthetic-tool/:id`: a synthetic tool’s body is the most private thing the product stores about a member (the ids of connections nobody else can see, literal `args_template` text, the output JSONata), and no permission opens it. Reading one is also not permission to RUN it: synthetic_tool.execute needs the same ownership. Read this before synthetic_tool.execute — the input schema is author-defined and cannot be guessed.
elaichi__synthetic_tool__executeRun a saved multi-step Elaichi tool. Its steps call real third-party connections, so this has the same real-world, possibly irreversible side effects as toolbox.execute — with several actions per call. `input` must satisfy the tool’s own input_schema from synthetic_tool.get. Only the tool’s OWNER may run one — and ownership is the gate on every other synthetic-tool operation too (read, edit, delete): no permission reaches somebody else’s. Returns the tool’s mapped output on success; a failing step returns an error naming that step, and steps that already ran are not rolled back. Use toolbox.execute for a single provider tool.
elaichi__synthetic_tool__createAuthor a new multi-step Elaichi tool: a named input_schema plus a DAG of steps, each calling one tool on one of YOUR OWN usable connections. Every step needs connection_id, tool_name and a JSONata args_template over { input, steps } — args_template must PARSE here but is not evaluated until synthetic_tool.execute, so a template that references a field the input_schema never declares still saves clean. depends_on orders the DAG; independent steps run in parallel at execute time. The whole definition is validated as one unit before anything saves — duplicate/unknown/cyclic step keys, bad JSONata, an input_schema whose type is not "object", an unknown connection, an unknown tool on that connection's connector, or a connection you cannot at least "use" (docs/access-model.md §6) all reject the call and save nothing. Name is sanitized into a lowercase_underscore MCP tool identifier and must be unique in the organization (a collision after sanitizing is a conflict, not a silent rename) — read back the returned `name`, which may differ from what you sent. You become this tool's OWNER, which is permanent: nothing here or elsewhere transfers a synthetic tool to somebody else, and no permission ever widens who may read, edit, execute or delete it beyond you (synthetic_tool.get/.execute/.update/.delete). Pin the created id into a toolbox with toolbox.set_entries (entry `type: "synthetic"`) to make it runnable by anyone the toolbox is shared with — at that point THEIR calls still run under YOUR connection authority, never theirs. Returns the created record, the same shape synthetic_tool.get returns.
elaichi__synthetic_tool__updateEdit a synthetic tool's definition — name, description, input_schema, steps or output_template. MERGE at the top level: fields you omit keep their current value. Sending `steps` is a WHOLESALE REPLACE of the step list, same as toolbox.set_entries — resend every step you want to keep, not only the ones you changed. The merged definition (your patch layered over what is stored) is re-validated as a whole exactly as synthetic_tool.create validates a new one, so an unrelated field edit can still fail if it leaves the stored steps inconsistent with a new input_schema. A step whose (connection_id, tool_name) pair is unchanged from what is already stored rides forward without re-checking your access to that connection — protects an honest read-modify-write from being refused by a restriction written after the step was first saved — but any step that is new, or that changes tool_name while keeping connection_id, is validated fresh against your CURRENT access and restrictions. OWNER-ONLY, same as every other synthetic-tool operation: this needs toolbox:create or toolbox:manage to call at all (the coarse permission gate PATCH /synthetic-tool/:id enforces), but that permission is not the real gate — unlike toolbox.update, a toolbox:manage holder who does not own this tool still gets the exact same 404 an unknown id gets, never access. Read synthetic_tool.get first: this endpoint does not tell you what changed, only the result after. Returns the updated record, the same shape synthetic_tool.get returns.
elaichi__invite__listList organization invitations nobody has redeemed or withdrawn — hardcoded, so accepted and revoked invites are never returned and their absence means nothing. Each row carries a server-computed `status`: `pending` while its link still works, `expired` once it ran out of time. An expired one is listed on purpose — it is the invitation a person is still waiting on, and it can be revoked (invite.delete) or replaced with a fresh one (invite.create); only RESENDING the exact same invitation (same link, new expiry) is a human action in the Elaichi web app, with no AI-surface equivalent. Ids are `inv_…`, and they are what invite.delete takes. For people who already joined, use member.list. Returns { result: [...], nextCursor, prevCursor } — camelCase here, unlike the REST API’s next_cursor. Page by sending nextCursor back as `cursor` until it is null. Sensitive read: returns records an operator treats as confidential (audit history, credential metadata, offboarding detail). Read-only, but not harmless — do not echo the contents further than needed.
elaichi__invite__createInvite somebody who is not yet a member into this organization by email: mints a fresh accept link and emails it to them, the same as the Invite button in Settings → People. A member holds exactly ONE role, so role_ids must carry exactly one id (role.list) — the role they land with the moment they accept, the same cardinality member.set_roles enforces; team_ids is optional and puts them straight into those teams on acceptance. You cannot invite someone at a role that carries a permission you do not hold yourself — the same ceiling member.set_roles enforces. Refuses with a conflict when the address already belongs to a member of this organization; check member.list first if you are not sure. Some addresses are refused for reasons this operation will not disclose — treat that refusal as final rather than retrying the same address a different way. Returns the created invite record — id (`inv_…`), email, role_ids, team_ids, status, expires_at — plus `email_sent`; the raw accept link itself is never returned here, unlike the console’s copy-link view, because the email IS the delivery channel on this surface — if `email_sent` is false, tell the user delivery failed and that resending the SAME invitation is a human action in Settings → People, but inviting the address again from here is not blocked. To revoke a pending invitation, use invite.delete; to see who is still waiting, invite.list. NOT CALLABLE FROM ANY AI SURFACE, in-app agent or MCP: an invitation is a redeemable credential for an address the caller names, so it needs step-up reauthentication (confirming it is really you), which neither surface can ask for — every call from here is refused before anything is written or emailed. Say what needs doing and tell the user to do it in Settings → People in the Elaichi app.
elaichi__access_request__createFile a request asking an org admin to grant access to a tool that just refused you — the operation behind a "blocked" result’s can_request_access flag. Only ever call this right after a tool call came back refused for `permission` or `restriction` (never for `scope` — that refusal is between you and your own client and no admin can fix it, so it never carries can_request_access). Pass the exact `tool` name and `reason` the refusal named, and the exact `permission` string too when reason is "permission" — never invent or guess a permission, and never pass one at all when reason is "restriction" (the request record must not become a way to learn which rule blocked you, so a restriction-reason call that names a permission is rejected). Filing twice for the same thing while the first request is still open does not create a second one — it returns the existing request (its own `status` tells you whether that was a fresh file or a repeat), so it is safe to call again if you are unsure whether one already exists. This only creates a RECORD for a human to review; it does not itself grant anything, and nothing about a tool becomes usable because you filed a request. Returns the request with `can_withdraw`.
elaichi__feedback__createReport a BUG to Elaichi — a connected app that behaved wrongly, or something broken in Elaichi itself. This is for defects, not for access: a tool that REFUSED you for permission or restriction is access_request.create, and a tool that is merely missing from your surface is usually search_tools, not a bug. Call it when the person says something is broken, wrong, or not doing what it should, and when a call failed for a reason that is nobody’s fault at this end — a vendor error, an empty result that should have had rows, a field that came back wrong. PASS WHAT YOU ALREADY HAVE, and do not interrogate the person for it: `connector_slug` and `tool_name` from the call that misbehaved, and `error_text` as the tool returned it, VERBATIM — a report carrying those three is actionable on sight, and one that says only "Notion is broken" is not and will be closed unanswered. Never paste the arguments you sent: they are the user’s own data, and the tool name plus the error is what identifies the bug. `summary` is what the PERSON was trying to do, in their words, one sentence. Filing twice for the same tool while your first report is open does not create a second one — it returns the existing report and `created` comes back false, so say "you already reported this" rather than implying a fresh one went in. This creates a RECORD for Elaichi to read; it does not fix anything, it cannot make the tool work, and you must not promise a timeline for it.
elaichi__access_request__listAccess requests members have filed — the ADMIN QUEUE by default, or the CALLER’S OWN requests with `mine: true`. The default needs member:manage (the same permission that gates inviting people and changing roles, because acting on a request always ends in one of those) and is refused without it; `mine: true` needs nothing beyond org membership, the same nothing that filing a request needed, and is how the person who filed one finds it again. Use `mine: true` whenever the question is about the user’s own asks ("did my request go through", "what have I asked for") — the default answers a different question and will refuse most people. `status` defaults to "pending" (the open queue) when omitted; pass "all" to see the full history including withdrawn ones. `q` matches the requested TOOL name or the requester’s own `note` (not the requester’s name — resolve that from the `requester` field on each row instead, and not `tool_label`, which is resolved outside the searchable store). Admin-queue rows carry `requester` and, once resolved, `resolved_by` — both { id, name, email } — plus `can_resolve`, which is false on the caller’s OWN row: nobody decides their own request, so offer access_request.withdraw there instead of a decision — that row’s `can_withdraw` is true, because a queue row carries both capabilities. `mine: true` rows are the requester’s own narrower shape instead — `can_withdraw`, and a bare `resolved_by_user_id` rather than a resolved name, because resolving an id into a name and email is a member-directory lookup this view does not hand a plain member for free. Returns { result: [...], nextCursor, prevCursor } — camelCase here, unlike the REST API’s next_cursor. Page by sending nextCursor back as `cursor` until it is null. Sensitive read: returns records an operator treats as confidential (audit history, credential metadata, offboarding detail). Read-only, but not harmless — do not echo the contents further than needed.
elaichi__access_request__getRead one access request by id. No permission gate here — like the REST route it mirrors, visibility is computed per-caller instead: an admin (member:manage) can read any request, and anyone else can read only their OWN, with `resolved_by` resolved to a name/email for an admin and left as a bare id otherwise (the same split GET /access-request/:id documents — resolving that id into a profile is a member-directory lookup this operation does not hand a plain member for free). Everyone else’s request is a 404, not a permission error — its existence is not something a bystander gets to learn. Returns the record with `can_resolve` for an admin or `can_withdraw` for the requester. Sensitive read: returns records an operator treats as confidential (audit history, credential metadata, offboarding detail). Read-only, but not harmless — do not echo the contents further than needed.
elaichi__access_request__resolveApprove or deny a pending access request that SOMEONE ELSE filed. Needs member:manage, and refuses (403) a request the caller filed themselves however many permissions they hold — a decision is a record that a second person reviewed the ask, and one signed by its own requester reads downstream exactly like one that was not. An admin who no longer needs what they asked for uses access_request.withdraw on their own row instead. THIS RECORDS A DECISION — IT DOES NOT GRANT ANYTHING, with one exception that is the reason this is refused here: approving a reason:"restriction" request in the Elaichi web app now actually LIFTS the restriction it names, and restrictions are read-only on this surface (see restriction.list), so that must never happen by the back door. Approving a reason:"permission" request only sets this request’s own status and an optional note back to the requester; the requester’s role and restrictions are completely untouched, and nothing about what they can call changes because of this call. To actually widen what they can do, follow up with member.set_roles or the restriction APIs. Refuses (error) a request that has already left "pending" — a decision is recorded once. Returns the resolved request. NOT CALLABLE FROM ANY AI SURFACE, in-app agent or MCP: this needs step-up reauthentication (confirming it is really you), which neither surface can ask for, so every call from here is refused — denials included, since the gate is on the route and not on the decision. Say what needs doing and tell the user to do it in Governance → Access requests in the Elaichi app.
elaichi__access_request__withdrawCancel your own pending access request — e.g. because you no longer need the tool, or you got access another way. Only the request’s own requester may withdraw it, and only while it is still "pending"; anyone else’s id, or a request that was already approved/denied/withdrawn, is refused. Returns the withdrawn request.
search_toolsFind a tool among the user’s connected third-party apps. This is the ONLY way to reach them: they are never listed in tools/list, however few of them there are, so a name you cannot see here is a name you do not have. This connection reaches: Airtable (Pranasri), Google (Pranasri). It indexes THOSE tools and nothing else: it never returns an elaichi__ operation — those administer Elaichi itself and stay listed individually, so an empty result here says nothing about them — and a synthetic (multi-step) tool is here only when it is an entry of a toolbox you can reach; a standalone one is listed by elaichi__synthetic_tool__list and run by elaichi__synthetic_tool__execute. Searching runs entirely inside Elaichi and touches no third party, so it is cheap and safe to call. Returns each match with its exact name, description and input schema. Ranking is lexical over the name, the app and the description, so query with concrete tool-ish words like "create deal" rather than a sentence. Omit the query to browse the first tools instead. Results are paged: a result that reports more matches than it returned carries a `next_cursor`, and passing it back as `cursor` gets the next page — so a tool you did not see on page one has not been ruled out. Then call execute_tool with a name exactly as returned, never one you have reformatted or guessed.
execute_toolRun one tool in one of the user’s connected third-party apps, by its exact name from search_tools. This makes a live call to that app under the user’s own credential, so it can create, change, send or delete real things there, and the app decides whether that can be undone. Confirm the target and the arguments with the user before calling it for anything other than a read. `arguments` must satisfy the schema search_tools returned for that exact name. If that schema has a `connection` property, the user has several accounts of that app connected: set it to one of its enum values, copied exactly, and ask the user which account they mean whenever their request does not say.