Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

OAuth connections

OAuth 2.0 is available for REST connections and for MCP connections over http or sse. Stdio MCP servers and external connection drivers do not support it. The runtime acquires and refreshes bearer tokens during requests. Tokens and client secrets stay in encrypted credential storage and never become environment variables.

An authorized account belongs to the organization. Each agent connection has one account binding, shared by its deployments and by cloud-backed local development. Other connections use that account only when an administrator binds them to it.

Declare OAuth in the connection

Set auth in spec.json:

{
  "baseUrl": "https://gitlab.com/api/v4",
  "auth": { "type": "oauth2" },
  "test": { "path": "user" }
}

auth.token is rejected with oauth2. The application configuration comes from the first of:

  1. auth.provider, the id of a managed provider. The CLI cannot use managed providers with --local.
  2. oauth.json next to spec.json, for a custom application.
  3. Discovery from the server URL, for an MCP connection whose server publishes OAuth resource metadata (RFC 9728) and registers public clients (RFC 7591).

A REST OAuth connection with none of these is listed as needing configuration. An existing approved binding takes precedence over the authentication declared in the files, so editing spec.json alone does not remove it.

oauth.json

oauth.json holds the application settings. It never holds the client secret: a clientSecret key, or any unknown key, fails validation.

FieldRequiredDescription
labelnoAccount label, 1 to 100 characters. Defaults to the connection name.
grantTypeyesauthorization_code or client_credentials.
issueryesIssuer URL.
discoveryUrlunless endpoints are setOpenID or OAuth discovery document.
authorizationUrl, tokenUrlwithout discoveryUrlExplicit endpoints. authorizationUrl is needed only for authorization_code.
revocationUrlnoRevocation endpoint used by disconnect.
clientIdsee belowRegistered client ID.
tokenAuthMethodyesclient_secret_post, client_secret_basic, or none. client_credentials cannot use none.
scopesyesArray of scope strings. May be empty.
resourceOriginsnoExact origins (scheme, host, port) the token may be sent to. Defaults to the origin of baseUrl or url. Not URL prefixes.
authorizationParameters, tokenParametersnoExtra parameters. Only access_type, prompt, audience, resource, login_hint, and include_granted_scopes are accepted.
accountUrl, accountIdPathtogetherAccount lookup used to identify the account on reconnect. The accountUrl origin must be in resourceOrigins. accountIdPath is a dotted path.
scopeSeparatorno"," for a provider that separates scopes with commas. Defaults to a space.
bearerTokenTypesnotoken_type values the provider uses for a bearer token, such as Slack's bot.

Omit clientId only for a public authorization-code client: the file then needs discoveryUrl, "grantType": "authorization_code", and "tokenAuthMethod": "none", and the broker registers a client bound to the callback URL.

Hosted OAuth requires public HTTPS destinations. Local development also permits explicit loopback URLs. Resource redirects are rejected, so configure the final API origin. The broker generates protected fields such as state and the redirect URI.

Changing scopes, resource origins, or other compared settings in oauth.json moves a connected connection to reconnect required. Discovery-derived endpoints and an omitted client ID are not compared. Reconnecting approves the edited file and requires authorizing an account again.

Add a connection in Amodal

Add connection on the Connections page writes the connection files through the connected source: a commit on the selected branch for a GitHub source, a source snapshot for an Amodal source. See Where changes are saved.

The dialog stages are Choose service, Set up, Save, and Connect. Services with nothing to fill in skip Set up.

  1. Choose service lists the managed providers the platform enables and the remote MCP presets, such as Linear, Jira and Confluence, Notion, GitLab, Sentry, and Stripe. A preset writes an ordinary OAuth MCP spec.json and needs no application registration. Custom connection offers an API key, your own OAuth app, or an MCP server.
  2. Set up takes the name and the service's values. The name must match [A-Za-z0-9_-]{1,100} and be unique. A managed provider writes auth: {"type": "oauth2", "provider": "<id>"} with no oauth.json. A custom OAuth app shows the redirect URL to register, writes oauth.json, and stores the client secret encrypted outside the repository. An API key is stored as an agent secret after the files are saved.
  3. Save shows the summary and the generated files under Configuration files, then commits with Save connection.
  4. Connect lists the remaining steps: add the credential for each env: reference with Save secret, then Deploy and connect and Continue to sign in. Custom applications wait for the deployment to become active, because their settings come from its bundle. With manual production promotion, the dialog offers Make production. Connect later leaves Finish setup on the row.

An MCP server without published resource metadata, or one that requires a pre-registered client ID, needs oauth.json. Register the exact callback URL shown in Amodal.

Manage connections in Amodal

  • Each row on the Connections page shows one state and the action that completes setup, such as Finish setup, Configure OAuth, Add credential, Connect, Reconnect, or Restart agent. A connection that declares no authentication reads No credentials.
  • Test connection runs the test operation declared in spec.json, or lists an MCP server's tools. A REST connection without a test operation offers Open instead.
  • The connection page's Account and access card shows the authorized account, its scopes, and Disconnect account. Disconnecting affects every connection bound to the same account.
  • The Settings card of a custom OAuth connection edits oauth.json through the connected source. Deploy the saved source, then connect.
  • When the saved account cannot be verified on reconnect, Authorize a new account and Replace account and authorize create a fresh binding. Other connections keep the replaced account.
  • Remove deletes the directory and its OAuth binding. It stops when code or configuration still references the connection. It does not deploy, so a deployed connection stays as Pending removal until the next deploy. Removing a binding does not disable the account.

Credential changes do not restart a hosted runtime. New sessions resolve them immediately. Restart agent restarts the deployment so open sessions, channels, and MCP sessions pick them up.

Use the CLI

Commands address the connection name, such as analytics, not its npm package name.

add writes the connection files into the working tree without committing, under amodal/connections/<name>/ when that directory holds connections, else connections/<name>/. It refuses an existing name. It takes one of --rest <baseUrl>, --mcp <url>, or --provider <id> (with an optional --rest <baseUrl>):

amodal connections add crm --rest https://api.example.com
amodal connections add shop --rest https://shop.example.com --auth header --header X-Shop-Token --secret-name SHOP_TOKEN
amodal connections add analytics --rest https://analyticsdata.googleapis.com --auth oauth2 --oauth ./oauth.json
amodal connections add drive --provider google-drive --rest https://www.googleapis.com/drive/v3
amodal connections add linear --mcp https://mcp.linear.app/mcp
amodal connections add notion --mcp https://mcp.notion.example --auth bearer

--auth selects bearer (default for --rest), api_key, basic, header (with --header <name>), or oauth2 (default for --mcp, only method for --provider). A token method writes auth.token: "env:<NAME>_API_KEY", <NAME>_BASIC_AUTH for basic, or <NAME>_TOKEN for an MCP server. --secret-name picks another variable. --oauth <file> copies and validates a custom application's oauth.json.

The other actions are configure, connect, status, test, list, disconnect, and remove:

amodal connections configure analytics --agent AGENT_ID --config oauth.json --secret-stdin
amodal connections connect analytics --agent AGENT_ID
amodal connections status analytics --agent AGENT_ID --json
amodal connections test analytics --agent AGENT_ID --path ./agent
amodal connections list --agent AGENT_ID --json
  • configure does not read the connection's oauth.json. Pass --config <file>, --grant GRANT_ID to bind an existing account, or --managed PROVIDER_ID.
  • --secret-stdin reads a confidential client's secret from standard input until it ends. Do not pass secrets as arguments.
  • connect prints and opens the authorization link (--no-open skips the browser) and polls until completion or denial. The authorization expires after ten minutes. For cloud authorization, sign into the same Amodal account in the browser.
  • disconnect disables the account, attempts revocation when the provider has a revocation endpoint, and blocks every binding of that account. It does not delete files.
  • remove deletes the connection directory only when every file in it is a source file. With --local it also deletes the local binding. Without --local, cloud bindings and accounts are unchanged.

Local and hosted execution

Add --local --path ./agent to configure, connect, status, test, list, and disconnect to store credentials on this machine. The local authorization-code callback is http://127.0.0.1:8788/callback; --callback-port selects another port. Register both this URL and the platform callback shown in Amodal when one provider application serves both.

Local credentials are encrypted under ~/.amodal/oauth/ with an owner-only key file. They are not copied into the repository or the deployment bundle. A missing key requires reauthorization.

amodal dev uses the local bindings. amodal dev --oauth-cloud uses the selected cloud agent's bindings through the platform broker; select the agent with amodal login or amodal agent first.

Hosted runtimes resolve the named connection for their agent, so the connection's files must be in the active deployment. REST requests read the current binding at request time. HTTP MCP sessions bind at initialization: create a fresh session after changing an MCP application's scopes, resource origins, or account.

The request tool keeps its permission and confirmation checks. Custom code using ctx.request() must enforce its own operation authorization. Both paths enforce the approved resource origin. A failed resource request is not replayed.

Managed providers

Managed providers are OAuth applications hosted by the platform: the Google APIs below and Slack. Select one with auth.provider. A provider appears in the picker only when the platform has its client ID and client secret configured.

auth.providerScopes beyond openidSelection
google-analyticsanalytics.readonlyProperty ID
google-sheetsspreadsheetsSpreadsheet ID
google-docsdocumentsDocument ID
google-drivedrive.filenone
google-calendarcalendar.readonly, calendar.eventsCalendar ID
google-search-consolewebmasters.readonlySite URL
google-bigquerybigqueryProject ID

All managed Google providers share one Google OAuth client. Drive's drive.file scope grants access to files created by, opened with, or shared with that Google app, per Google account. Connections that authorize the same Google account therefore see the same files, including files created through another connection. Amodal has no Google Picker integration, so it cannot authorize arbitrary existing files.

Slack

slack is a managed provider whose authorized account is the workspace, so reconnect requires the same workspace. The connection's spec.json gets the Web API base URL and auth.test as its test operation. The bot token has the app_mentions:read, assistant:write, chat:write, im:history, channels:read, groups:read, channels:history, groups:history, and users:read scopes: the agent reads the channels the app is invited to and posts as the app.

To answer direct messages and @mentions, the agent lists @amodalai/channel-slack-assistant among the packages in amodal.json. The package ships the slack-assistant channel, so the agent needs no channels/slack-assistant/channel.json of its own. Adding a managed Slack connection from the Studio connections page commits that packages entry in the same commit as the connection files, unless the package is already listed. Studio leaves amodal.json unchanged when the repository has none or it is not valid JSON; list the package by hand in that case.

In a channel the app is invited to, an @mention gets a reply in a thread under it. A follow-up in that thread needs another @mention, and everyone in the thread shares one conversation. Nothing else is configured per agent. The platform's Slack app receives every workspace's events at one request URL and relays each one, with the workspace's bot token, to the agent whose Slack connection is bound to that workspace. An event is not delivered when two agents are bound to the same workspace, when the grant needs reconnection, or when the agent has no active production deployment. A channel that declares its own signingSecret still receives relayed events, and it also accepts requests that Slack signs with that secret.

While the agent works on a message, in a direct message or a channel thread, Slack shows its working indicator with a Stop button, and the reply streams into the thread as the agent writes it. The indicator ends when the turn ends: after the reply, when the agent writes no text, when the turn fails, and when someone presses Stop. Stop ends the turn, and the text already streamed stays in the thread. Slack shows Stop only when the app subscribes to the agent_session_stopped bot event, which the generated manifest includes.

A custom Slack application with "scopeSeparator": ",", "bearerTokenTypes": ["bot"], and "accountIdPath": "team.id" in its oauth.json can call the Web API, but only the managed provider receives relayed events.

Platform operators create the shared Slack app from a generated manifest. Run node scripts/slack-app-manifest.mjs https://platform.example.com with the platform's base URL (add --name "My App" to change the app name from Amodal). In Slack, choose Create New App, From an app manifest, and paste the output. The manifest carries the redirect URL, the Events API request URL, the event subscriptions, the Agents & AI Apps feature, and the bot scopes from the managed Slack provider. After creating the app, set SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, and SLACK_SIGNING_SECRET on the platform from the app's Basic Information page. When SLACK_CLIENT_ID is set without SLACK_SIGNING_SECRET, the platform logs slack_signing_secret_missing once per process and answers every Slack event with a 503.

Recipes

Google Analytics 4 with your own application

Register a Google OAuth web application and enable the Analytics Data API:

{
  "label": "Analytics account",
  "grantType": "authorization_code",
  "issuer": "https://accounts.google.com",
  "discoveryUrl": "https://accounts.google.com/.well-known/openid-configuration",
  "clientId": "YOUR_CLIENT_ID",
  "tokenAuthMethod": "client_secret_post",
  "scopes": ["openid", "https://www.googleapis.com/auth/analytics.readonly"],
  "resourceOrigins": ["https://analyticsdata.googleapis.com", "https://openidconnect.googleapis.com"],
  "authorizationParameters": { "access_type": "offline" },
  "accountUrl": "https://openidconnect.googleapis.com/v1/userinfo",
  "accountIdPath": "sub"
}

openid and the account lookup identify the account on reconnect; the broker does not use the ID token as the user's identity. access_type: offline obtains a refresh token. Add prompt: "consent" only when reconsent is needed.

Test against a real property. For property 123456789:

{
  "baseUrl": "https://analyticsdata.googleapis.com/v1beta",
  "auth": { "type": "oauth2" },
  "test": {
    "method": "POST",
    "path": "properties/123456789:runReport",
    "body": {
      "dateRanges": [{ "startDate": "yesterday", "endDate": "yesterday" }],
      "metrics": [{ "name": "eventCount" }],
      "limit": "1"
    }
  }
}

An empty successful report verifies access. An account without access to the property fails the test even when OAuth connects.

GitLab

{
  "label": "GitLab account",
  "grantType": "authorization_code",
  "issuer": "https://gitlab.com",
  "discoveryUrl": "https://gitlab.com/.well-known/openid-configuration",
  "clientId": "YOUR_CLIENT_ID",
  "tokenAuthMethod": "client_secret_post",
  "scopes": ["read_api", "read_user"],
  "resourceOrigins": ["https://gitlab.com"],
  "accountUrl": "https://gitlab.com/api/v4/user",
  "accountIdPath": "id"
}

With base URL https://gitlab.com/api/v4, use GET user as the test.

Auth0 client credentials

Create a machine-to-machine application authorized for the API, with token authentication set to Client Secret (Post):

{
  "label": "Reporting service",
  "grantType": "client_credentials",
  "issuer": "https://YOUR_TENANT.us.auth0.com/",
  "tokenUrl": "https://YOUR_TENANT.us.auth0.com/oauth/token",
  "clientId": "YOUR_CLIENT_ID",
  "tokenAuthMethod": "client_secret_post",
  "scopes": ["read:reports"],
  "resourceOrigins": ["https://YOUR_API_HOST"],
  "tokenParameters": { "audience": "YOUR_API_IDENTIFIER" }
}

The audience is the Auth0 API identifier, and the resource origin is the API host. This grant has no account lookup or browser consent; expired tokens are reacquired with client credentials.

Troubleshooting

  • Revoked credentials or reduced consent: reconnect the account.
  • Configuration revision errors: approve the intended oauth.json by reconnecting.
  • Resource permission failures: grant the account access to the selected resource.
  • An approved scope that returns 403: enable the matching API in the provider's project.

Error messages include safe codes and correlation IDs. Token values never appear in diagnostics.