Skip to content

MCP tools

Every Exponential instance is an MCP server. Agents in sessions get it wired in by the launcher; any other client connects with the URL below.

https://app.exponential.at/api/mcp

Self-hosted: https://<your-instance>/api/mcp. Streamable HTTP only.

OAuth (interactive clients): add the URL with no credentials; the client registers itself and opens your browser on a scope picker (everything, some teams, or some boards). Run consent again to change the grant. API key (scripts, CI): create one under Settings → Security and send Authorization: Bearer expu_…. A key without a scope acts as you with your full membership; a scoped key works for MCP only.

client setup
Claude (desktop, claude.ai) Settings → Connectors → Add custom connector, paste the URL, Connect. Connectors dial from Anthropic’s cloud, so the instance must be reachable from the internet.
ChatGPT Settings → Apps & Connectors → Advanced → Developer mode, then Create connector with OAuth.
Claude Code claude mcp add --transport http --scope user exponential https://app.exponential.at/api/mcp, then /mcp to sign in (or add --header "Authorization: Bearer expu_…").
Codex CLI codex mcp add exponential --url https://app.exponential.at/api/mcp and codex mcp login exponential; or bearer_token_env_var in config.toml.
Cursor {"mcpServers": {"exponential": {"url": "https://app.exponential.at/api/mcp"}}} in ~/.cursor/mcp.json, then Connect.
OpenClaw openclaw plugins install clawhub:@exponential/openclaw-plugin with EXPONENTIAL_API_KEY, or openclaw mcp add … --auth oauth.
stdio-only clients npx mcp-remote https://app.exponential.at/api/mcp

Clients that render MCP Apps show exponential_issues op show, exponential_notifications op list and exponential_devices op list as Exponential’s own views.

Session content is end-to-end encrypted: a client connected straight to /api/mcp sees a session’s metadata (who, when, state) and none of its text.

A team’s eApps expose their tools at /api/mcp/apps, each as <app>__<tool>, running as you. Connecting asks for its own consent: a team, all apps or a pick, then Read only or Full. Owners set each member’s access per app and can switch single tools off.

Owners add the team’s other MCP servers under Settings → MCP servers; each member connects their own account once (Connect or Set key), and the server holds that credential for every device. Pick the servers a session gets in the composer’s + menu. On a headless box: exponential mcp list|connect|set-secret|disconnect (CLI).

22 tools in 7 groups, one per noun. A call carries an op and that op’s fields beside it: exponential_issues {op: "get", id: "EXP-42"}. Every caller gets the same tools; an op that needs a session or the cloud answers with a message saying so. Calls stay inside the OAuth grant or the API key’s scope. Issue parameters take an identifier (EXP-42) wherever they take a UUID. Parameters with ? are optional. Tools marked always loaded are in an agent’s context from its first turn; the rest load through tool search.

Always loaded.

Issues (id = UUID or identifier like EXP-12; list = open only unless includeClosed or a status filter; widget = reporter data). create links only via sessionId or link: true.

  • list{boardId?, boardIds?, teamId?, status?, statusId?, statusCategory?, excludeStatus?, excludeStatusId?, excludeStatusCategory?, includeClosed?, priority?, assigneeId?, source?, labelIds?, labelMatch?, unlabeled?, hasComments?, commentedBy?, notCommentedBy?, createdAfter?, createdBefore?, updatedAfter?, updatedBefore?, dueAfter?, dueBefore?, search?, sort?, limit?, offset?}: List issues, OPEN only: completed/cancelled/duplicate need includeClosed or a status* filter. Descriptions cut at 200. statusId: exponential_statuses {op: “list”}; exclude* invert. created*/updated*: ISO. sort: [-]createdAt|updatedAt|priority. search: text + identifier; assigneeId null = unassigned; source: user|widget.
  • get{id, commentsLimit?}: Get a single issue by UUID or identifier (e.g. “MET-12”), including its label ids and latest comments (newest first, capped at 50; commentsLimit overrides).
  • show{boardId?, teamId?, assigneeId?, search?, includeClosed?, limit?}: Show issues as an interactive list where the client renders MCP Apps (OpenClaw, Claude, ChatGPT); elsewhere the same JSON as exponential_issues_list. OPEN only unless includeClosed.
  • create{boardId, title, parentId?, sessionId?, link?, status?, statusId?, priority?, assigneeId?, assigneeAgentId?, description?, dueDate?, estimate?, labelIds?}: Create a new issue in a board the MCP user has access to. Description must be plain text (no embedded images on creation). For a custom status pass statusId (not status); see exponential_statuses {op: “list”}. parentId files it as a sub-issue of that issue. Unlinked unless sessionId (that session holds it) or link: true (your own session; link: false with sessionId is refused); filing never moves its status, a later link or start does. The result’s sessionId = the session it linked to, null = none.
  • update{id, title?, status?, statusId?, priority?, assigneeId?, assigneeAgentId?, description?, dueDate?, estimate?}: Update an issue’s fields (by UUID or identifier, e.g. “MET-12”). Pass only the fields you want to change. For a custom status pass statusId (not status); see exponential_statuses {op: “list”}.
  • delete{id}: Permanently delete an issue (by UUID or identifier). Cascades to its labels, attachments, comments, and relations. Attachment storage objects are also removed.
  • activity{issueId, since?, limit?}: Issue activity history, oldest first: {field, from, to, actorId, actorKind user|agent|automation, sessionId, at} per change (status ids, assignee, label, priority, estimate, title, board, pr, relation). since: ISO, at or after (the last row’s at; rows sharing that instant repeat, dedupe by id).
  • update_status{id, status?, statusId?}: Set an issue’s status (UUID or identifier). Pass status (builtin enum) or statusId (a team status row from exponential_statuses {op: “list”}). Status changes are normally AUTOMATIC — PR open/merge apply the team’s configured status automation — so set one directly only when the user explicitly asks.
  • subscribe{issueId}: Subscribe the MCP user to an issue (by UUID or human identifier, e.g. “MET-12”) so they receive its notifications. Idempotent.
  • unsubscribe{issueId}: Unsubscribe the MCP user from an issue (UUID or identifier). Suppresses auto-resubscribe until they act on the issue again.
  • unlink{id, sessionId?}: Unlink an issue from a session (sessionId, default your own; else whichever session of its thread holds it). Errors when none held it. An already-open PR keeps it: exponential_pr {op: “unlink”}.
  • labels_add{issueId, labelId}: Attach a label to an issue (UUID or identifier; teams must match).
  • labels_remove{issueId, labelId}: Detach a label from an issue (UUID or identifier).
  • relations_add{issueId, relatedIssueId, type, inverse?}: Link two issues (UUIDs or identifiers, same team). Stored one way: blocks = issueId blocks relatedIssueId, parent = issueId is the parent, duplicate = issueId duplicates relatedIssueId, related is symmetric. Pass inverse:true to state it the other way round (blocked by / sub-issue of / duplicated by).
  • relations_remove{issueId, relatedIssueId, type}: Unlink two issues. Name the pair in the direction the link is stored (see exponential_issue_relations_add); exponential_issues_get lists them.

Always loaded.

Comments on an issue (plain GFM; threads one level deep; audience reporter emails a widget reporter; a report’s reporter sees only that thread).

  • list{issueId, limit?, offset?}: List comments on an issue (oldest first) by UUID or human identifier (e.g. “MET-12”). Rows include their linked attachments. The MCP user must have access to the issue’s team.
  • create{issueId, body, attachmentIds?, parentId?, audience?}: Comment on an issue (UUID or identifier, e.g. “MET-12”) as the MCP user (“via MCP”); body = plain GFM. parentId (from exponential_comments_list) replies under that comment; threads are one level deep. audience “reporter” (top-level, a widget issue with a reporter email) also emails it; reporterEmailed says whether it went out.
  • update{id, body, attachmentIds?}: Edit the body of an existing comment (by its UUID). Only the comment’s author can edit it. Body is plain text; the edit stamps editedAt.
  • delete{id}: Permanently delete a comment (by its UUID). Only the comment’s author can delete it.

Always loaded.

Pull requests (name one by pullRequestId, issueId or repositoryId + prNumber; default = your session’s newest open PR): open, merge, close, retarget, update, unlink an issue, files. Every reply carries checks (the PR’s CI); CI finishing wakes the PR’s session, so never poll GitHub.

  • open{title, body?, head?, base?, draft?, repositoryId?, issueIds?, notRun?}: Open a GitHub PR (via the GitHub App, never ‘gh’) from head (default your session’s branch) into base (default: the open PR below head, else the default branch; an open PR’s branch joins its stack). issueIds (UUIDs or identifiers) = the issues it closes. Omitted = your session’s SHARE of the thread, not the root’s whole list: the issues your session or its children linked plus the root issues your start message names as #IDENT (the root’s own PR takes them all; no share links none; issues a merged PR closed are left out). repositoryId when there are none. Body = a server summary + your story pictures. A UI diff needs a story picture per platform (exponential_show pictures, label starts web/web-mobile/ios/android/desktop) or notRun naming the screen you could not run. Linked issues (the result’s issues) take the team’s PR-open status.
  • merge{pullRequestId?, issueId?, repositoryId?, prNumber?, stack?, force?}: Squash-merge an open PR (via the GitHub App). Name it by pullRequestId, issueId or repositoryId + prNumber; default = your session’s newest open PR. stack lands a stack member and every open PR beneath it. Merging your own PR never ends your session. Merge a tree root first. When the server gates on CI, a red or running check refuses it; force: true merges anyway (only when a person says so).
  • close{pullRequestId?, issueId?, repositoryId?, prNumber?}: Close an open PR without merging (via the GitHub App), e.g. one superseded by another PR. Its issues keep their status. Name it by pullRequestId, issueId or repositoryId + prNumber; default = your session’s newest open PR. Only when a person asks.
  • retarget{pullRequestId?, issueId?, repositoryId?, prNumber?, base?}: Change an open PR’s base branch (omitted = the repo’s default branch), e.g. when its base PR merged. Name it by pullRequestId, issueId or repositoryId + prNumber; default = your session’s newest open PR. Then rebase onto the new base and push with –force-with-lease.
  • update{pullRequestId?, issueId?, repositoryId?, prNumber?, title?, body?, draft?}: Rewrite an open PR’s title or body, or flip draft. Name it by pullRequestId, issueId or repositoryId + prNumber; default = your session’s newest open PR.
  • unlink{pullRequestId?, repositoryId?, prNumber?, issueId}: Unlink one issue (UUID or identifier) from a PR; its status stays as is and no issue event is written. The thread keeps the issue (exponential_sessions_unlink drops it), so a later pr_open without issueIds on this branch re-links it. Name the PR by pullRequestId or repositoryId + prNumber; default = your session’s newest open PR.
  • files{pullRequestId?, issueId?, repositoryId?, prNumber?}: List a PR’s changed files with patches and add/delete counts. Name it by pullRequestId, issueId or repositoryId + prNumber; default = your session’s newest open PR.

Always loaded.

THE one call for sessions. sessionId = message it (idle resumes; an ended thread resumes the row, an ended child continues as a sibling; answer = answer its open question/plan {answer|answers|decision|dismissed}, first wins; issueIds = forward; account/model/effort = set; deviceId = move; stop = interrupt its turn). agentId = message a team agent (your conversation with it); appId = your app editor session. parentId = start a child. Neither = a new thread (text, issueIds, promptId = a saved prompt’s body first, repositoryId); its transitions wake you, as do wakeOn sessions. New threads need deviceId; teamId defaults to yours. Never resend, never wait.

Parameters: {teamId?, sessionId?, agentId?, appId?, parentId?, deviceId?, cli?, account?, model?, effort?, planMode?, computerUse?, ultracode?, mcpServerIds?, name?, text?, attachmentIds?, issueIds?, promptId?, repositoryId?, answer?, wakeOn?, mentionUserIds?, refIssueIds?, sealedName?, seal?, stop?}

Always loaded.

ONE card in your chat a turn (result, status, draft, choice); replace: true edits it. surface = nested nodes {id, component, props?, children?, slots?, on?} + data. The chat draws the card: never a Card root; a Stack/List of real components (Row, Badge, StatTile, Table…); text only = a plain message. E.g. {root:{id:“s”,component:“Stack”,children:[{id:“r”,component:“Row”,props:{title:“Tests”},slots:{trailing:{id:“g”,component:“Badge”,props:{text:“Green”}}}},{id:“b”,component:“Button”,props:{label:“Ship”},on:{press:{event:{name:“reply”,context:{text:“Ship”}}}}}]}}: a reply event = the answer. Components: Box Stack Grid Card Separator List Row Section Text Heading Markdown CodeBlock Label Image Icon Avatar Carousel Tabs Accordion Stepper Badge Alert Chip Progress Meter Ring EmptyState Table Chart Sparkline StatTile Button Link Toggle Form Input Textarea Checkbox Radio ChoiceTiles Switch Select Slider; props: exponential_apps {op:“catalog”, only:[…]}. ask = one question (options; multi; custom free text, default on). plan = text (GFM) + questions [{id, prompt, options}]. pictures = labels (screenshots, platform first: ‘web chats’; options; a in plan.text); run each curl. Ask/plan/pictures are stories (PR pictures; +description); story: false = chat only. After ask/plan end your turn; the answer comes next.

Parameters: {surface?, title?, replace?, ask?, plan?, pictures?, description?, story?, sealedCard?, kind?, askText?, listenerCopies?}

Boards of a team: list, get, create, update, delete (trash), set_repository.

  • list{teamId?, limit?, offset?}: List boards in a team, or across all teams the user belongs to.
  • get{id}: Get a single board by id.
  • create{teamId, name, prefix, color?, icon?, repository?, defaultBranch?}: Create a board in a team (member; owner/admin to connect a new repo). The repository is optional. Coding features gate on repo presence. Pass repository.repositoryId (registry repo) or repository.fullName (“owner/name”) to connect one inline; defaultBranch pins the branch this board’s coding sessions branch from and its PRs target (omit = the repo’s default). icon is a curated icon name.
  • update{id, icon?, name?, color?, defaultBranch?}: Update a board’s name, color, icon, or defaultBranch (the branch its coding sessions branch from and its PRs target; null = follow the repo’s default).
  • delete{id}: Move a board to the trash (owner only; by its UUID). Purged with all issues after 48 hours; owners can restore from web settings before then.
  • set_repository{id, repositoryId, defaultBranch?}: Point a board (by its UUID) at a different registered repository (both must be in the same team), or pass repositoryId: null to detach it. Owner/admin only. Existing worktrees keep working; new coding sessions use the new repo. The board’s branch pin resets unless defaultBranch is passed.

A team’s issue labels: list, get, create, update, delete.

  • list{teamId, limit?, offset?}: List labels for a team.
  • get{id}: Get a label by id (must be in a team the user belongs to).
  • create{teamId, name, color?}: Create a label in a team.
  • update{id, name?, color?}: Update a label’s name or color (by its UUID). Returns the updated label.
  • delete{id}: Delete a label (by its UUID).

A team’s issue statuses (each in an anchor category): list, create, update, delete.

  • list{teamId, limit?, offset?}: List a team’s issue statuses (id, name, category, color, position, builtinKey). Use id as statusId in exponential_issues_update.
  • create{teamId, category, name, color}: Create a custom issue status in a category (never duplicate; started allows at most 4 rows per team). Names are unique per team, color is #rrggbb. Team members only; ids via exponential_statuses {op: “list”}.
  • update{id, name?, color?}: Rename or recolor a custom issue status (by its UUID). Builtin statuses (builtinKey set) are locked and the category is immutable. Team members only. Returns the updated status.
  • delete{id, reassignToId?}: Delete a custom issue status (by its UUID). Builtins refuse. When issues still use it the call fails with their count until reassignToId names a same-team replacement (not duplicate). Team members only.

Files on an issue: list, get (metadata + contents or a signed URL), upload (bytes now, or a signed uploadUrl then finalize; no issueId = a loose file for sessions_send attachmentIds), delete.

  • list{issueId, limit?, offset?}: List an issue’s attachments (UUID or identifier), newest first: id, filename, contentType, sizeBytes, createdAt and the commentId when a file hangs on a comment. Metadata only; exponential_attachments_get fetches one file’s bytes. Paged with limit/offset; total counts every row.
  • get{id}: Fetch an attachment by id — every content type. Markdown embeds look like alt; pass that {id}. Always returns metadata plus a short-lived signed downloadUrl: fetch it (curl/wget) into your working directory to read non-image files (xlsx, PDF, CSV, …) with real tooling. Images additionally come back as inline image content, downscaled when large (the JSON’s inline field reports what was sent; downloadUrl always has the original); small text files include their text inline.
  • upload{issueId?, filename?, contentType?, dataBase64?, alt?, commentId?, attachmentId?}: Upload a file to an issue (issueId: UUID or identifier), or without issueId a loose file in your session’s team for sessions_send attachmentIds. With dataBase64: uploads the bytes now (base64 inflates ~33%). Without it: returns attachmentId, a 10-minute signed uploadUrl and a ready curl line for filename/contentType (+ optional commentId); then call again with attachmentId alone to finalize. Images (png/jpeg/webp/gif/avif, 10 MB) and video/audio (50 MB) return “markdown” to embed; other types (50 MB) land in Files, no markdown, never embed. Storage limits apply.
  • delete{id}: Permanently delete an issue attachment by id (the {id} in /api/attachments/{id}) and reclaim its bytes. Descriptions/comments embedding it are rewritten to (deleted image: …) in the same transaction.

Teams you are a member of: list, get, create, update.

  • list{limit?, offset?}: List teams the MCP user is a member of.
  • get{id}: Get a single team by id.
  • create{name, iconUrl?}: Create a new team owned by the MCP user (a unique slug is derived from the name).
  • update{id, name?, iconUrl?, estimationType?, yoloMode?, timeZone?}: Update a team’s name, icon, estimate scale, yolo mode or time zone (IANA; null = each device’s; apps read dates in it) by its UUID. Team owner only.

A team’s members and invites: list/add members, invites_list/invites_create/invites_revoke.

  • list{teamId, limit?, offset?}: List the members of a team. id is the USER id (use it for assigneeId); memberId is the team_members row id. placeholder: no account yet (added or invited, not joined).
  • add{teamId, email, name?}: Add a person to a team WITHOUT inviting them: a placeholder member (no account) to assign issues to; id is the assigneeId. No mail, no seat; Send invite brings them in later. Owner only. Fails for an address that has an account: use exponential_invites_create then.
  • invites_list{teamId, limit?, offset?}: List the pending (unaccepted) invites for a team. The MCP user must be a member of the team.
  • invites_create{teamId, role?, email?, name?}: Create an invite link for a team, returning the token to share. Owner only. Pass email to have the server mail the link (emailDelivered reports the attempt); an email invite also adds the person to the team at once as a placeholder member (memberUserId, assignable now; their content carries over when they join). name labels that member.
  • invites_revoke{id}: Revoke a pending invite by its UUID. Owner only.

A team’s GitHub repositories: list, create (connect one the team’s GitHub App can see).

  • list{teamId, limit?, offset?}: List the repositories registered in a team, each with the boards it backs. The MCP user must be a member of the team.
  • create{teamId, fullName, defaultBranch?, private?, installationId?}: Register a GitHub repository (“owner/name”) in a team so boards can be backed by it. Any member; the repo must be one YOUR GitHub connection grants (team settings → Repositories) — connecting shares it with the team.

Devices (machines running sessions): list, account_login (sign a device’s agent login in).

  • list{teamId?, limit?, offset?}: List the machines teamId (default: your session’s team) may use: your own personal ones or those shared with it, plus servers teammates shared with it; a machine shared only with other teams is never listed. Pick an online device whose agents includes the agent you want. agentUsage.<agent> = its last used login: windows[] (percent + resetsAt), fetchedAt = when those numbers were read, stale: true = the last refresh failed and they are as old as fetchedAt; agentUsageAt = when the device last reported. A session running on another account moves that account’s own row under agentAccounts.<agent>.profiles[].usage instead. A live session refreshes only the account it runs on, per turn; once it ends — or a window’s resetsAt passes — that login returns to the polled cadence. doctor = its readiness report: items[] {key, state ok|action|missing|off|error, detail, action} (null = not reported yet).
  • account_login{deviceId, agent, profileId?, longLived?, code?, commandId?}: Sign an agent account in on one of your own machines (exponential_devices_list: online). The machine runs the agent CLI’s own browser login; only the sign-in URL and the code you type travel, the credential never leaves the machine. Start: {deviceId, agent} adds an account, or profileId re-signs an existing login (agentAccounts.<agent>.profiles[].id, e.g. one that needs a re-login); the login lands on the profile with its email. longLived + profileId (claude) = Persist login: a 1-year token attached to that signed-in login so its runs survive the browser login expiring. Returns status url + url (+ code for codex: enter it at url). Claude: the browser then shows a code, call again with {deviceId, agent, code}. status pending = the machine has not answered yet, call again with commandId. The login appears in exponential_devices_list after the machine’s next heartbeat.

A team’s agents (message one with exponential_sessions_send {agentId}): list, get.

  • list{teamId, limit?, offset?}: List a team’s agents: id, name, triggers (schedules and the event names they listen on), device and owner. Message one with exponential_sessions_send {agentId, text}.
  • get{id}: Read one agent by UUID: name, triggers, device, owner and its instructions.

A team’s saved prompts (run one with exponential_sessions_send {promptId}): list, get, create, update, delete.

  • list{teamId, limit?, offset?}: List a team’s saved prompts (name, icon). Run one with exponential_sessions_send {promptId, text}; exponential_prompts {op: “get”} reads its body.
  • get{id}: Read one saved prompt by UUID, its markdown body included.
  • create{teamId, name, body, icon?}: Create a saved prompt in a team (owners only): a name, a markdown body, an optional icon.
  • update{id, name?, body?, icon?}: Update a saved prompt’s name, body or icon (by its UUID; owners only). Returns the updated prompt.
  • delete{id}: Delete a saved prompt (by its UUID; owners only).

A team’s skills (SKILL.md instructions a session can run with): list, get (by id or name), create, update, delete (owners write).

  • list{teamId?, search?, limit?, offset?}: List skills (name, description, enabledByDefault, builtin; no body) of a team, or of every team you reach. search matches name or description. exponential_skills {op: “get”} reads a body.
  • get{id?, name?, teamId?, file?}: Read one skill, its SKILL.md body included: by UUID, or by name (+ teamId when several of your teams have one). A built-in (builtin: true, read-only) lists its files; file = one of them returns that file instead.
  • create{teamId, name, description, body, enabledByDefault?}: Create a skill in a team (owners only): name ([a-z0-9-], unique), description (what makes an agent load it), markdown body (≤ 64 KB), enabledByDefault.
  • update{id, name?, description?, body?, enabledByDefault?}: Update a skill’s name, description, body or enabledByDefault (by its UUID; owners only). Returns the updated skill.
  • delete{id}: Delete a skill (by its UUID; owners only). Sessions that picked it run without it from their next start.

A team’s MCP servers for coding sessions (never .mcp.json): list, create, delete.

  • list{teamId, limit?, offset?}: List a team’s MCP servers (Linear, Sentry, …) that coding sessions can connect to, with YOUR connection status (connected | not_connected | expired | error | not_needed), who connected and who shared it with the team’s action sessions, and connectUrl: the settings page where you (a person) connect it. Team members only.
  • create{teamId, url, name?}: Add a remote (https) MCP server to a team’s registry, never a repo .mcp.json. Owner only. Probes the URL to detect OAuth; name defaults from the host. Returns the row and connectUrl, where each member connects once.
  • delete{id}: Remove an MCP server from its team’s registry (by UUID); every member’s stored connection to it goes too. Owner only.

Sessions (threads, agents, children): get a tree (no id = yours), list, link/unlink issues, configure (model, account, effort; no message), escalate a waiting card to the managing agent’s conversation, end one.

  • get{id?, messages?}: Read a session tree: the root (thread or agent) with its facets (title, issues, PRs), its child sessions and the last messages (count = messages, default 20). id = any session in the tree; omitted = your own session’s root.
  • list{teamId?, agentId?, live?, limit?, offset?}: List thread roots you can see, newest first (agentId = your conversations with that agent instead). teamId narrows to one team; live: true = not ended.
  • link{issueIds, sessionId?}: Link issues (UUIDs or identifiers) to a session (sessionId, default your own): it holds them, a session with a repository takes them (In progress). Same team only.
  • unlink{issueId, sessionId?}: Unlink an issue (UUID or identifier) from a session (sessionId, default your own; else from whichever session of its thread holds it). Its last worker dropping it sends it back to backlog.
  • configure{sessionId?, model?, account?, effort?, computerUse?, ultracode?}: Change a session’s model, account (its login), effort, computer use or ultracode, no message (sessionId omitted = your own). Applies on a blocked session too: a new account, or a new model on a model wall, unblocks it and runs its queued messages; a pick that cannot lift the wall errors.
  • escalate{messageId, sealedCard?}: Show a waiting question/plan card (messageId; its ask mirror works too) in the team agent’s conversation that manages its thread, so a person answers it there. The same card: an answer either side answers both. From inside that thread or from the agent’s conversation. Idempotent. sealedCard: the card encrypted to the target tree (sessions.escalateTarget).
  • end{id?, summary?, sealedReport?, copies?}: End a session for good (by UUID; omitted = your own). summary (one paragraph: finished, stopped for a human, or changed nothing) becomes its final message. Agents never end.

Your own session: suggest replies or tasks, compact your context, report_bug when Exponential itself misbehaves (cards, questions and plans: exponential_show).

  • suggest{tasks?, replies?, sealed?}: Propose follow-ups on your own session. tasks = one card in your conversation; a person starts, files or dismisses each (title = a short name, text = the request a started session gets, issueIds = issues it is about). replies = quick replies above the composer: likely next messages; a tap sends one to you as the next message; your newest replies replace older ones.
  • compact{reason, keep?}: Ask the host to compact this session’s context at the next turn boundary. reason is logged on the session; keep names what the summary must preserve (open threads, decisions, file paths). Returns accepted, or refusedBecause: too_early (under half the context used), cooldown (compacted within the last 20 turns), not_own_session, unsupported_agent. Only your own session.
  • report_bug{title, description}: File a bug report about Exponential itself (the issue tracker — any client, these MCP tools, sync, relays) to the Exponential team. Use it the moment Exponential misbehaves or a tool result misleads you mid-task. Not for issues in the user’s own project.

Exponential apps (a team’s declarative Exponential UI packages, rendered in every client): list, get (the head or a version’s package), upsert (= a new DRAFT version; creates the app on a new slug), publish (members and the app’s tools run the published version), delete, catalog (the components, bindings, functions and records a package may use), records_list / records_upsert / records_delete (an app’s own data), records_history / records_restore (a record’s 90-day history), handoff (run an app’s agent hand-off).

  • list{teamId?, limit?, offset?}: List a team’s apps (slug, name, head version). teamId defaults to yours.
  • get{slug, teamId?, version?}: Read one app by slug with its package (the head, or version = an older one’s).
  • upsert{slug, teamId?, name?, icon?, color?, description?, package, note?}: Write a NEW DRAFT VERSION of an app’s package (the editor previews it live; members keep the published version until op publish); a new slug creates the app (name required); on a draft (no version yet) name/icon/color/description rename it and the slug follows (use the returned slug). package = the format exponential_apps {op: “catalog”} describes; note = one line on what changed.
  • publish{slug, teamId?, version?}: Publish a version of an app (default: the head draft; an older version = a rollback). Members, the app’s exposed tools and automations run the published version. Publish only when the person asks.
  • delete{slug, teamId?}: Move an app to the trash (its creator or a team owner).
  • catalog{only?}: The package format, the bindings (exp: sources), the host functions, records and every Exponential UI component an app may use. Read it once before writing a package. only = just these components’ props (for an exponential_show surface).
  • records_list{slug, teamId?, kind?, query?, limit?, offset?}: List an app’s records (its own data) newest first: id, kind, data, createdBy, authorId, createdAt. kind narrows. query = the app’s JSON records query (its from or kind) (where, sort, group, aggregate, running, before, sum, limit; catalog): answers its value {rows, count, groups?, totals?…} as the app’s bindings see it.
  • records_upsert{slug, teamId?, kind, data, id?, author?}: Create one record of an app (data validated against the kind’s schema in its package), or with id patch that record’s data (only its author or a team owner). author = a member’s user id to write for them (owners only; with id: reassigns). The app’s bindings repaint live.
  • records_delete{slug, teamId?, id}: Delete one record of an app by id (only its author or a team owner).
  • records_history{slug, teamId?, id}: A record’s history newest first (90 days): id (the eventId), op (create/update/delete/restore), actor, via, patch, before, createdAt.
  • records_restore{slug, teamId?, id, eventId}: Restore a record to the state one of its history events left (eventId from records_history), as a new write (needs update on the record).
  • handoff{slug, teamId?, tool, input?, record?}: Run an app’s agent hand-off (one of its published exposes whose run is agent) as you: its team agent gets the app’s prompt + input as data in your conversation with it. record = the app record it is about.

The team’s event stream: emit one (every agent with a trigger on that name wakes), list recent ones.

  • emit{teamId?, name, payload?}: Emit a team event: name (free text, e.g. ui-changed; builtins are issue.created, issue.status_changed, issue.assigned, pr.opened, pr.merged, session.ended) plus a JSON payload. Every agent with an event trigger on that name is woken with it; never your own agent. teamId defaults to your session’s.
  • list{teamId?, name?, limit?, offset?}: List a team’s recent events, newest first: id, name, payload, emitter (session, user or server), createdAt. name filters to one event name. teamId defaults to your session’s.

Your notifications: list, mark_read (one or all).

  • list{unreadOnly?, limit?, offset?}: List the MCP user’s own notifications, newest first. Set unreadOnly to show only those not yet read.
  • mark_read{id?, all?}: Mark one notification read by id, or all unread ones with all=true. Only the MCP user’s own notifications are affected.