# Executor docs > Connect your accounts once, then use them from the dashboard or any MCP client. # What Executor is Source: https://executor.sh/docs Executor holds your **accounts** — the credentials for the services you already use — and connects them to **apps**. An app is a small program that turns those accounts into **tools**. People use those tools in the dashboard. Agents use them through one MCP endpoint. You connect an account once. Any app that needs the same **provider** can use it, without a second copy of the credential. The agent never sees the credential; Executor attaches it inside the app. ## The short version 1. **Add an app** Add one from the catalog, copy a public listing, or deploy your own source. 2. **Connect its accounts** On the app's Accounts tab, connect a credential such as an API key or an OAuth sign-in, or link one you saved for another app, for each requirement. 3. **Use it** Connect your agent to the MCP endpoint, or use the app in the dashboard. ## Where to run it - **Hosted**: sign in at [app.executor.sh](https://app.executor.sh). There is nothing to install. - **Local**: run the server on your own machine with the [command line](/run/cli). Your accounts stay on that machine. - **Self-host**: run the server in [Docker](/run/self-host) on your own infrastructure, with your own database. All three run the same apps and expose the same `/mcp` endpoint. Hosted and self-host add organizations and browser sign-in. ## Next **[Connect an account](/connect-an-account)** Save a credential and link it to an app. **[Add an MCP client](/mcp-clients)** Point Claude Code, Cursor or Codex at your endpoint. **[Providers and accounts](/concepts/providers-and-accounts)** What Executor stores, and what an app sees. **[Author an app](/author-an-app)** Write queries, mutations and the approvals that guard them. --- # API keys Source: https://executor.sh/docs/api-keys ## Personal access tokens Executor API keys are personal access tokens (PATs). A token authenticates as you and uses your current permissions in the organization targeted by each request. Tokens have no permission or tool-selection settings of their own. To limit what an agent reaches, connect it through a [scoped connection](/mcp#scoped-connections) instead. Tokens belong to your account, not to an organization. They live under **Account settings → Tokens** (`/account/tokens`), reached from your avatar menu, and the same list shows every token you own across all your organizations. 1. Open **Account settings → Tokens** and select **Create token**. 2. Give the token a name. 3. Choose a scope: one organization, or **Full account**. 4. Optionally set an expiry. 5. Copy the token into your script's secret manager. It is shown only once. A token limited to one organization only works there. Requests that target another organization return `403`, even if you belong to it. A full-account token works in every organization you belong to, including ones you join later. Choose the narrowest option the script needs. The organization cannot be changed after creation; create a new token instead. Use a separate token for each script so you can revoke access independently. Keep tokens out of browser code and source control. ## Use a token Each organization's **Connections** page has a **Personal access token** tab with the MCP and HTTP addresses for that organization. For example: ```sh curl '/api/organizations//inventory' \ --header "Authorization: Bearer $EXECUTOR_API_KEY" ``` A full-account token can access your other organizations. Each request checks your current membership and role. Changing the organization URL does not grant access to an organization you do not belong to. Tool approval rules still apply. For `GET /api/context`, pass `?organization=` instead. A token limited to one organization can omit it. Invalid, expired, or revoked tokens return `401`. Insufficient access returns `403`. ## Expiry and revocation Signing out does not revoke tokens. Removing an organization membership stops access to that organization, and role changes apply to subsequent requests. To replace a token, create another, update your script, then revoke the old one. Revocation prevents new requests. Requests already running may finish. Revoking one token does not affect other tokens. The saved Executor app credential uses the same key system and appears as **Executor app**. Revoking that key also stops the saved connection. Revoked tokens are removed from the list. ## Connect an MCP client Use your PAT as the bearer token. The organization URL names where calls run: ```json { "mcpServers": { "executor": { "type": "http", "url": "https://mcp.executor.sh/org//mcp", "headers": { "Authorization": "Bearer " } } } } ``` The **Connections** page fills in your server URL and the organization you are viewing, and links to your tokens with that organization preselected. For self-host, use your own origin. Keep the token in your client's private config or secret store. A token limited to one organization also works on the bare `/mcp` URL with no extra configuration. A full-account token on `/mcp` must say which organization to use with an `X-Executor-Organization` header. A token limited to one organization is rejected on any other organization's URL. The same token works with model, native, and browser approval delivery. Add `?elicitation_mode=native` or `?elicitation_mode=browser` to the MCP URL when needed. Tool approval rules still apply. Browser approvals require the token owner to sign in. Revocation or expiry stops new MCP calls and continuations, including on already-connected clients. PATs do not require an OAuth sign-in flow. The existing [browser OAuth flow](/mcp-clients) remains available. PATs do not create browser sessions; manage them from your signed-in browser. Both methods use the same authorization checks and current organization roles. --- # Author an app Source: https://executor.sh/docs/author-an-app An app is TypeScript source with a default `defineApp` export from the `apps` package. A new app is two files: `index.ts` and a `package.json` that pins the exact `apps` version the host runs: ```json { "name": "hello", "private": true, "type": "module", "dependencies": { "apps": "" } } ``` Read that version from the host. Agents call the Executor app's `framework.release`; the CLI prints it with `executor apps framework`. Create the app with both files; a deploy without the version fails and names it. To type-check outside Executor, install exactly that version with `npm install --save-exact apps@`. Do not install `apps@latest`: that tag is separate from the Executor beta series. Executor builds the app with the pinned package's server and browser framework. It retains the compiled code with the version, so host updates do not replace it. The exact version also keeps rebuilds on that version; a version range or tag can select a newer version on the next build. Change it only to upgrade the app. An unavailable or unsupported package fails the build and leaves the active app unchanged. The host protocol is small and shared; early beta releases do not promise indefinite support for every past version. `defineApp` declares behavior and does not take a name. Set the package name in `package.json`, such as `"name": "@team.executor.community/calendar"` when publishing under the organization `team`. The name you choose when creating or renaming an installed app is its editable display label. Write ordinary async TypeScript. You do not import Effect and you do not import the Executor SDK. The framework validates input and supplies the accounts each call runs with. ## A first app ```ts import { defineApp, object, query, router, string } from "apps"; const Greet = object({ name: string().default("world") }); export default defineApp( {}, { tools: router({ greet: query( { description: "Greet someone by name", input: Greet }, async (_ctx, { name }) => ({ message: `Hello, ${name}!` }), ), }), }, ); ``` `defineApp(requirements, definition)` takes what the app needs and what it offers. `query(options, handler)` and `mutation(options, handler)` declare the operations. Options are `description`, `input`, and optionally `output` and `approval`. The `tools` router names them: this app has one tool, `greet`. Nest routers to group tools; `router({ issues: router({ list, close }) })` gives `issues.list` and `issues.close`. The schema helpers are `object`, `string`, `number`, `boolean`, `array`, `record`, `json` and `literal`. Use `.optional()` for a field that may be absent and `.default(value)` for a default. `object` strips properties you did not declare. `Infer` gives you the parsed input type. ## Using an account Declare a provider, require it, and read it off the context. A requirement under `user` is filled by each caller's own linked account. ```ts import { array, decodeJson, defineApp, defineProvider, object, query, type QueryContext, router, secrets, string, } from "apps"; const vercel = defineProvider({ name: "Vercel", auth: { apiKey: secrets({ label: "API token", fields: object({ token: string({ minLength: 1 }) }), }), }, }); const requirements = { user: { accounts: { vercel } } }; type Context = QueryContext; const Projects = object({ projects: array(object({ id: string(), name: string() })) }); const listProjects = query( { description: "List projects for the caller's Vercel account", input: object({}) }, async (ctx: Context) => { const response = await ctx.fetch("https://api.vercel.com/v9/projects?limit=10", { headers: { Authorization: `Bearer ${ctx.user.accounts.vercel.fields.token}` }, }); return decodeJson(response, Projects); }, ); export default defineApp(requirements, { tools: router({ listProjects }) }); ``` `ctx.user.accounts.vercel.fields` holds exactly the fields the method declared. The token arrives per call, from the account the caller linked to the app. Nothing in the source names an account, so the same code serves each member's own account. `decodeJson(response, schema)` checks the HTTP status and parses the body. Use `oauth2({ discover: "..." })` instead of `secrets` for a browser sign-in; the access token then arrives as `ctx.user.accounts..fields.access_token`. For a requirement that takes several accounts, use `provider.many()`. The context gives you a list. ### App accounts Some accounts belong to the app rather than to whoever calls it: a team's Datadog key, or one service account for an integration. Declare them under `app`: ```ts const requirements = { app: { accounts: { datadog } }, user: { accounts: { github } }, }; async (ctx: MutationContext) => { ctx.app.accounts.datadog; // linked for everyone by people who can edit the app ctx.user.accounts.github; // the caller's own linked account }; ``` People who can edit an app link its app accounts, for everyone, on the app's **Accounts** tab. Every caller, and the app's background work, uses the linked account through the app, without access to the account itself. Callers never choose or replace it. A requirement name is used once across `app` and `user`, and `.many()` works in both. A factory, `defineApp(requirements, async (ctx) => ...)`, is also evaluated without a caller for the app's background work, so `ctx.user` may be undefined there. Apps written before `apps@0.0.1-beta.90` declared `{ accounts }` and read `ctx.accounts`, which now fail. Every such requirement was a caller's account, so it moves to `user`: `{ user: { accounts } }` and `ctx.user.accounts`. The Executor app's `app-authoring` skill has the steps in `upgrades/0.0.1-beta.90.md`. Nothing stored is converted: versions already running keep their members' schedules. Deploying a schedule as `appCron` or `appInterval` deletes the members' copies of it, with their settings, run history and pending approvals, and the app schedule starts paused until an app manager enables it. ## Queries and mutations Both become tools. The difference is what they may do to the app's own stored data. - A query gets a read-only view. - A mutation writes in a transaction that commits only after its output validates. External calls are allowed from both, and external effects are never rolled back. ## Approvals Approval is declared on the operation. ```ts import { mutation, object, string } from "apps"; import { always } from "apps/operations/approval"; const closeIssue = mutation( { description: "Close an issue", input: object({ id: string() }), approval: always(), // ask a person first; never() runs without asking }, async (_ctx, { id }) => ({ closed: id }), ); ``` A function gets the real decision. It receives the tool name, the decoded input and an abort signal, and returns `approved`, `denied` or `user-approval`. Annotate a shared one with `Approval` to reuse it. For an operation declared elsewhere, wrap it: `withApproval(operation, policy)`. For an imported router, `withApprovals(router, (tool, name) => policy)` picks each tool's policy. There is no app-level approval setting, and nothing is inferred from an operation looking read-only. See [Tools and approvals](/concepts/tools-and-approvals). ## Schedules and webhooks A schedule runs a mutation on a timer, in one of two scopes: ```ts import { appInterval, defineApp, mutation, type MutationContext, object, router, userCron, } from "apps"; const requirements = { app: { accounts: { datadog } }, user: { accounts: { github } }, }; // Runs as the app, without a caller, with the app's accounts only. const sync = mutation( { input: object({}) }, async (ctx: MutationContext) => { await ctx.fetch("https://api.datadoghq.com/api/v1/validate", { headers: { "DD-API-KEY": ctx.app.accounts.datadog.fields.key }, }); return null; }, ); // Runs as each member who enables it, with their linked accounts. const digest = mutation( { input: object({}) }, async (ctx: MutationContext) => { await ctx.fetch("https://api.github.com/notifications", { headers: { Authorization: `Bearer ${ctx.user.accounts.github.fields.token}` }, }); return null; }, ); export default defineApp(requirements, { tools: router({ sync, digest }), schedules: { sync: appInterval({ minutes: 15 }, sync, {}), digest: userCron({ expression: "0 9 * * MON-FRI", timezone: "America/Los_Angeles" }, digest, {}), }, }); ``` `appInterval` and `appCron` declare one schedule, which an app manager enables and which runs with the app's accounts. `userInterval` and `userCron` give each member their own copy, which they enable and which runs as them. Schedules start paused. A context of the `"app"` scope, `MutationContext`, has no `user`; app schedules take only such mutations, or ones whose handler declares no parameters, and only ones without an `approval`, because no person is there to ask. A handler that declares its context without a type gets the caller's, with `ctx.user`, so a tool can read the caller's accounts; give it the `"app"` scope's type to schedule it as the app. With explicit type arguments, `mutation(…)` gets the caller's context even when its handler declares no parameters; pass the `"app"` scope's context as the third. A webhook names the requirement whose account is its source: `account: { user: "github" }` registers it for each linked GitHub account, and `account: { app: "datadog" }` for the account linked for everyone, without a caller. A webhook written inline in `defineApp` states its context, so its handlers are typed: `satisfies Webhook, Config, State>`. ## Asking for input mid-tool ```ts import { object, query } from "apps"; const nameResult = query( { description: "Ask for a name", input: object({}) }, async (ctx) => { const answer = await ctx.elicit({ mode: "form", message: "Name this result", requestedSchema: { type: "object", properties: { name: { type: "string" } }, required: ["name"], }, }); return answer.action === "accept" ? { ...answer.content } : {}; }, ); ``` The question reaches the person the same way an approval does. The answer's `action` is `accept`, `decline` or `cancel`; only an accepted answer carries `content`, the submitted fields. ## Importing an existing service You do not have to write handlers to wrap an API. ```ts import { defineApp } from "apps"; import { mcpRouter } from "apps/mcp"; export default defineApp({}, async ({ signal }) => ({ tools: await mcpRouter({ url: "https://mcp.deepwiki.com/mcp", signal, }), })); ``` The helpers are `mcpRouter` from `apps/mcp`, `graphqlRouter` from `apps/graphql` and `liveOpenapiRouter` from `apps/openapi`. Each returns a router. Use it as `tools`, or mount several under keys, such as `tools: router({ docs: await mcpRouter(...) })`, which names their tools `docs.`. An operation that cannot be classified becomes a mutation. `mcpRouter` needs `@modelcontextprotocol/sdk` in the app's `package.json` dependencies, and `graphqlRouter` needs `graphql`. The dashboard's **Custom app** form does the same thing from a URL, for MCP, GraphQL and OpenAPI, without writing any source. ## Storing data Add SQL files to `migrations/` and the app gets its own SQLite database. The host applies new ones in number order when you deploy. A failing migration fails the deploy with SQLite's error, and applied migrations cannot be edited: add a new one instead. ```sql -- migrations/0001_messages.sql CREATE TABLE messages ( id TEXT PRIMARY KEY NOT NULL, mailbox TEXT NOT NULL, subject TEXT NOT NULL, created_at INTEGER NOT NULL ); CREATE INDEX messages_mailbox ON messages (mailbox, created_at); ``` Handlers read and write through `ctx.sql`, with `?` for values: ```ts import { defineApp, mutation, type MutationContext, object, query, type QueryContext, router, string, } from "apps"; const requirements = {}; type Message = { id: string; subject: string }; const list = query( { description: "Recent messages", input: object({ mailbox: string() }) }, async ({ sql }: QueryContext, { mailbox }) => sql .exec( "SELECT id, subject FROM messages WHERE mailbox = ? ORDER BY created_at DESC LIMIT 50", mailbox, ) .toArray(), ); const add = mutation( { description: "Store a message", input: object({ mailbox: string(), subject: string() }) }, async ({ sql }: MutationContext, message) => sql .exec( "INSERT INTO messages (id, mailbox, subject, created_at) VALUES (?, ?, ?, ?) RETURNING id, subject", crypto.randomUUID(), message.mailbox, message.subject, Date.now(), ) .one(), ); export default defineApp(requirements, { tools: router({ list, add }) }); ``` `exec` types the rows it returns. Queries get read-only SQL; mutations can write. Each statement commits on its own, and `ctx.sql.transaction(...)` groups statements. A transaction is synchronous, so calls to other services always happen between transactions, never inside one. ## Shipping instructions An app can carry its own guidance for agents. Put it beside the source: - index.ts - skills/ - triage/ - SKILL.md - references/ - examples.md ```md --- name: triage description: Search cached messages before fetching more history. --- Read [examples](references/examples.md), then discover this app's tools. ``` An agent reads it with the `skills` tool. Skill text never grants a permission; the approval still decides. ## Deploying Deploying is an operation, not a command. Have your agent call it through the MCP endpoint. Read the version with `framework.release`, create the app from `index.ts` and the `package.json` that pins it, then deploy the same files: ```js Hosted const executor = tools.executor; const path = { organization: "" }; const { version } = await executor.framework.release({ path }); const files = [ { path: "index.ts", content: "" }, { path: "package.json", content: JSON.stringify({ name: "hello", private: true, type: "module", dependencies: { apps: version }, }), }, ]; const app = await executor.appManagement.create({ path, body: { name: "Hello", files } }); return await executor.appManagement.deploy({ path: { ...path, app: app.id }, body: { files } }); ``` ```js Local const executor = tools.executor; const { version } = await executor.framework.release({}); const files = [ { path: "index.ts", content: "" }, { path: "package.json", content: JSON.stringify({ name: "hello", private: true, type: "module", dependencies: { apps: version }, }), }, ]; const app = await executor.appManagement.create({ body: { name: "Hello", files } }); return await executor.appManagement.deploy({ path: { app: app.id }, body: { files } }); ``` From a shell, write the same two files into a directory, using the version `executor apps framework` prints: ```sh executor apps create --name "Hello" --files ./hello # prints the app, including its id executor apps source --app | jq -r .revision.commit executor apps deploy --app --commit ``` The app activates only after the build succeeds, and earlier versions are retained; see [Apps and versions](/concepts/apps-and-versions). Then connect an account for each requirement and, in a new `execute`, call the path `tools.search` returns. An app without accounts is called by its slug: ```js return await tools.hello.greet({ name: "Ada" }); ``` An app with accounts is called the same way. Each call uses the accounts you linked to the app; there is no account input. To use another account, link it on the app's **Accounts** tab, which replaces the one before for a single-account requirement. ### Updating an existing app Create-by-name calls reject an existing name. Edit an existing app through the same source, commit and deploy operations on Local and Hosted. Read their current signatures first. Hosted paths include `organization`; Local paths contain only `app`. ```js const executor = tools.executor; const path = { organization: "", app: "" }; const source = await executor.appManagement.source({ path }); const files = source.files.map((file) => file.path === "index.ts" ? { ...file, content: "" } : file, ); const saved = await executor.appManagement.commit({ path, body: { expected: source.revision.commit, message: "Update greeting", files }, }); if (saved.removed.count > 0) { throw new Error(`The save removed ${saved.removed.count} files: ${saved.removed.paths.join(", ")}`); } return await executor.appManagement.deploy({ path, body: { commit: saved.revision.commit }, }); ``` The commit replaces the complete file list, so keep `package.json` in it. Its result's `removed` gives the `count` of files it deleted and at most 100 of their `paths`; check it before deploying. A deleted file is still in `source.files` and in the `expected` commit, so add it back and commit again to keep it. A path cannot be both a file and a folder; such a save fails with `SourcePathConflict` and saves nothing. Saving source does not deploy it. Web pages show source; agents or ordinary Git clients edit it. Discovery is not cached across a change, so run `tools.search` again after you deploy. The CLI also provides `executor apps source`, `executor apps commit`, `executor apps deploy --commit ` and `executor apps deploy --files `. Deploying reads the chosen commit and leaves the Git branch unchanged. Ordinary Git pushes save source without deploying it. Code, every file outside `ui/public/`, is UTF-8 text: at most 4,096 files and 16 MiB of file contents. Files under `ui/public/` are served as they are and may be binary, such as images, fonts, audio and WebAssembly: at most 10,000 files, each at most 32 MB and 1 GB in total. Builds never load them; each deploy copies them to the host's storage and serves them from there. The CLI checks a directory or JSON file against these limits before uploading it and names the files that are too large, or binary outside `ui/public/`. A directory over 16 MiB in total is pushed with Git, in several pushes when it is large. Source reads list binary public files as `{ "path", "object", "size" }`; send them back unchanged to keep them, or clone the app to get their bytes. A failed CLI command prints the host's error code, HTTP status, message and recovery; when its output is not a terminal it also prints them as `{ "error": ... }` JSON on standard output. When a newer CLI release exists, the failure also says so and how to update. ## Update a hosted app Use `appManagement.deploy` with `{ files: [...] }` to deploy a complete file set, or `{ commit: "" }` to deploy an existing Git commit. Deploying files does not save changes to Git. The app keeps its ID and stored data. The newest successful version becomes active. Use `apps.activate` to select a retained version; activation does not roll back stored data. ## Open an app UI React is the only supported app UI framework for now. Include `ui/index.html`, a React entry such as `ui/main.tsx`, and styles in the app's files. Declare `react` and `react-dom` in the app's package dependencies. The host builds and activates the browser assets with the server code. No separate publish step is needed. SSR and React Server Components are not supported yet. You can bring your own React components or browser-compatible npm component libraries. Declare their dependencies and include the required styles and assets. Libraries that need custom build plugins require additional build support. Tailwind CSS v4 compilation is built in. Import a stylesheet from your React entry: ```tsx import "./style.css"; ``` Start `ui/style.css` with: ```css @import "tailwindcss"; ``` Write complete utility classes in your React components. No Tailwind dependency, config file or separate build command is needed. The build scans browser code, including imported components and lazy chunks. Ordinary CSS and component-library styles can share the same app. Use CSS `@theme` for custom tokens. Use `@source inline("...")` to include classes that only arrive at runtime. Filesystem `@source` paths and JavaScript `@config` or `@plugin` files are not supported. For hosted apps, discover `appUi.location` through `tools.search` and call it: ```js return await tools.executor.appUi.location({ path: { organization: "", app: "" }, }); ``` Open the returned `url` in a browser. Executor Cloud uses `https://..executor.website`; self-host uses its configured app domain. Use the returned URL instead of guessing it. A null URL with `status: "unavailable"` gives a `reason`: `"no_ui"` when the app has no UI, or `"no_app_domain"` when the host has no app domain configured. App pages are private. Opening the link starts browser sign-in through Executor. A successful tool call or a `403` from a guessed address does not verify that the UI renders. Check the actual page before reporting it as working. ## What is coming later - Calling one app from another. - Authoring general HTTP endpoints. - A local `executor dev` loop. --- # Apps and versions Source: https://executor.sh/docs/concepts/apps-and-versions ## App An **app** has a name, an owner, Git source and its stored data. It can have source before it is deployed. Once deployed, it also points to the version it currently runs. **Make a copy** creates a new app with its own source and fresh Git history. It does not copy connected accounts or app data. A running app is copied from its deployed source, even when its working files have newer changes. The copy deploys automatically. An unfinished app copies its working files and stays undeployed. Each copy records the original app or public package and source commit. That record does not connect their updates. Changing or deleting the original does not change the copy. Each app has a slug. That slug is the namespace an agent uses: `tools..`. ## Accounts Each person links their own accounts to an app, and every call runs with the accounts that person linked: one for each single-account requirement, and all of them for a `.many()` requirement. Calls never choose accounts. To use your work and personal Vercel accounts side by side, copy the app and link one to each. An app can also require accounts of its own, which people who can edit it link for everyone. See [Providers and accounts](/concepts/providers-and-accounts). Managing an app does not grant use of somebody else's accounts. Every caller runs the app's active version and shares the app's stored data. ## Version A **version** is one immutable snapshot of an app's source and build. A build must succeed before it becomes the running version. Earlier versions remain available for rollback. Rollback changes the running code; it does not rewind app data or external actions. Source edits and Git pushes do not deploy automatically. An agent reads working source, commits changes against that revision, then deploys the saved commit. Deploying accepts complete files or an existing Git commit. It does not edit Git. The newest successful version becomes active. ## Public apps **Publish** shares a chosen source snapshot independently of the running app. `package.json` supplies the public name and description. Publication shares source files, not private Git history, connected accounts, credentials, or app data. A public package has one current published revision. Republishing replaces that listing; there is no public version catalog. Making a copy uses the exact published revision you reviewed and creates a normal, independently owned app. Republishing or unpublishing the original does not update or revoke your copy. ## Getting an app - **From a public listing.** Make your own copy, then link its accounts. - **From the catalog.** Add an MCP server that needs no sign-in or supports OAuth. For any other service, copy the setup prompt and your agent writes the app. - **From a URL.** Add an MCP server by URL, with the same check. - **From source.** Write TypeScript and deploy it. See [Author an app](/author-an-app). After deploying or changing account links, discover tools again in a new `execute` call. ## What is coming later - Automatic updates from another app or public package. --- # Organizations and access Source: https://executor.sh/docs/concepts/organizations-and-access An **organization** is the boundary that owns accounts, apps and versions on hosted and self-host. Everything you connect or deploy belongs to one. Local Executor has no organizations; it is one person on one machine. ## Members and roles Every person in an organization is a member with one role: | Role | Can do | | -------- | ----------------------------------------------------------------------------------------------------------------------- | | `member` | Create apps, connect accounts, use their own apps and what is shared with them, and manage what they created. | | `admin` | A member's rights, plus invite and remove people, manage groups, publish apps, and manage every app and shared account. | | `owner` | An admin's rights, plus delete the organization on Cloud. | Roles decide who manages things, not who uses them. Using an app, including calling its tools through MCP, follows the app's audience: - **Only me**, the default for a new app: only the person who created it. - **Everyone** in the organization. - **Groups**: the members of the groups it is shared with. A member can share only with groups they belong to; admins and owners can pick any group. Accounts are not shared with groups. A personal account is used and managed only by the person who connected it. To give a team one account, such as a service key, link it to the app as one of the app's own accounts, described below. Managing does not grant use. An admin or owner can see, edit, deploy, share and delete another person's private app, but cannot call its tools unless it is shared with them. Each call also uses only the caller's own linked accounts, so nobody runs with another person's accounts. A [scoped connection](/mcp) can narrow an agent's access further. The exception is an app's own accounts. An app can require accounts that belong to the app, such as a team's service key. People who can edit the app link them for everyone, and only accounts they can use themselves. Everyone who can use the app then uses the linked account through the app's code, for their calls and the app's background work, without access to the account itself. Editing the app does not let anyone link somebody else's account. Unlinking stops the app using it at once. See [Providers and accounts](/concepts/providers-and-accounts#app-accounts). People join by invitation. On hosted you can also be admitted by your identity provider when one is configured. ## Slugs An organization has a slug: lowercase letters, numbers and hyphens, up to 80 characters. Dashboard URLs are built from it, as `/org//...`. If the slug you want is taken, Executor adds a short random suffix. Changing a slug does not leave a redirect behind, so old links stop working. ## Access is never implicit Being in an organization is what grants access. There is no ambient "current organization" that decides it: the organization you are looking at in the dashboard is a display preference and nothing more. That matters most for agents. An MCP connection is approved for one organization, and the grant records it. Switching organization in the dashboard afterwards does not retarget the connection, including after the token refreshes. To reach a second organization, connect again and approve it separately. An API call whose URL names a different organization than the grant is refused, even when you belong to both. Every request revalidates your membership. Losing it stops the connection working immediately. ## Signing in - **Hosted** supports Google, GitHub, passkeys and a verified email code. - **Self-host** uses a password. The first person to finish setup becomes the owner, and there is one organization. After that, people join by invitation. You can add an OIDC identity provider and restrict it to your email domains. Self-host does not offer the hosted Google and GitHub buttons. A login is not an account. Signing in with Google does not create a Google account an app can use, and no tool ever receives your login token. See [Providers and accounts](/concepts/providers-and-accounts). ## What is coming later - Workspaces: a grouping inside an organization for installing apps and sharing accounts with a smaller set of people. --- # Providers and accounts Source: https://executor.sh/docs/concepts/providers-and-accounts ## Provider A **provider** is a service and its authentication, declared in app code. It has a name and one or more named authentication methods. ```ts import { defineProvider, object, secrets, string } from "apps"; const vercel = defineProvider({ name: "Vercel", auth: { apiKey: secrets({ label: "API token", fields: object({ token: string({ minLength: 1 }) }), }), }, }); ``` `apiKey` is the method name. `secrets` means fields you paste; `oauth2` means a browser sign-in. Tell people where to get credentials with each field's `description` and, for ordered setup or prerequisites, a `guide` on a `secrets` method. `title` replaces a field's name as its label, and an `oauth2` method's `clientGuide` explains creating an OAuth client at the service: ```ts apiKey: secrets({ label: "API token", guide: "Sign in as the user the app acts as, then create the token.", fields: object({ token: string({ minLength: 1, description: "Create one under [Account settings](https://vercel.com/account/tokens).", }), }), }), ``` The connect form shows this setup help as Markdown: the guide above the fields, in place of a generic pointer to the service's account settings, and each description under its field. Links must be absolute `https:` URLs. A deploy refuses raw HTML, images, a guide over 4,000 characters and a description over 1,000. A provider is not registered anywhere and has no owner and no slug. Two apps that declare the same provider resolve to the same provider reference, so an account saved for one can be used by the other. You do not coordinate an identifier between apps; matching definitions are enough. Setup help is not part of what has to match: editing it keeps the provider and its accounts, and two apps may explain one provider differently. A provider reference is also not permission. Knowing it does not let an app read an account. Access is authorized separately. ## Account An **account** is one saved instance of one provider method: a label plus the field values. It is owned, and it is reusable. Two accounts of the same provider are normal. "Work Vercel" and "Personal Vercel" hold different tokens, and both can be linked to one app. Several apps can use the same account without copying the credential. An account can also have a **description**: free text for agents, such as "reads only; use the sandbox account for writes". Agents read it with the label, for example when deciding which account to link. Set it when you create the account or edit it later; neither changes the credential. Fields are an object that matches the method's schema — `{ token }`, or `{ email, key }` — not one normalized secret string. ## Requirements An app declares a **requirement** for each provider it needs. The requirement is a named slot on the app, and it belongs to one of two scopes: ```ts const requirements = { app: { accounts: { datadog } }, user: { accounts: { vercel } }, }; ``` - A **user account** requirement (`user`) is filled by each caller's own account. Every member links their own, and each call uses the caller's. - An **app account** requirement (`app`) is filled by an account linked for everyone by people who can edit the app. Every caller, and the app's schedules and webhooks, use it through the app. `vercel` and `datadog` are the slot names; a name is used once across both scopes. A plain provider needs exactly one account. `provider.many()` accepts zero or more, and the app receives a list. ### User accounts Using an app means linking your accounts to it. A link records that you use an account with that app; it is not copied into the app and grants nobody else anything. Replace the credentials on the account and every app linked to it follows. Hosted and local link accounts the same way, from the app's **Accounts** tab. Each link names the slot it fills. A plain slot has one linked account per member. Linking another account replaces it in the app; the previous account stays saved. A `.many()` slot uses every linked account; unlink one to stop using it. Calls never choose accounts: tools, schedules and app pages all run with your links. ### App accounts People who can edit an app link its app accounts for everyone, in the **App accounts** section of the app's **Accounts** tab. Linking needs edit access to the app and use of the account. Callers use the account through the app without access to the account itself, and they cannot choose or replace it; everyone else sees the section read-only, without the account's details from the provider. Linking another account to a plain slot replaces the one before; a `.many()` slot uses every linked account. Unlinking stops the app using the account, which stays saved. An account linked to an app for everyone cannot be deleted until it is unlinked. The same account may also be linked for yourself. When an app account needs reconnecting, the people who can edit the app are told, and a call that needs it fails saying that one of them must reconnect it. An app cannot run a tool that needs a slot with no linked account. ## Not a login Signing in to Executor with Google or GitHub is not an account in this sense. A login proves who you are. An account is a credential an app uses. A login never creates an account, and a tool never receives your login token. ## What is coming later - Providers backed by a signed-in browser session. See [Connect an account](/connect-an-account) for the steps. --- # Tools and approvals Source: https://executor.sh/docs/concepts/tools-and-approvals ## Tools A **tool** is one operation an app exposes. Apps author two kinds, and both become tools automatically: - **Queries** read. They get a read-only view of the app's stored data. They may still call an external service. - **Mutations** write. Their stored-data writes run in a transaction and roll back if the operation fails. External effects cannot be rolled back. Queries and mutations live in the app's `tools` router. Writing one there is what publishes the tool. Nested routers group related tools, like folders: `router({ issues: router({ list, close }, { description: "Issue triage" }) })` publishes `issues.list` and `issues.close`. A router's description is shown to agents beside its tools, and its instructions become a skill. Imported MCP servers and OpenAPI documents are routers too. Remote MCP servers and live OpenAPI documents are described by their own metadata. An agent reaches a tool by its path: ```js await tools.vercel.listProjects({}); await tools["support-inbox"].archive({ id: "msg_1" }); await tools.acme.issues.close({ id: "123" }); ``` `tools.search` returns that exact expression along with the input type, and `tools.search.describe` returns the whole signature with the output type, so an agent does not have to guess. Which tools are available can depend on the app's configuration. A tool needing an unfilled requirement is not usable, and an imported app only exposes what the upstream service currently offers. ## Approvals An **approval** is declared on the individual query or mutation. There is no app-wide setting, and nothing is inferred from whether an operation looks read-only. ```ts import { always, never } from "apps/operations/approval"; // Ask the person before this runs. approval: always(), // Run without asking. approval: never(), ``` For a real decision, write a function. It receives the tool name, the decoded input and an abort signal, and returns one of three values: | Value | Effect | | --------------- | ------------------------------------------------ | | `approved` | The tool runs. | | `denied` | The tool is blocked, and the program is told so. | | `user-approval` | The tool waits for a person to decide. | Because the function sees the input, the decision can depend on it. A refund under a threshold can run; a larger one can ask. Annotate a shared function with `Approval` and assign it to several tools to reuse one rule. Attach an approval to one shared operation with `withApproval(operation, policy)`. For an imported MCP, OpenAPI or GraphQL router, `withApprovals(router, policy)` picks a policy for each of its tools. The rule stays in the app's own code: ```ts import { toolAnnotations, withApprovals } from "apps"; import { always } from "apps/operations/approval"; // Ask before every write to an OpenAPI or GraphQL API. withApprovals(api, (tool) => (tool.kind === "mutation" ? always() : undefined)); // Ask before MCP tools the server marks destructive. withApprovals(server, (tool) => toolAnnotations(tool)?.destructiveHint === true ? always() : undefined, ); ``` MCP servers added from the dashboard are generated with the second rule. Edit the app's source to change it. Apps added earlier keep the source they were generated with. ## What happens when a tool asks The call pauses. What you see next depends on the [elicitation mode](/mcp) of the connection: - **Model**: the agent shows you the request and calls `resume` with your answer. - **Native**: your MCP client prompts you, and the same program continues. - **Browser**: you get a link to a signed-in page and decide there. When you run a tool yourself from an app's **Tools** tab, the dashboard shows the saved call (the tool and its arguments) and resumes it only after you approve. Only you can answer a run you started there. Approvals from an MCP client or a schedule are answered where they were requested. A tool can also ask for input rather than permission, using a form. The mechanism is the same. Declining or cancelling fails that tool call. It does not undo tool calls that already ran. An agent must not run the program again to get past a refusal. ## What approval does not cover The app's own setup has already run, with its credentials, before any tool approval is evaluated. Approval governs the selected tool, not your trust in the app's code. Decide that when you deploy or install it. Skill text and its metadata never grant permission either. A skill can describe a tool; only the approval decides whether it runs. ## What is coming later - Remembering a decision, so the same action is not asked twice. - Browser approval for a tool called from an app's own web page. Today such a call fails instead of asking. --- # Connect an account Source: https://executor.sh/docs/connect-an-account An **account** is one saved set of credentials for one **provider**. You connect it once. Every app that needs that provider can use it, and none of them holds a second copy. There are two kinds of credential, and the provider decides which it accepts: - **Secrets**: fields you paste, such as an API token, or an email and a key. - **OAuth**: browser sign-in or a client ID and secret for machine-to-machine access. Executor stores and renews the resulting tokens. ## From the dashboard Every account is connected for an app. Choose **Connect new account** at the end of a provider's list on the app's **Accounts** tab, then choose **Connect** to start browser sign-in. If the provider needs credentials, enter them in the same dialog. The connection dialog contains the sign-in methods supported by the provider. The dialog uses the provider fields already loaded with the app. Opening it does not create a connection or fetch the fields again. A connection is created when you submit; retries in that dialog reuse the same request. If the app explains where to get its credentials, the dialog shows its steps above the fields and each field's description under its input. OAuth setup is checked in the background and reused across forms. If Executor can use a saved client or register one automatically, the dialog can connect without asking for client details. If your own client is required, its fields appear immediately. A failed check offers **Retry** without guessing which setup to show. Client IDs and secrets stay on the server during these checks. The provider's code declares the grant, endpoints, scopes, and authentication method. Open **Advanced** below Connect to review **Required permissions**. The dialog asks only for a client ID and, when required, a client secret. For browser sign-in, copy the fixed redirect URL into the OAuth app's settings. When the app explains how to create that OAuth app at the service, its steps follow the redirect URL. Machine-to-machine connections complete in the dialog and need no browser redirect. If saved OAuth client details are wrong, open **Advanced** below the Connect button, choose **Change** next to OAuth client, and enter the replacement. Executor saves manual client details only after a successful sign-in. A failed replacement keeps the previous client and account. If browser sign-in rejects the client, **Update client details** opens the form again. You name an account after it connects. Executor names each new account `Default`, then `Default 2`, and so on when that name is already in use for the provider. Once the account is saved, or browser sign-in returns, a **Name this account** dialog offers to rename it to something such as “Work” or “Personal.” Closing it keeps the default. Reconnecting keeps the existing name. You can also rename an account later from its menu on the app's **Accounts** tab, or with **Edit details** on the **Accounts** page. Completing a connection saves the account and links it to this app. There is no separate step. Browser sign-in returns to the app's Accounts tab; cancelling or failing sign-in offers a direct way back to the app. The Accounts tab has two sections. **Your accounts** ("Linked for you") holds the accounts you linked for your own calls. **App accounts** ("Linked for everyone") holds the accounts the app uses for every caller and its background work; it appears only when the app requires accounts of its own. Each requirement lists every saved account of its provider you can use. A single-account requirement shows a radio button per account: selecting one links it and replaces the account linked before, which stays saved in **Accounts**. A `.many()` requirement shows checkboxes: checking one links it, unchecking one unlinks it, and the app uses every checked account. **Connect new account** at the end of the list adds an account and links it. Each change is saved immediately. Members who can view an app but not use it cannot change its links. Only people who can edit the app (organization owners and admins, and the app's creator; on local, the owner) change **App accounts**, and only with accounts they can use. Everyone else sees that section greyed out and read-only: which account fills each requirement and whether it works, without the account's email, username or other details from the provider. The same account may be linked in both sections. Unlinking keeps the saved account available to other apps. When no app uses it any more, Executor offers to delete it. The global **Accounts** page lists saved accounts and the apps that use them; it does not connect accounts or change their credentials. Local has the same Accounts tab. See "Which accounts a call uses" below. ## From an agent An agent can start the same flow. It must never ask you for a secret in chat, and must never read a token out of your files. It asks Executor for a connection link and gives you the link; you finish in your browser. ```js Hosted return await tools.executor.appAccounts.userConnect({ path: { organization: "", app: "", requirement: "vercel" }, }); ``` ```js Local return await tools.executor.appAccounts.userConnect({ path: { app: "", requirement: "vercel" }, body: {}, }); ``` Neither request names an account owner: hosted saves the account under your organization and local under the app's owner. Every request names the app requirement it connects for. Use `appAccounts.userConnect` for a user requirement and `appAccounts.appConnect` for one of the app's own requirements; on hosted, only people who can edit the app can start the second. To replace the credentials of an account the app already uses, add its ID as `account` in the body. The account keeps its ID, name and links. The app's requirements carry its setup help, so the agent can explain in chat where to get the credentials before it sends the link. Check whether you finished: ```js Hosted const connection = await tools.executor.accounts.connection({ path: { organization: "", connection: "" }, }); return connection.state; ``` ```js Local const connection = await tools.executor.accountConnections.get({ path: { connection: "" }, }); return connection.state; ``` `Completed` means the account is saved and linked to the requirement, for you or for everyone, in the same transaction. If the app's requirement changed while the request was open, such as moving between user and app accounts, the request fails with `AccountConnectionTargetChanged` rather than saving the account. If you can no longer link the account in that scope, it fails with `AccountConnectionRefused`. A pending request expires after thirty minutes. Issue a new one if it does. An agent manages links with the same five tools in each scope, `appAccounts.user.*` for your own links and `appAccounts.app.*` for the app's. Local takes the same paths without `organization`: - `list({ path: { organization, app } })` lists the links. - `get({ path: { organization, app, requirement } })` lists the requirement's linked accounts, then the other accounts you could link. - `link({ path: { organization, app, requirement, account } })` links an account. For a single-account requirement it replaces the one before. - `unlink({ path: { organization, app, requirement, account } })` unlinks it. - `connect({ path: { organization, app, requirement } })` connects a new account, as above. Linking fails with `RequirementMismatch` when the app has no such requirement in that scope or the account's provider differs. Changing app links needs edit access to the app. ## Which accounts a call uses Calls never choose accounts. Every call, schedule and page runs with the accounts linked to the app: for each of your requirements, the one account you linked to a single-account requirement and every account you linked to a `.many()` requirement; for each of the app's own requirements, the accounts linked for everyone. A single-account requirement with no account fails with `AccountRequired`, which says whether you need to link one or someone who can edit the app does. A tool input that passes `$accounts` fails with `AccountChoiceUnsupported`. To use a different account, link it on the app's **Accounts** tab. To use two accounts of one provider side by side, copy the app and link one to each copy. ## Changing and removing an account You can rename an account, replace its credentials, and remove it. Replace credentials from an app that uses the account: open its menu on the app's **Accounts** tab and choose **Reconnect** or **Update credentials**. A **Needs sign-in** account also has a **Reconnect** button there. Replacing credentials keeps the same account, so every app linked to it keeps working. Removing an account removes your links to it. An account linked to an app for everyone cannot be removed until someone who can edit the app unlinks it (`AccountLinkedToApp`). An app left with no account for a requirement exposes no tools until you link or connect another. Executor does not substitute a different account. --- # The MCP endpoint Source: https://executor.sh/docs/mcp Executor serves one MCP endpoint at `/mcp`. Every app you have configured is reachable through it. You do not add one MCP server per service, and you do not paste a credential into the agent. - Hosted: `https://mcp.executor.sh/mcp` - Self-host: `/mcp` - Local: `http://127.0.0.1:4312/mcp` The transport is streamable HTTP. See [Add an MCP client](/mcp-clients) for the exact setup commands. ## The tools The endpoint does not expose one MCP tool per app tool. However many apps you add, the tool list stays the same size. When a client connects, the server's MCP instructions introduce Executor to the agent. They are the Executor app's `executor` skill, which points to its `code-mode` and `app-authoring` skills. - **`skills`** reads the instructions an app ships. Call `skills({})` for a list of what is visible, `skills({ app: "support-inbox" })` for one app's skills, and `skills({ app: "support-inbox", name: "triage" })` to read one. It connects no account and runs no code. - **`search`** finds app tools by words in their paths, descriptions and labels, and returns each tool's exact path and input type. - **`describe`** returns the whole description and signature of tools at exact paths from `search`. - **`call`** calls tools directly by path. Pass one `{ path, input }` as `calls`, or an array of independent calls, which run concurrently. - **`execute`** runs a JavaScript program over your apps. - **`resume`** continues a call or program that paused for an approval or for input. It is not present in native mode, where the client answers in place. ## Direct calls For one or a few calls, an agent needs no program: ```json { "calls": [{ "path": "tools.vercel.listProjects", "input": {} }] } ``` `call` returns each call's outcome in order, with `ok` and its `value` or `error`. One failed call does not stop the others. An unknown path comes back with the closest paths. Results come back whole, however large; `execute` can return only the fields you need. Approvals and input pause `call` exactly as they pause `execute`, except in [native mode](#elicitation-modes): there `call` is marked destructive and open-world, so your MCP client confirms each call before it runs, and that confirmation answers the apps' approval policies. Tool input still opens the client's prompt. ## Codemode `execute` is for work where one result feeds another call, or a large result should be reduced before it reaches the agent. The agent writes a short JavaScript program; Executor runs it in a sandboxed interpreter and returns what the program returns. Inside the program, app tools are ordinary async functions: ```js return await tools.search({ query: "vercel projects" }); ``` Search returns a page of tools: each one's exact callable path, the first line of its description and its input type. Each app and its linked accounts are listed once. `tools.search(next)`, with the returned `next` as the whole input, reads the following page, and `tools.search.describe({ paths })` returns a tool's whole signature, including its output type. Every app is called by its slug, `tools..`. A call uses the accounts you linked to the app: one for each single-account requirement, and every linked account for a `.many()` requirement. An app's own accounts, linked for everyone by people who can edit it, fill its app account requirements for every caller; search and `$accounts` describe only the accounts you link. Calls never choose accounts; an input with `$accounts` fails with `AccountChoiceUnsupported`. To use another account, link it on the app's **Accounts** tab. ```js const projects = await tools.vercel.listProjects({}); return projects.projects.map((project) => project.name); ``` The agent can call several tools, join the results and return only the part it needs. That is the point: the whole result set never has to pass through the model's context. A remote MCP tool returns its whole MCP result: `content`, `structuredContent` and `isError`. A server that returns `structuredContent` usually repeats it as JSON text in `content`; Executor drops that copy, so read `structuredContent`. Other content stays as the server sent it. `isError: true` is the tool saying it failed. The call returns that result instead of throwing, so programs that read `isError` keep working, and the result's `toolCalls` lists the call with outcome `failure`. The server may still have made changes. `call` lists it the same way. An outcome of `success` only means no tool error was reported; check the result before treating its action as done. Images and audio reach the agent as media. Wherever the value `execute`, `call` or `resume` returns holds `{ type: "image", mimeType, data }` or `{ type: "audio", mimeType, data }`, with `data` in standard or URL-safe base64, the response carries that block as its own MCP content after the JSON text, once even if the value repeats it. The JSON keeps `{ type, mimeType, contentIndex }` in its place, the block's index in the response's `content`. MCP image and audio results, such as screenshots, already have that shape. A program can show bytes a tool returns another way, such as an email attachment's base64, by returning them in it. Inside a program the block is whole, so its data can be passed on to another tool. The interpreter has no `fetch`, no `process`, no filesystem, no timers and no imports. Its only way out is a tool call, which enters the trusted runtime where your credentials live. Limits are cooperative, not process isolation. Discovery is not cached across changes, so run `tools.search` again in a new `execute` after you add or reconfigure an app. Some limits are fixed by the server and a client cannot raise them: 65,536 characters of program source, 100 tool calls and 5 minutes. Executor does not limit the size of a result. The runtime limits one value crossing between an app and Executor, such as a tool's result, to 32 MiB, and on Cloud a result of a few tens of megabytes can pass the MCP session's 128 MB memory. Failures Executor recognizes at these limits return an error naming the likely limit, never a cut value. A failure that loses the response first, such as a 64 MiB value on Cloud, can end with no answer before the client's timeout. Time spent waiting for your approval or input does not count toward the 5 minutes. Execute returns the result when the program ends, or sooner with a request for approval or input when the program pauses. An MCP client with a shorter tool-call timeout, often 60 seconds, may give up and cancel the program. ## Signing in from the browser Hosted and self-host use OAuth. Your client registers itself, opens your browser at `/mcp/authorize`, and you sign in there. You then choose an organization and approve the connection. The client receives a token; it never receives your password or your accounts. The grant that comes back is bound to one [organization](/concepts/organizations-and-access). Switching organization in the dashboard later does not retarget an existing connection, including after the token refreshes. To use a second organization, connect again and approve it separately. Every request revalidates the token, the approval and your current membership. A revoked grant returns 401. Losing membership returns 403. The consent screen approves every app available to you. To give an agent less, connect it through a scoped connection instead. ## Scoped connections A **scoped connection** is a named MCP URL that reaches only what you turned on. Create one under **Scoped connections** on the dashboard's Connect page. For each app, choose which of the accounts you linked to it the connection runs as, or none for an app without accounts, and which tools it exposes: all tools, read-only tools, or the tools you pick. All and read-only include tools the app adds later. Approval rules still come from each app's code. A connection never lists the accounts linked to an app for everyone: access to the app authorizes them. Each connection has its own URL, `/mcp?connection=`. Add `&elicitation_mode=browser` or `native` to it for another [mode](#elicitation-modes). The client signs in at that URL as above, and its grant follows the connection: edits apply to connected agents at once, and revoking the connection revokes every grant issued through it. Personal access tokens and the local server key are not limited by a connection; they keep full access at the plain URL. On hosted and self-host, **Connected agents** on the same page lists each client you approved, with the apps it can reach, and revokes its grant. ## Elicitation modes When a tool needs an approval, or needs input, Executor has to ask you. Where that question appears depends on the mode, which you pick in the endpoint URL and which is then fixed in the grant. | Mode | URL | Where you answer | | --------- | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `model` | `/mcp` | The agent shows you the request and calls `resume` with your answer. This is the default. | | `native` | `/mcp?elicitation_mode=native` | Your MCP client's own prompt. The `call` or `execute` request stays open and continues. There is no `resume`. `call` approvals are the client's confirmation of the call itself. | | `browser` | `/mcp?elicitation_mode=browser` | A signed-in page in your browser. The result carries an `approvalUrl`; the agent calls `resume({ requestId })` to collect your answer. | Native mode needs a client that implements form elicitation. Browser mode is the one to use when you do not want the agent to be the one holding the question. The mode is part of the grant, not just the URL. Changing the query string on an issued connection does not change the mode. ## What is coming later - Remembering an approval, so the same action is not asked again. - Resuming a program after the server restarts. A lost session cannot be continued, and the program must not be run again automatically, because earlier tool calls may already have taken effect. --- # Add an MCP client Source: https://executor.sh/docs/mcp-clients The dashboard's **Connect an agent** page shows these commands with your own endpoint already filled in. Open it at `/org//connect` on hosted and self-host, or **Connect** in the local dashboard. The endpoint is your origin plus `/mcp`: | Where | Endpoint | | --- | --- | | Hosted | `https://mcp.executor.sh/mcp` | | Self-host | `/mcp` | | Local | `http://127.0.0.1:4312/mcp` | You can also connect with a [personal access token](/api-keys#connect-an-mcp-client) as the Authorization bearer on `/org//mcp`. The **Personal access token** tab on an organization's Connections page shows that address filled in. Most clients load MCP servers only at startup. After you add Executor, restart the client or open a new session before its tools appear. Moving from a local Executor or Executor 1 to a self-hosted server? Remove the old entry first, and see [Move from a local Executor](/run/self-host#move-from-a-local-executor). ## ChatGPT For Executor Cloud, [open the Executor plugin](https://chatgpt.com/plugins/plugin_asdk_app_6acd465c1d148191832781c3c8ab4d99) and click **Install plugin**. Connect the included Executor app when prompted, sign in to Executor, choose your team and approve access. Installation and account authorization are separate steps; you may be asked to connect on first use. Start a new chat and ask: **Show me the apps and tools available in my Executor workspace.** If your workspace prevents installation, ask your admin to allow Executor. For self-host or a scoped MCP URL, use **Plugins → Add → Add custom MCP server** instead. Name it **Executor**, enter your endpoint, choose **OAuth**, then click **Create as a plugin** and **Connect**. Sign in and approve access. ## Codex In the ChatGPT desktop app, open the same [Executor plugin](https://chatgpt.com/plugins/plugin_asdk_app_6acd465c1d148191832781c3c8ab4d99), install it and connect your Executor account when prompted. Start a new Codex session and ask it to show the apps and tools in your Executor workspace. In Codex CLI, run `/plugins`, find Executor in the plugin browser and install it. Start a new session to load its skills and tools, and complete sign-in when prompted. Marketplace availability depends on your Codex environment. The IDE extension does not support plugins. For the IDE extension, self-host, or a scoped connection, add the MCP server directly: ```bash codex mcp add executor --url 'https://mcp.executor.sh/mcp' codex mcp login executor ``` Replace the URL with your own endpoint when needed. Sign in to Executor, choose your team and approve access, then start a new session. See [OpenAI's plugin guide](https://learn.chatgpt.com/docs/plugins) for supported surfaces and installation details. ## Claude Code Claude Code has its own command. `--scope user` adds Executor for every project. 1. **Add the server** ```bash claude mcp add --transport http --scope user executor 'https://mcp.executor.sh/mcp' ``` 2. **Run /mcp inside Claude Code** It opens Executor in your browser. 3. **Sign in and approve** Choose an organization, then approve the connection. ## Cursor and other clients Use the installer. It detects the client and writes its configuration for you. ```bash Installer npx add-mcp 'https://mcp.executor.sh/mcp' --transport http --name executor ``` ```json By hand { "mcpServers": { "executor": { "type": "http", "url": "https://mcp.executor.sh/mcp" } } } ``` If your client does not accept the installer, merge the configuration above into its MCP file by hand. Clients that support this format read a `mcpServers` map, and Executor needs nothing beyond a streamable HTTP URL. Sign-in happens in your browser the first time the client connects. ## Connecting to a local server A local server also signs clients in through your browser, using the dashboard you paired with `executor`. A client that cannot sign in can send the local API key as a bearer token instead. The port is `4312` unless you changed it with `executor service set port`: ```bash Installer npx add-mcp 'http://127.0.0.1:4312/mcp' --transport http --name executor \ --header 'Authorization: Bearer ' ``` ```json By hand { "mcpServers": { "executor": { "type": "http", "url": "http://127.0.0.1:4312/mcp", "headers": { "Authorization": "Bearer " } } } } ``` The background Executor keeps running until you stop it, but it does not start with your computer: after a restart, run `executor` or `executor service start`, or the client's connection is refused. ## Choosing where approvals appear By default the agent relays an approval request to you and answers on your behalf with `resume`. To answer in your browser instead, add the mode to the URL before you connect: ```bash claude mcp add --transport http --scope user executor 'https://mcp.executor.sh/mcp?elicitation_mode=browser' ``` The mode is fixed when the connection is approved. To change it, connect again with the new URL. See [The MCP endpoint](/mcp) for what each mode does. ## Check that it worked Ask the agent to run this: ```js return await tools.search({ query: "Executor" }); ``` It should return callable paths for the Executor app itself. If the tool list is empty, the client has not reconnected since you added the server. --- # Command line Source: https://executor.sh/docs/run/cli The local server runs Executor on one machine for one person. It holds your accounts, runs your apps, and serves an MCP endpoint for your agents. ## Install and start With npm, on Node 14.18.0 or newer. The package is a launcher that downloads Executor's own runtime for your platform on first run, so Node only starts it. Check that executor --version prints 2.0.0-beta.41 before starting. Supports macOS on Apple Silicon and Intel, Linux (glibc) on ARM64 and x64, and Windows on x64. ```bash npm i -g executor@beta executor --version executor ``` Without Node, on macOS or Linux: the script checks the download against the release's SHA256SUMS and links ~/.local/bin/executor. ```bash curl -fsSL https://executor.sh/docs/install.sh | sh executor --version executor ``` Without Node, on Windows, in PowerShell: ```bash irm https://executor.sh/docs/install.ps1 | iex ``` With Homebrew, on macOS or Linux: ```bash brew install usefulsoftwareco/tap/executor-beta ``` `executor` starts Executor in the background, opens the dashboard in your browser and exits. Executor keeps running until you stop it; see [The background Executor](#the-background-executor). Executor is one executable with its runtime, dashboard, Git and workerd beside it; no source checkout, Node or Bun is needed to run it. The npm package is a small launcher. The first time a version starts, it downloads that release's archive for your platform from the public GitHub release, checks it against the SHA-256 the package records, and keeps it in `~/.executor/v2/launcher/`. Set `EXECUTOR_DOWNLOAD_URL` to use a mirror of the releases. The install script keeps each release in `~/.executor/v2/runtime/versions/`, points `~/.executor/v2/runtime/current` at the one in use and links `~/.local/bin/executor` to it. It never edits your shell profile: if `~/.local/bin` is not on your `PATH`, it prints the line to add. On Windows the script adds `%USERPROFILE%\.executor\v2\runtime\current` to your user `PATH`. Pass a version to install that release, for example `curl -fsSL https://executor.sh/docs/install.sh | sh -s -- 2.0.0-beta.16`. If `executor --version` still shows v1, your shell is finding an older install. Run `type -a executor` on macOS or Linux, or `where.exe executor` on Windows, to see which executable takes priority. Remove the old install with the package manager that installed it, then open a new terminal. For example, use `bun remove -g executor` for an old Bun install. This does not delete your data. On first launch, Executor saves its API and encryption keys in macOS Keychain, Windows Credential Manager, or Linux Secret Service. Unlock the credential store and allow Executor access when asked. If there is no credential store on the first launch, for example on a Linux server without GNOME Keyring, Executor saves the keys to `keys.json` in its data directory instead. The file is readable only by your user. Keep that directory private. If a credential store exists but access is denied, for example because you dismissed the prompt or the store is locked, Executor stops without writing `keys.json`. Start it again to be asked again. Where no prompt can appear, such as over SSH, unlock the store first: on Linux run `gnome-keyring-daemon --unlock`, on macOS `security unlock-keychain`. To use `keys.json` on a new data directory even when a credential store works, set `EXECUTOR_KEY_STORAGE=file` for its first start. Set `EXECUTOR_KEY_STORAGE=os` to require the credential store and never fall back to `keys.json`. Executor never switches an existing directory's key storage: a value that does not match the directory stops startup and changes nothing. | Command | Purpose | | --- | --- | | `executor` | Start the background Executor if it is not running, open the dashboard and exit. | | `executor service` | Start, stop and configure the background Executor. | | `executor serve` | Run Executor in this terminal, without opening a browser, until you press Ctrl+C. | | `executor pair` | Print a new connection link for the background Executor. | | `executor apps` | Create, edit, deploy and install apps. See [Author an app](/author-an-app). | | `executor upgrade` | Install the newest release the way Executor was installed; `--rollback` returns to the previous one. | | `executor rotate-key` | Replace the saved API key. Run `executor service restart`, then update clients that used the old key. | `executor pair` asks the background Executor for a link, waiting up to 30 seconds while it starts, stops or does not respond. It only reads: it never starts Executor or creates a data directory or keys. When you set `EXECUTOR_API_KEY`, it sends that key instead, to the background Executor or, when none runs, to the server on `EXECUTOR_PORT`. ### Coming from Executor 1 Executor 2 answers the old commands with the command to run instead. | Executor 1 | Executor 2 | | --- | --- | | `executor web` | `executor` starts the background Executor if needed and opens the dashboard. | | `executor open` | `executor pair` prints a connection link for the background Executor. | | `executor daemon run` | `executor service start` starts the background Executor without opening a browser. It runs until `executor service stop`. | | `executor install`, `executor service install` | Nothing to install. `executor` or `executor service start` starts the background Executor, which runs until you stop it; it does not start with your computer. | | `executor service uninstall` | Nothing to remove. `executor service stop` stops the background Executor. | | `executor mcp` | The background Executor serves MCP over HTTP at `http://127.0.0.1:4312/mcp`; see [Connect an agent](#connect-an-agent). | | `executor call`, `executor tools`, `executor resume` | Agents call tools through the MCP endpoint. | | `executor server` | No server profiles. `executor apps` commands take `--host`, and `executor rotate-key` replaces `executor server rotate-token`. | | `executor login` | `executor apps login --host https://api.executor.sh` signs in to hosted Executor for app commands. | | `executor docs` | Open [executor.sh/docs](https://executor.sh/docs). | ## The background Executor Each data directory has one background Executor that every command shares. The first command that needs it starts it: `executor`, `executor service start` or an `executor apps` command. It keeps running after that command exits, until you run `executor service stop`. Nothing is installed with your operating system, so it does not start again after you restart your computer: MCP clients cannot connect until you run `executor` or `executor service start`. The desktop app uses the same background Executor and data directory. When you stop it, with `executor service stop` or by changing a setting, the desktop app says it was stopped and does not start it again. It reconnects once a command starts it, or when you click **Start** there. Desktop app releases before the background Executor kept their own data. The first launch of a newer desktop app moves that data into the command line's data directory, `~/.executor/v2/cli`, once, so both use the same apps and accounts. Nothing is merged: if the command line already had data there, the desktop app's data replaces it, and the command line's folder, with its apps, accounts and `service.json`, moves to `~/.executor/v2/backups/cli-data-