# Scalekit AgentKit > Every AgentKit guide, in the order the docs teach it: what AgentKit is, the quickstart, how it works, connecting users, calling tools, Virtual MCP servers, frameworks, agent platforms, custom connectors, managing your workspace, going to production and troubleshooting, then the AgentKit cookbooks. Connectors are listed in https://docs.scalekit.com/_llms-txt/agentkit-connectors.txt, the framework guides and SDK setup are in https://docs.scalekit.com/_llms-txt/agentkit-frameworks.txt, and the REST API reference, with the Python and Node.js SDK method for each endpoint and the webhooks, is in https://docs.scalekit.com/_llms-txt/agentkit-reference.txt. ## Pages in this file, in reading order - https://docs.scalekit.com/agentkit/overview.md - https://docs.scalekit.com/agentkit/quickstart.md - https://docs.scalekit.com/agentkit/concepts.md - https://docs.scalekit.com/agentkit/connections.md - https://docs.scalekit.com/agentkit/tools/authorize.md - https://docs.scalekit.com/agentkit/hosted-connect-widget.md - https://docs.scalekit.com/agentkit/user-verification.md - https://docs.scalekit.com/agentkit/connected-accounts.md - https://docs.scalekit.com/agentkit/account-events.md - https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools.md - https://docs.scalekit.com/agentkit/tools/custom-tools.md - https://docs.scalekit.com/agentkit/mcp/overview.md - https://docs.scalekit.com/agentkit/mcp/configure-mcp-server.md - https://docs.scalekit.com/agentkit/mcp/connection-mcp-servers.md - https://docs.scalekit.com/agentkit/mcp/session-tokens.md - https://docs.scalekit.com/agentkit/mcp/read-only-tokens.md - https://docs.scalekit.com/agentkit/mcp/connect-any-client.md - https://docs.scalekit.com/agentkit/triggers/overview.md - https://docs.scalekit.com/agentkit/triggers/set-up.md - https://docs.scalekit.com/agentkit/triggers/receive-events.md - https://docs.scalekit.com/agentkit/triggers/manage.md - https://docs.scalekit.com/agentkit/agent-gateway/overview.md - https://docs.scalekit.com/agentkit/agent-gateway/quickstart.md - https://docs.scalekit.com/agentkit/agent-gateway/connect-ai-clients.md - https://docs.scalekit.com/agentkit/agent-gateway/users-and-access.md - https://docs.scalekit.com/agentkit/agent-gateway/access-policies.md - https://docs.scalekit.com/agentkit/agent-gateway/plans-and-limits.md - https://docs.scalekit.com/agentkit/examples.md - https://docs.scalekit.com/agentkit/examples/langchain.md - https://docs.scalekit.com/agentkit/examples/langgraph.md - https://docs.scalekit.com/agentkit/examples/google-adk.md - https://docs.scalekit.com/agentkit/examples/openai.md - https://docs.scalekit.com/agentkit/examples/openai-agents-sdk.md - https://docs.scalekit.com/agentkit/examples/anthropic.md - https://docs.scalekit.com/agentkit/examples/vercel-ai.md - https://docs.scalekit.com/agentkit/examples/mastra.md - https://docs.scalekit.com/agentkit/examples/crewai.md - https://docs.scalekit.com/agentkit/examples/claude-managed-agents.md - https://docs.scalekit.com/agentkit/coding-agents.md - https://docs.scalekit.com/agentkit/hermes.md - https://docs.scalekit.com/agentkit/bring-your-own-connector/overview.md - https://docs.scalekit.com/agentkit/bring-your-own-connector/create-connector.md - https://docs.scalekit.com/agentkit/bring-your-own-connector/making-tool-calls.md - https://docs.scalekit.com/agentkit/environments.md - https://docs.scalekit.com/agentkit/api-credentials.md - https://docs.scalekit.com/agentkit/team-members.md - https://docs.scalekit.com/agentkit/billing.md - https://docs.scalekit.com/agentkit/enterprise-features.md - https://docs.scalekit.com/agentkit/log-streaming.md - https://docs.scalekit.com/agentkit/encryption-keys.md - https://docs.scalekit.com/agentkit/advanced/launch-checklist.md - https://docs.scalekit.com/agentkit/security.md - https://docs.scalekit.com/agentkit/advanced/branding.md - https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth.md - https://docs.scalekit.com/agentkit/advanced/custom-domain.md - https://docs.scalekit.com/agentkit/self-hosted.md - https://docs.scalekit.com/agentkit/troubleshooting.md - https://docs.scalekit.com/agentkit/advanced/migrate-from-composio.md ## API reference concepts - [Authentication](https://docs.scalekit.com/agentkit/reference/authentication.md): Authenticate to the AgentKit API with the OAuth 2.0 client credentials grant: get your credentials, request an access token and send it as a bearer token. - [Errors and rate limits](https://docs.scalekit.com/agentkit/reference/errors.md): AgentKit API errors: the error body, how to tell a Scalekit error from the app's, what each tool error code means, and how to handle them and rate limits. - [Pagination](https://docs.scalekit.com/agentkit/reference/pagination.md): Page through AgentKit list endpoints with page_size and page_token: which endpoints page, their page size limits, and a loop in cURL, Python and Node.js. --- Source: https://docs.scalekit.com/agentkit/overview.md # What is AgentKit How AgentKit connects your AI agent to each user's Gmail, Slack, GitHub and other apps, what you can build with it, and what Scalekit handles for you. **AgentKit lets your AI agent act in third-party apps on behalf of each of your users.** A user connects their Gmail, Slack, Salesforce or GitHub account once through an authorization link. Scalekit stores and refreshes their tokens, and your agent calls prebuilt tools such as `gmail_send_message` with that user's ID. Your code never handles OAuth tokens. 622 [connectors](https://docs.scalekit.com/agentkit/connectors/) · Python and Node.js SDKs · REST API · Any MCP client New to AgentKit? Start with the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/). Looking for something specific? See [all AgentKit sections](#all-agentkit-sections). > tip: Giving your own team's AI tools access to company apps? > > That's [Agent Gateway](https://docs.scalekit.com/agentkit/agent-gateway/overview/): one MCP URL for Claude, ChatGPT, Cursor and other > AI clients, set up in the dashboard. See [Choose AgentKit or Agent > Gateway](https://docs.scalekit.com/agentkit/agent-gateway/overview/#choose-agentkit-or-agent-gateway). ## What you can build - **An inbox assistant** reads, labels and drafts email in each user's Gmail or Outlook. - **A sales copilot** logs calls and updates deals in Salesforce or HubSpot after every meeting. - **An engineering agent** files issues and reviews pull requests in GitHub, Linear or Jira. ## How it works 1. **You set up a connection** (once per app): in the dashboard. 2. **Your user approves access** (once per user): authorization link. 3. **Scalekit stores tokens**: connected account is ACTIVE once any user verification passes. 4. **Your agent calls a tool** (every tool call): identifier + tool name. On every call: Scalekit adds the token, calls the API and returns JSON to your agent. The app (Gmail, Slack, GitHub…) sees an API call with the user's token. Steps 1 and 2 happen once. Step 4 happens on every call. Your code never handles an OAuth token. [How AgentKit works](https://docs.scalekit.com/agentkit/concepts/) covers the full model, every term and every connected-account status. ## Two ways to give your agent tools | | From your code (SDK) | From an MCP client | | --- | --- | --- | | You get | Tool schemas and `execute_tool` in Python or Node.js | A [Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/overview/) URL, with a session token for each user | | Works with | [LangChain](https://docs.scalekit.com/agentkit/examples/langchain/), [Google ADK](https://docs.scalekit.com/agentkit/examples/google-adk/), [OpenAI](https://docs.scalekit.com/agentkit/examples/openai/), [Anthropic](https://docs.scalekit.com/agentkit/examples/anthropic/), [Vercel AI SDK](https://docs.scalekit.com/agentkit/examples/vercel-ai/), [Mastra](https://docs.scalekit.com/agentkit/examples/mastra/), your own loop | [Claude Managed Agents](https://docs.scalekit.com/agentkit/examples/claude-managed-agents/), [CrewAI](https://docs.scalekit.com/agentkit/examples/crewai/), any MCP client | | Pick it when | You build and host the agent | The agent already exists and speaks MCP | ## What Scalekit handles, and what you build | Scalekit handles | You build | | --- | --- | | OAuth flows and consent screens for every connector | Pick which apps to connect, once, in the dashboard | | Encrypted token storage and refresh | Pass a stable, hard-to-guess ID for each user | | Tool schemas, tool execution and the API proxy | [Verify users](https://docs.scalekit.com/agentkit/user-verification/) in production | | Webhooks when an account's status changes | Choose which tools each agent gets | ## Built for production - **Compliance**: SOC 2 Type II and ISO 27001 certified. GDPR and CCPA compliant. [HIPAA](https://www.scalekit.com/legal/hipaa) eligible, with a Business Associate Agreement on Enterprise plans. - **Token vault**: every OAuth token and API key is encrypted with a key unique to your environment before it's stored. That key is wrapped by a master key in Google Cloud KMS, held apart from the database, so a copy of the database alone yields nothing usable. - **Tool call data**: responses pass through to your agent and aren't stored. Tool call logs record the call's metadata, not its content. See [what Scalekit stores](https://docs.scalekit.com/agentkit/security/#what-scalekit-stores). - **Encryption**: AES-256 at rest and TLS 1.3 in transit. [Bring your own key](https://docs.scalekit.com/agentkit/encryption-keys/) from your Google Cloud KMS; revoking it makes the stored data unusable to Scalekit. - **Data residency**: separate [US and EU deployments](https://docs.scalekit.com/agentkit/environments/#choose-a-region) with no shared state. The EU region runs in Frankfurt. - **Self-hosted**: with an enterprise license, run AgentKit [in your own infrastructure](https://docs.scalekit.com/agentkit/self-hosted/), on Kubernetes or Docker. Once installed, it has no connection to Scalekit's infrastructure. [Security and compliance](https://docs.scalekit.com/agentkit/security/) covers how credentials are stored, what Scalekit keeps, and what your app is responsible for. Audit reports and penetration test summaries are available under NDA from the [Trust Center](https://www.scalekit.com/trust-center). Read the [Data Processing Agreement](https://www.scalekit.com/legal/data-processing-agreement), which lists sub-processors. See [pricing](https://www.scalekit.com/pricing) for plans. ## All AgentKit sections Every part of the AgentKit docs, in the order you build. Each section matches a group in the sidebar. ### Start Learn what AgentKit does and make your first tool call. - [What is AgentKit](https://docs.scalekit.com/agentkit/overview.md) - [Quickstart](https://docs.scalekit.com/agentkit/quickstart.md) - [How AgentKit works](https://docs.scalekit.com/agentkit/concepts.md) ### Connect users Set up an app connection, then have each user authorize it. - [Set up a connection](https://docs.scalekit.com/agentkit/connections.md) - [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize.md) - [Hosted Connect Widget](https://docs.scalekit.com/agentkit/hosted-connect-widget.md) - [Verify users](https://docs.scalekit.com/agentkit/user-verification.md) - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts.md) - [Account events](https://docs.scalekit.com/agentkit/account-events.md) ### Call tools with the SDK Run prebuilt tools, or call any API endpoint as the user. - [Use built-in tools](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools.md) - [Call any API](https://docs.scalekit.com/agentkit/tools/custom-tools.md) ### Call tools over MCP Give any MCP client a per-user set of tools. - [How Virtual MCP servers work](https://docs.scalekit.com/agentkit/mcp/overview.md) - [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server.md) - [Connection MCP servers](https://docs.scalekit.com/agentkit/mcp/connection-mcp-servers.md) - [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens.md) - [Read-only tokens](https://docs.scalekit.com/agentkit/mcp/read-only-tokens.md) - [Connect any MCP client](https://docs.scalekit.com/agentkit/mcp/connect-any-client.md) ### Triggers (Beta) Have your agent act when something changes in a connected app. - [How triggers work](https://docs.scalekit.com/agentkit/triggers/overview.md) - [Set up a trigger](https://docs.scalekit.com/agentkit/triggers/set-up.md) - [Receive events](https://docs.scalekit.com/agentkit/triggers/receive-events.md) - [Manage subscriptions](https://docs.scalekit.com/agentkit/triggers/manage.md) ### Agent Gateway Give your team's AI tools one governed MCP URL for your apps. - [What is Agent Gateway](https://docs.scalekit.com/agentkit/agent-gateway/overview.md) - [Quickstart](https://docs.scalekit.com/agentkit/agent-gateway/quickstart.md) - [Connect AI clients](https://docs.scalekit.com/agentkit/agent-gateway/connect-ai-clients.md) - [Users and access](https://docs.scalekit.com/agentkit/agent-gateway/users-and-access.md) - [Access policies](https://docs.scalekit.com/agentkit/agent-gateway/access-policies.md) - [Plans and limits](https://docs.scalekit.com/agentkit/agent-gateway/plans-and-limits.md) ### Frameworks Wire AgentKit into your agent framework. - [Choose a framework](https://docs.scalekit.com/agentkit/examples.md) - [LangChain](https://docs.scalekit.com/agentkit/examples/langchain.md) - [LangGraph](https://docs.scalekit.com/agentkit/examples/langgraph.md) - [Google ADK](https://docs.scalekit.com/agentkit/examples/google-adk.md) - [OpenAI](https://docs.scalekit.com/agentkit/examples/openai.md) - [OpenAI Agents SDK](https://docs.scalekit.com/agentkit/examples/openai-agents-sdk.md) - [Anthropic](https://docs.scalekit.com/agentkit/examples/anthropic.md) - [Vercel AI SDK](https://docs.scalekit.com/agentkit/examples/vercel-ai.md) - [Mastra](https://docs.scalekit.com/agentkit/examples/mastra.md) - [CrewAI](https://docs.scalekit.com/agentkit/examples/crewai.md) - [Claude Managed Agents](https://docs.scalekit.com/agentkit/examples/claude-managed-agents.md) ### Agent platforms Use AgentKit from coding agents and agent platforms. - [Coding agents](https://docs.scalekit.com/agentkit/coding-agents.md) - [Hermes](https://docs.scalekit.com/agentkit/hermes.md) ### Add your own connector Connect an app or internal API that isn't in the catalog. - [When to add one](https://docs.scalekit.com/agentkit/bring-your-own-connector/overview.md) - [Create a connector](https://docs.scalekit.com/agentkit/bring-your-own-connector/create-connector.md) - [Call your connector](https://docs.scalekit.com/agentkit/bring-your-own-connector/making-tool-calls.md) ### Manage your workspace Environments, API credentials, team members and billing. - [Environments and regions](https://docs.scalekit.com/agentkit/environments.md) - [API credentials](https://docs.scalekit.com/agentkit/api-credentials.md) - [Team members and roles](https://docs.scalekit.com/agentkit/team-members.md) - [Billing](https://docs.scalekit.com/agentkit/billing.md) - [Enterprise features](https://docs.scalekit.com/agentkit/enterprise-features.md) - [Log streaming](https://docs.scalekit.com/agentkit/log-streaming.md) - [Encryption keys](https://docs.scalekit.com/agentkit/encryption-keys.md) - [Pricing](https://www.scalekit.com/pricing) ### Go to production Check everything before launch, and harden your setup. - [Launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist.md) - [Security and compliance](https://docs.scalekit.com/agentkit/security.md) - [Brand the widget](https://docs.scalekit.com/agentkit/advanced/branding.md) - [Use your own OAuth app](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth.md) - [Custom domain](https://docs.scalekit.com/agentkit/advanced/custom-domain.md) - [Self-hosted deployment](https://docs.scalekit.com/agentkit/self-hosted.md) ### Help Fix common errors, or move an existing integration over. - [Troubleshooting](https://docs.scalekit.com/agentkit/troubleshooting.md) - [Migrate from Composio](https://docs.scalekit.com/agentkit/advanced/migrate-from-composio.md) ### Connectors Every app AgentKit connects to, with its tools. - [Browse connectors](https://docs.scalekit.com/agentkit/connectors.md) ### Reference Every SDK method, REST endpoint and webhook event. - [API reference](https://docs.scalekit.com/agentkit/reference.md) --- Source: https://docs.scalekit.com/agentkit/quickstart.md # Quickstart Make your first AgentKit tool call in Python, Node.js or cURL: install the SDK, add your credentials, and call GitHub as yourself. AgentKit lets your AI agents act in your users' apps on their behalf. New to it? Read [What is AgentKit](https://docs.scalekit.com/agentkit/overview/). Adding login to your own app instead? See the [SaaSKit quickstart](https://docs.scalekit.com/authenticate/fsa/quickstart/). Connect your own GitHub account and make your first tool call as that user. New Scalekit environments include a GitHub connection, so there's nothing to set up in GitHub. **About 10 minutes.** ## Before you start - A Scalekit account at [app.scalekit.com](https://app.scalekit.com) - Python 3.11+, Node.js 18.14+, or `curl` and [`jq`](https://jqlang.org/) - Your environment URL, client ID and client secret. Open **Developers** > **Settings** > **API Credentials**: copy the **Environment URL** and **Client ID** from **Environment details**, then select **Generate new secret** under **Client secrets**. The secret is shown once, so copy it right away. See [API credentials](https://docs.scalekit.com/agentkit/api-credentials/). - The GitHub connection name from **AgentKit** > **Connections**. New environments call it `github-connect`. No GitHub connection? Create one in **AgentKit** > **Connections** (see [Set up a connection](https://docs.scalekit.com/agentkit/connections/)) and use its name. - **AgentKit** > **Settings** > **User Verification** set to **None** or **Scalekit users only**. With **Scalekit users only**, stay signed in to the dashboard in the same browser when you approve access. 1. ## Install the SDK **Python** ```sh pip install scalekit-sdk-python python-dotenv ``` **Node.js** ```sh npm init -y npm pkg set type=module npm install @scalekit-sdk/node dotenv npm install -D tsx ``` 2. ## Add your credentials Create a `.env` file next to your script. Don't commit it. ```sh title=".env" SCALEKIT_ENVIRONMENT_URL= SCALEKIT_CLIENT_ID= SCALEKIT_CLIENT_SECRET= GITHUB_CONNECTION_NAME=github-connect ``` 3. ## Write the script Save this as `agent.py`, `agent.ts` or `agent.sh`. It connects the user if needed, then calls GitHub as them. **Python** ```python title="agent.py" import os from dotenv import load_dotenv from scalekit import ScalekitClient from scalekit.common.exceptions import ScalekitServerException load_dotenv() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions connection_name = os.environ["GITHUB_CONNECTION_NAME"] user_id = "user_123" # your app's ID for this user: stable and hard to guess # Find this user's connected account, or create it on the first run account = actions.get_or_create_connected_account( connection_name=connection_name, identifier=user_id, ).connected_account if account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=connection_name, identifier=user_id, ).link print(f"Open this link and approve access:\n{link}") input("Press Enter when you're done...") account = actions.get_connected_account_details( connection_name=connection_name, identifier=user_id, ).connected_account if account.status != "ACTIVE": raise SystemExit(f"The account is {account.status}, not ACTIVE. Run the script again.") # Call GitHub as this user. The tool is read-only. try: result = actions.execute_tool( tool_name="github_user_get_authenticated", tool_input={}, connected_account_id=account.id, ) except ScalekitServerException as e: raise SystemExit(f"Tool call failed: {e.error_code} {e.message}") print(result.data) ``` **Node.js** ```ts title="agent.ts" import 'dotenv/config'; import { createInterface } from 'node:readline/promises'; import { ConnectorStatus, ScalekitClient, ScalekitServerException } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const actions = scalekit.actions; const connectionName = process.env.GITHUB_CONNECTION_NAME!; const userId = 'user_123'; // your app's ID for this user: stable and hard to guess // Find this user's connected account, or create it on the first run let { connectedAccount: account } = await actions.getOrCreateConnectedAccount({ connectionName, identifier: userId, }); if (account?.status !== ConnectorStatus.ACTIVE) { const { link } = await actions.getAuthorizationLink({ connectionName, identifier: userId }); console.log(`Open this link and approve access:\n${link}`); const rl = createInterface({ input: process.stdin, output: process.stdout }); await rl.question("Press Enter when you're done..."); rl.close(); ({ connectedAccount: account } = await actions.getConnectedAccount({ connectionName, identifier: userId, })); if (account?.status !== ConnectorStatus.ACTIVE) { throw new Error('The account is not ACTIVE yet. Run the script again.'); } } // Call GitHub as this user. The tool is read-only. try { const result = await actions.executeTool({ toolName: 'github_user_get_authenticated', toolInput: {}, connectedAccountId: account.id, }); console.log(result.data); } catch (err) { if (!(err instanceof ScalekitServerException)) throw err; console.error('Tool call failed:', err.errorCode, err.message); process.exit(1); } ``` **cURL** ```bash title="agent.sh" #!/usr/bin/env bash set -euo pipefail set -a; source .env; set +a USER_ID="user_123" # your app's ID for this user: stable and hard to guess API="$SCALEKIT_ENVIRONMENT_URL/api/v1" # Exchange your API credentials for an access token TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \ -d grant_type=client_credentials \ -d client_id="$SCALEKIT_CLIENT_ID" \ -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token) account_status() { curl -sS -G "$API/connected_accounts/details" \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode "connector=$GITHUB_CONNECTION_NAME" \ --data-urlencode "identifier=$USER_ID" | jq -r '.connected_account.status // "NOT_CREATED"' } if [ "$(account_status)" != "ACTIVE" ]; then # Creates the connected account on the first run, and returns a link LINK=$(curl -sS "$API/connected_accounts/magic_link" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"connector\": \"$GITHUB_CONNECTION_NAME\", \"identifier\": \"$USER_ID\"}" | jq -r .link) printf 'Open this link and approve access:\n%s\n' "$LINK" read -r -p "Press Enter when you're done..." STATUS=$(account_status) if [ "$STATUS" != "ACTIVE" ]; then echo "The account is $STATUS, not ACTIVE. Run the script again." >&2 exit 1 fi fi # Call GitHub as this user. The tool is read-only. curl -sS "$API/execute_tool" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d "{\"tool_name\": \"github_user_get_authenticated\", \"connector\": \"$GITHUB_CONNECTION_NAME\", \"identifier\": \"$USER_ID\", \"params\": {}}" | jq .data ``` `user_123` stands in for your app's user ID. [Choose an identifier](https://docs.scalekit.com/agentkit/concepts/#choose-an-identifier) explains what to use in production. 4. ## Run it **Python** ```sh python agent.py ``` **Node.js** ```sh npx tsx agent.ts ``` **cURL** ```sh bash agent.sh ``` The first run prints a link. Open it, approve access on GitHub, then press Enter. You'll see your GitHub profile. Run it again: the account is already `ACTIVE`, so the script skips the link. ## If it doesn't work ### `connection not found for the given key` `GITHUB_CONNECTION_NAME` doesn't match the dashboard exactly. Copy it again from **AgentKit** > **Connections**. ### The account stays `PENDING_AUTH` The approval wasn't finished, or the link expired. Links last five minutes by default. Run the script again for a fresh link. ### `user_verify_url is required when user_verify_mode is b2b` User verification is set to **Custom user verifier**, so the link request needs a verify URL. For this quickstart, use **None** or **Scalekit users only** in **AgentKit** > **Settings** > **User Verification**. To set up the custom user verifier, see [Verify users](https://docs.scalekit.com/agentkit/user-verification/). ### `KeyError: 'SCALEKIT_ENVIRONMENT_URL'` or `TypeError: Invalid URL` The `.env` file wasn't loaded. Run the script from the folder that contains it. ### `401` or `invalid_client` The client ID or secret is wrong, or the secret was regenerated. ### Still stuck? Find the error message or the account status on [Troubleshoot connection and OAuth errors](https://docs.scalekit.com/agentkit/troubleshooting/). It covers each connected-account status, the errors on Scalekit's connection page, OAuth app misconfiguration and provider-specific errors. ## Next - [How AgentKit works](https://docs.scalekit.com/agentkit/concepts/): What you just created, and why. - [Set up a connection](https://docs.scalekit.com/agentkit/connections/): Connect Gmail, Slack or any other app. - [Choose a framework](https://docs.scalekit.com/agentkit/examples/): LangChain, OpenAI, Anthropic and more. Before real users connect accounts, [verify users](https://docs.scalekit.com/agentkit/user-verification/). ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Create a connected account](https://docs.scalekit.com/agentkit/reference/connected-accounts/create-a-connected-account.md) - `GET` [Get a connected account](https://docs.scalekit.com/agentkit/reference/connected-accounts/get-a-connected-account.md) - `GET` [Get a connected account's credentials](https://docs.scalekit.com/agentkit/reference/connected-accounts/get-connected-account-credentials.md) - `POST` [Get an authorization link](https://docs.scalekit.com/agentkit/reference/authorization/get-an-authorization-link.md) - `POST` [Execute a tool](https://docs.scalekit.com/agentkit/reference/tools/execute-a-tool.md) --- Source: https://docs.scalekit.com/agentkit/concepts.md # How AgentKit works Connections, connected accounts, identifiers and tools: the AgentKit model, every connected-account status, and the user verification modes. This page explains the model behind AgentKit. For what AgentKit is and when to use it, see [What is AgentKit](https://docs.scalekit.com/agentkit/overview/). You configure a **connection** once per app. Each user approves access once, which activates their **connected account**. After that, your agent calls **tools** with the user's **identifier**, and Scalekit makes the API call with that user's token. 1. **You set up a connection** (once per app): in the dashboard. 2. **Your user approves access** (once per user): authorization link. 3. **Scalekit stores tokens**: connected account is ACTIVE once any user verification passes. 4. **Your agent calls a tool** (every tool call): identifier + tool name. On every call: Scalekit adds the token, calls the API and returns JSON to your agent. The app (Gmail, Slack, GitHub…) sees an API call with the user's token. Steps 1 and 2 happen once. Step 4 happens on every call. Your code never handles an OAuth token. ## Key terms | Term | What it is | Who creates it | In code | | --- | --- | --- | --- | | Connector | A supported app, such as Gmail, and its library of tools. | Scalekit, or you for a [custom connector](https://docs.scalekit.com/agentkit/bring-your-own-connector/overview/) | `gmail` | | Connection | Your environment's settings for one connector: an OAuth client and scopes, or the fields for API-key sign-in. All your users share it. | You, once, in the dashboard | `connection_name` | | Connected account | One user's link to a connection. It holds their tokens and a status. | Your code creates it. The user activates it by approving access. | `connected_account_id` | | Identifier | Your app's ID for the user. Always pass it with the connection name. | You | `identifier` | | Authorization link | A one-time URL where the user approves access. | Scalekit, when your code asks for one | `get_authorization_link` | | User verification | The check that the person who approved access is the user you meant. | Your server, or Scalekit | `verify_connected_account_user` | | Tool | One action on a connector, with an input schema. | Scalekit for built-in tools, or you for custom tools | `tool_name` | | Virtual MCP server | A URL that exposes chosen tools for one user to any MCP client. | You, per agent | `actions.mcp` | | Session token | A short-lived bearer token that lets an MCP client call a Virtual MCP server's tools as one user. [Mint one](https://docs.scalekit.com/agentkit/mcp/session-tokens/) before each run. | Your server, per run | `actions.mcp.create_session_token` | | API proxy | A call to the app's own API through Scalekit, which adds the user's credentials. Use it when no [built-in tool](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/) fits. See [Call any API](https://docs.scalekit.com/agentkit/tools/custom-tools/). | Your code | `actions.request` | | Trigger | An event in a user's connected app, such as a new lead, that Scalekit sends to your server as a signed webhook so your agent can act. In beta. See [How triggers work](https://docs.scalekit.com/agentkit/triggers/overview/). | You subscribe, in the dashboard | `trigger_type` | The **In code** column shows Python names. The Node.js SDK uses the camelCase form, such as `connectionName` and `connectedAccountId`. One exception: Node.js `executeTool` takes the connection name as `connector` and the tool's inputs as `toolInput`. The REST API also names the connection `connector`. ## Connected account statuses | Status | Means | What to do | | --- | --- | --- | | `ACTIVE` | Tokens are valid. | Call tools. | | `PENDING_AUTH` | The user hasn't finished approving access. | Send a new authorization link. | | `PENDING_VERIFICATION` | The user approved, but user verification hasn't confirmed them. | Finish your verify step, or check the verification mode. | | `EXPIRED` | Tokens expired or were revoked and couldn't be refreshed. | Send a new authorization link. | | `DISCONNECTED` | The account was disconnected. | Reconnect with a new authorization link. | In Python, `status` is a string such as `"ACTIVE"`. In Node.js, it is the numeric `ConnectorStatus` enum: import it from `@scalekit-sdk/node` (2.18.0 or later) and compare with `ConnectorStatus.ACTIVE`. The REST API returns the name as a string. ## Choose an identifier Use your app's internal user ID. It must be stable (it never changes), unique per user and hard to guess. Don't use an email address: a user can change their email, and an old address can be reassigned to someone else, so the identifier would no longer point to the same person. An email is also easy to guess, which makes misuse easier. Pass the identifier together with the connection name. When you pass `connected_account_id` instead, Scalekit ignores the identifier. ## User verification modes Set the mode in **AgentKit** > **Settings** > **User Verification**. New environments start with **None**. | Mode | What happens | Use it for | | --- | --- | --- | | Custom user verifier | Scalekit redirects the user to your verify URL. Your server confirms the user and calls the verify API. | Production (recommended) | | Scalekit users only | The person approving access must be signed in to your Scalekit dashboard. | Internal testing | | None | Anyone with the link activates the account. | Development only | [Verify users](https://docs.scalekit.com/agentkit/user-verification/) shows how to set up the custom user verifier. ## Next - [Set up a connection](https://docs.scalekit.com/agentkit/connections/): Configure the app your agent needs. - [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/): Send the authorization link and wait until the account is ACTIVE. - [Verify users](https://docs.scalekit.com/agentkit/user-verification/): Set up the custom user verifier before real users connect. --- Source: https://docs.scalekit.com/agentkit/connections.md # Set up a connection Set up a connection in the Scalekit dashboard or with the API, so your agent can use a third-party connector on behalf of your users. A **connection** is a configuration you create once, in the Scalekit dashboard or [with the API](#set-up-a-connection-with-the-api). It holds everything Scalekit needs to interact with a connector's API: OAuth app credentials, scopes, redirect URIs, and so on. One connection serves all your users. Users don't configure connections. Each user gets a **connected account**, the per-user record that links them to a connection and holds their tokens. Your code creates it with `get_or_create_connected_account`, or when it requests an authorization link for the user, and the user activates it by authorizing. ## What the connection form asks for The connection form adapts to what the connector requires. Two things determine how much you need to configure: - **OAuth-based connectors** require the most setup. You register an OAuth app with the provider, then enter those credentials into Scalekit. - **Non-OAuth connectors** (API key, basic auth, key pairs, and similar) require minimal developer setup (usually just a name). The user provides their own credentials when they create their connected account. The sections below walk through both patterns. ## Set up an OAuth connection OAuth connections need an OAuth app registered with the provider. Scalekit provides the Redirect URI; you bring the Client ID and Client Secret. Many connectors also offer Scalekit's own credentials, so you can skip registration entirely (see the tip at the end of this section). **Already see pre-filled credentials? DCR handled registration for you.** For some connectors, Scalekit automatically completes **Dynamic Client Registration (DCR)** with the provider. If the **Client ID**, **Client Secret**, **OAuth Authorization URL**, and **Token Endpoint** fields are already filled in when you open the connection form, DCR was successful — skip steps 2–4 below and go directly to [configuring scopes](#configure-scopes). If the fields are empty, the provider does not support DCR. Follow the manual registration steps below. 1. ### Open the connection form In the Scalekit dashboard, go to **AgentKit** > **Connections** and click **Add connection**. Select the connector you want to configure. The form shows the fields that connector requires. 2. ### Copy the redirect URI Scalekit generates a **Redirect URI** for this connection. Copy it; you'll need it in the next step. This URI is where the provider sends the user after they complete the OAuth consent screen. Scalekit handles the callback automatically. 3. ### Register your OAuth app with the provider In the provider's developer console (GitHub, Salesforce, Google, etc.), create an OAuth app and add Scalekit's Redirect URI to the list of authorized redirect URIs. The provider will give you a **Client ID** and **Client Secret** after registration. > caution: Redirect URI must match exactly > > The URI in the provider's console must match what Scalekit shows character-for-character, including trailing slashes. A mismatch causes the OAuth flow to fail with a `redirect_uri_mismatch` error. 4. ### Enter your credentials Back in the Scalekit dashboard, enter the **Client ID** and **Client Secret** from the provider. 5. ### Configure scopes Select the scopes your agent needs. Scopes define what your agent can do on the user's behalf: for example, `user:email` or `repo` for GitHub. > note: Scopes apply to all connected accounts > > The scopes you set here apply to every connected account that uses this connection. If you need different scopes for different user groups, create separate connections for each group. > tip: Add the offline-access scope > > If the provider has an offline-access scope, such as `offline_access`, select it. Providers issue a refresh token only when the connection asks for offline access. Without one, each connected account becomes [`EXPIRED`](https://docs.scalekit.com/agentkit/troubleshooting/#expired) when its first access token does. 6. ### Save the connection Click **Save**. The connection is now active and ready for connected accounts to be created against it. > tip: Use Scalekit credentials to get started faster > > For many OAuth connectors, the connection form offers a **Use Scalekit credentials** option. This lets you skip OAuth app registration, and it works in production too. Before real users connect, switch to your own credentials, so the consent screen shows your app's name and your app gets its own rate limits. See [Bring your own credentials](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/). ## Set up a non-OAuth connection For connectors that use API keys, basic auth, key pairs, or similar, the connection form asks for very little. In many cases, you only need to give the connection a name. The user provides their own credentials (their API key, account details, or private key) when they create a connected account. Scalekit collects those credentials through the connected account form and stores them securely. 1. Go to **AgentKit** > **Connections** and click **Add connection** 2. Select the connector 3. Enter a **Connection name**: this identifies the connection in the dashboard and in your code 4. Click **Save** When a connected account is created for this connection, Scalekit presents the user with a form that collects the credentials their specific account requires. ## Set up a connection with the API To create connections from code, for example one per environment or from your own setup script, use the [Connections API](https://docs.scalekit.com/agentkit/reference/connections/). This follows the same steps as the dashboard for an OAuth connector: create the connection, register its redirect URI with the provider, then add your OAuth app's credentials and scopes. The examples create a Gmail connection named `gmail-work`. They read your Scalekit API credentials and the Client ID and Client Secret of the OAuth app you registered with Google from a `.env` file: ```bash title=".env" SCALEKIT_ENVIRONMENT_URL= SCALEKIT_CLIENT_ID= SCALEKIT_CLIENT_SECRET= GMAIL_CLIENT_ID= GMAIL_CLIENT_SECRET= ``` Install the SDK with `pip install scalekit-sdk-python python-dotenv` or `npm install @scalekit-sdk/node dotenv`. Each step below continues the same script. Save the Node.js code in a `.mts` file, such as `connection.mts`, and run it with `npx tsx connection.mts`. The cURL tabs use `jq`. 1. ### Create the connection Pass the connector's identifier, such as `GMAIL`, and a connection name. The response includes the connection's redirect URI. **Python** ```python import os from dotenv import load_dotenv from scalekit import ScalekitClient from scalekit.actions.connections import OAuthSettings load_dotenv() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions connection = actions.create_connection(provider="GMAIL", connection_name="gmail-work") print("Redirect URI:", connection.oauth.redirect_uri) ``` **Node.js** ```typescript import 'dotenv/config'; import { ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const actions = scalekit.actions; const connection = await actions.createConnection({ provider: 'GMAIL', connectionName: 'gmail-work', }); if (connection.settings.case === 'oauthConfig') { console.log('Redirect URI:', connection.settings.value.redirectUri); } ``` **cURL** ```bash set -a; source .env; set +a ACCESS_TOKEN=$(curl -s "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \ -d grant_type=client_credentials \ -d client_id="$SCALEKIT_CLIENT_ID" \ -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token) curl -s -X POST "$SCALEKIT_ENVIRONMENT_URL/api/v1/agentkit/connection" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"provider": "GMAIL", "connection_name": "gmail-work"}' \ | jq -r .connection.oauth_config.redirect_uri ``` To find a connector's identifier, call [List connectors](https://docs.scalekit.com/agentkit/bring-your-own-connector/create-connector/#list-connectors) and read `identifier`. 2. ### Register your OAuth app with the provider In the provider's developer console, add the redirect URI from step 1 to your OAuth app's authorized redirect URIs, exactly as returned. Put the app's Client ID and Client Secret in `.env`. 3. ### Add the credentials and scopes Update the connection with your OAuth app's credentials and the scopes your agent needs. Only the fields you send change. **Python** ```python actions.update_connection( "gmail-work", oauth=OAuthSettings( client_id=os.environ["GMAIL_CLIENT_ID"], client_secret=os.environ["GMAIL_CLIENT_SECRET"], scopes=["https://www.googleapis.com/auth/gmail.readonly"], ), ) ``` **Node.js** ```typescript await actions.updateConnection({ connectionName: 'gmail-work', oauth: { clientId: process.env.GMAIL_CLIENT_ID!, clientSecret: process.env.GMAIL_CLIENT_SECRET!, scopes: ['https://www.googleapis.com/auth/gmail.readonly'], }, }); ``` **cURL** ```bash CONNECTION_ID=$(curl -s "$SCALEKIT_ENVIRONMENT_URL/api/v1/agentkit/connection/gmail-work" \ -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r .connection.id) curl -s -X PATCH "$SCALEKIT_ENVIRONMENT_URL/api/v1/agentkit/connection/$CONNECTION_ID" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"oauth_config\": {\"client_id\": \"$GMAIL_CLIENT_ID\", \"client_secret\": \"$GMAIL_CLIENT_SECRET\", \"scopes\": [\"https://www.googleapis.com/auth/gmail.readonly\"]}}" \ | jq -r .connection.connection_name ``` 4. ### Check it worked Get the connection and confirm it has your Client ID and scopes. The client secret comes back masked. **Python** ```python connection = actions.get_connection("gmail-work") print(connection.connection_name, connection.oauth.client_id, connection.oauth.scopes) ``` **Node.js** ```typescript const saved = await actions.getConnection('gmail-work'); if (saved.settings.case === 'oauthConfig') { console.log(saved.connectionName, saved.settings.value.clientId, saved.settings.value.scopes); } ``` **cURL** ```bash curl -s "$SCALEKIT_ENVIRONMENT_URL/api/v1/agentkit/connection/gmail-work" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ | jq '{connection_name: .connection.connection_name, client_id: .connection.oauth_config.client_id, scopes: .connection.oauth_config.scopes}' ``` Expected output: `gmail-work`, your Client ID and `https://www.googleapis.com/auth/gmail.readonly`. To rename, list or delete connections, see the [Connections API reference](https://docs.scalekit.com/agentkit/reference/connections/). ## Create multiple connections for the same connector You can create more than one connection for the same connector. This is useful when: - Different groups of users need different scopes - You want to maintain separate OAuth apps for staging and production - You're integrating with multiple instances of the same service (for example, two different Salesforce orgs) Each connection has its own name, which you use to identify it in API calls and in the dashboard. ## Use the connection in code Your code refers to a connection by its **connection name**, the name you gave it in the form. New environments include a GitHub connection named `github-connect`. Pass it, together with the user's [identifier](https://docs.scalekit.com/agentkit/concepts/#choose-an-identifier), when you create a connected account, request an authorization link or execute a tool: | Call | Python | Node.js | REST API | | --- | --- | --- | --- | | Create a connected account or an authorization link | `connection_name="github-connect"` | `connectionName: 'github-connect'` | `connector` | | Execute a tool | `connection_name="github-connect"` | `connector: 'github-connect'` | `connector` | Use the name, not the connection's `conn_...` ID. The REST field is called `connector`, but it also takes the connection name. To see these calls in full, follow [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/), or open [Create a connected account](https://docs.scalekit.com/agentkit/reference/connected-accounts/create-a-connected-account/) and [Get an authorization link](https://docs.scalekit.com/agentkit/reference/authorization/get-an-authorization-link/) in the API reference. ## Common scenarios If a user can't finish connecting, find the error on [Troubleshoot connection and OAuth errors](https://docs.scalekit.com/agentkit/troubleshooting/): - [`failed_to_exchange_token`](https://docs.scalekit.com/agentkit/troubleshooting/#authorization-failed-failed_to_exchange_token): the page says **Authorization failed** after the user approved access. - [`session_not_found`](https://docs.scalekit.com/agentkit/troubleshooting/#session-expired-or-not-available-session_not_found): the page says **Session expired or not available**. - [`redirect_uri_mismatch`](https://docs.scalekit.com/agentkit/troubleshooting/#redirect_uri_mismatch): the provider rejects the redirect URI. - [It works for your account but fails for your customers](https://docs.scalekit.com/agentkit/troubleshooting/#it-works-for-your-account-but-fails-for-your-customers): the provider's OAuth app is still in development or testing. ## Next - [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/): Create a connected account and send the user an authorization link. - [Troubleshoot connection and OAuth errors](https://docs.scalekit.com/agentkit/troubleshooting/): Find the error your user saw and how to fix it. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Create a connection](https://docs.scalekit.com/agentkit/reference/connections/create-a-connection.md) - `GET` [Get a connection](https://docs.scalekit.com/agentkit/reference/connections/get-a-connection.md) - `PATCH` [Update a connection](https://docs.scalekit.com/agentkit/reference/connections/update-a-connection.md) --- Source: https://docs.scalekit.com/agentkit/tools/authorize.md # Authorize a user Generate an authorization link, send it to your user, and confirm their connected account is active before your agent executes tools. Once a connection is configured, your users need to grant your agent access to their account. This happens once per user per connection. Scalekit stores their tokens and keeps them fresh automatically. The flow is: 1. Create a connected account for the user 2. Generate an authorization link and send it to the user 3. The user completes the OAuth consent screen 4. The connected account becomes `ACTIVE`. Your agent can now execute tools. ## Create a connected account and generate a link These samples use `actions`, set up as in the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/). The first block shows the setup; later blocks reuse it. **Python** ```python import os from dotenv import load_dotenv from scalekit import ScalekitClient from scalekit.common.exceptions import ScalekitServerException load_dotenv() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions try: # Create or retrieve the connected account for this user response = actions.get_or_create_connected_account( connection_name="github-connect", identifier="user_123" # your app's unique user ID ) connected_account = response.connected_account # Generate the authorization link if the account is not yet active if connected_account.status != "ACTIVE": link_response = actions.get_authorization_link( connection_name="github-connect", identifier="user_123" ) auth_url = link_response.link # Redirect or send auth_url to the user except ScalekitServerException as e: # For example, the connection name doesn't match the dashboard print("Scalekit error:", e.error_code, e.message) ``` **Node.js** ```typescript import 'dotenv/config'; import { ConnectorStatus, ScalekitClient, ScalekitServerException } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const actions = scalekit.actions; try { // Create or retrieve the connected account for this user const response = await actions.getOrCreateConnectedAccount({ connectionName: 'github-connect', identifier: 'user_123', // your app's unique user ID }); const connectedAccount = response.connectedAccount; // Generate the authorization link if the account is not yet active if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { const linkResponse = await actions.getAuthorizationLink({ connectionName: 'github-connect', identifier: 'user_123', }); const authUrl = linkResponse.link; // Redirect or send authUrl to the user } } catch (err) { if (!(err instanceof ScalekitServerException)) throw err; // For example, the connection name doesn't match the dashboard console.error('Scalekit error:', err.errorCode, err.message); } ``` > note: Custom user verifier needs user_verify_url > > If the environment uses the **Custom user verifier** mode, also pass `user_verify_url` and `state` when you request the link. Without `user_verify_url`, the request fails. See [Verify users](https://docs.scalekit.com/agentkit/user-verification/#generate-the-authorization-link). By default, the link expires five minutes after you create it. The response's `expiry` field gives the exact time, so create the link when you're ready to send it, and create a new one if it expires. ## Send the link to the user How you deliver the link depends on your application: - **Web app:** redirect the user to `auth_url` directly if they're in an active browser session - **Email or notification:** send the link when the user isn't actively in your app, or when connecting at their own pace is acceptable - **In-app prompt:** show a button ("Connect GitHub") when you want to prompt connection at a specific moment in the user's workflow Once the user opens the link and approves the OAuth consent screen, Scalekit exchanges the authorization code for tokens and marks the connected account `ACTIVE`. You do not need to handle the OAuth callback yourself. To learn when the user has finished, [listen for account events](https://docs.scalekit.com/agentkit/account-events/): a `connected_account.status_updated` event with status `ACTIVE` arrives when the account activates. Or check the status again with `get_connected_account_details` in Python or `getConnectedAccount` in Node.js. > note: Production: add user verification > > New environments use the **None** verification mode, so any user who completes the OAuth flow activates the connected account. Before production, switch to **Custom user verifier** so your app confirms the authorizing user is the one it intended to connect. See [Verify user identity](https://docs.scalekit.com/agentkit/user-verification/). ## Check status and re-authorize Check the connected account status before executing tools. Tokens can expire or be revoked, so generate a new authorization link using the same flow when that happens. **Python** ```python response = actions.get_or_create_connected_account( connection_name="github-connect", identifier="user_123" ) connected_account = response.connected_account # ACTIVE: ready for tool calls # PENDING_AUTH: the user hasn't completed authorization yet # PENDING_VERIFICATION: authorized, but user verification is still pending # EXPIRED: tokens expired or were revoked; the user must authorize again # DISCONNECTED: the account was disconnected if connected_account.status != "ACTIVE": link_response = actions.get_authorization_link( connection_name="github-connect", identifier="user_123" ) # Redirect or send link_response.link to the user ``` **Node.js** ```typescript import { ConnectorStatus } from '@scalekit-sdk/node'; const response = await actions.getOrCreateConnectedAccount({ connectionName: 'github-connect', identifier: 'user_123', }); const connectedAccount = response.connectedAccount; // ACTIVE: ready for tool calls // PENDING_AUTH: the user hasn't completed authorization yet // PENDING_VERIFICATION: authorized, but user verification is still pending // EXPIRED: tokens expired or were revoked; the user must authorize again // DISCONNECTED: the account was disconnected if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { const linkResponse = await actions.getAuthorizationLink({ connectionName: 'github-connect', identifier: 'user_123', }); // Redirect or send linkResponse.link to the user } ``` ## Common problems ### `user_verify_url is required when user_verify_mode is b2b` The environment uses the **Custom user verifier** mode, and the link request didn't include `user_verify_url`. The request fails with a `400`. Pass `user_verify_url` and `state` as shown in [Verify users](https://docs.scalekit.com/agentkit/user-verification/#generate-the-authorization-link), or switch to **Scalekit users only** for internal testing. ### The account stays `PENDING_AUTH` The user didn't finish authorizing, or the link expired before they opened it. Create a new link and send it again. ### Still stuck? Find the error message or the account status on [Troubleshoot connection and OAuth errors](https://docs.scalekit.com/agentkit/troubleshooting/). It covers each connected-account status, the errors on Scalekit's connection page, OAuth app misconfiguration and provider-specific errors. ## Next - [Verify users](https://docs.scalekit.com/agentkit/user-verification/): Confirm that the person who authorized is the user you meant. - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and ask users to reconnect. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Create a connected account](https://docs.scalekit.com/agentkit/reference/connected-accounts/create-a-connected-account.md) - `POST` [Get an authorization link](https://docs.scalekit.com/agentkit/reference/authorization/get-an-authorization-link.md) --- Source: https://docs.scalekit.com/agentkit/hosted-connect-widget.md # Hosted Connect Widget What your users see when they open an AgentKit authorization link: OAuth consent or a credential form, scope choices, account states and errors. The Hosted Connect Widget is the page your users land on when they open an [authorization link](https://docs.scalekit.com/agentkit/tools/authorize/). Scalekit hosts it, so the user's OAuth consent, password or API key goes to Scalekit, not through your app. It adapts to the connection: OAuth connectors send the user to the app's consent screen, and API key, basic auth and similar connectors show a form for the credentials. This page explains what users see and do in the widget, and what you control. The widget opens as a full page in the user's browser, not as a component you embed in your app. To make it look like your product, see [Brand the Hosted Connect Widget](https://docs.scalekit.com/agentkit/advanced/branding/). ## What the user sees The widget shows your logo and a headline that names your application and the app, such as "Acme wants to connect to your Gmail". Under it are a short overview of the app, then either a **Connect** button or a form, and links to your terms of service and privacy policy. > Image: Hosted Connect Widget for an OAuth connector: Acme Inc. wants to connect to your Notion, with a short description of Notion and a Connect button What happens next depends on the connector's auth type: | Auth type | What the user does | When the account becomes `ACTIVE` | | --- | --- | --- | | OAuth | Selects **Connect**, grants access on the app's consent screen, and returns to the widget | When the app returns a token | | API key, basic auth, bearer token and similar | Fills in the form and selects **Connect** | As soon as the form is submitted. Scalekit doesn't test the credential, so a wrong value shows up later as a failed tool call | | Client credentials or Google domain-wide delegation | Fills in the form and selects **Connect** | After Scalekit gets a token from the app with the submitted values | The form fields come from the connector: their labels, hints and which ones are required. Secret fields, such as an API key or password, are masked as the user types. A required field can't be left empty. > Image: Hosted Connect Widget for an API key connector: Acme Inc. wants to connect to your Clari Copilot, with required API Key and API Password fields, each with a hint about where to find it When the user is done, the widget shows **Account connected successfully** and the headline confirms the connection. The user stays on the widget page. Your app learns the result by [checking the account's status](https://docs.scalekit.com/agentkit/connected-accounts/) or from the [`connected_account.status_updated` event](https://docs.scalekit.com/agentkit/account-events/). ## Let users choose scopes For OAuth connectors, you can let users decide which of the connection's scopes to grant. Turn on **Allow users to choose scopes** on the connection's configuration, and mark the scopes your agent can't work without as **Required**. The widget then lists the scopes under "Choose what Acme can access in your Gmail". Every scope starts selected. Users can clear the optional ones; required scopes stay selected. If a user clears everything, the widget warns that the app may reject the connection or apply its own defaults. > Image: Hosted Connect Widget scope list: Choose what Acme Inc. can access in your Gmail, with two required scopes shown checked and disabled, and three optional scopes checked Scalekit grants only the scopes the user kept, plus the required ones. Your agent's tool calls that need a scope the user cleared fail with the app's permission error, so mark a scope required when a tool you rely on needs it. ## Account states The widget shows the connected account's current state each time the user opens a link: | Account status | What the widget shows | | --- | --- | | Not yet connected | The **Connect** button or the credential form | | `ACTIVE` | That the account is connected, with a **Disconnect** button. The user doesn't need to connect again | | `EXPIRED` | **Re-Authentication Required** and a **Reconnect** button | | `PENDING_VERIFICATION` | **Verification pending**: the user authorized, and your app still has to [verify the user](https://docs.scalekit.com/agentkit/user-verification/) | ## How credentials are stored Everything a user submits in the widget, whether OAuth tokens, an API key or a username and password, is stored the same way: encrypted with your environment's key before it's written. See [How credentials are stored](https://docs.scalekit.com/agentkit/security/#how-credentials-are-stored). By default, your app doesn't receive these values back. Your agent uses them through [tool calls](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/) and the [API proxy](https://docs.scalekit.com/agentkit/tools/custom-tools/), and Scalekit adds the credential to each request. If your app needs the raw values, see [Get a connected account's credentials](https://docs.scalekit.com/agentkit/reference/connected-accounts/get-connected-account-credentials/). ## Customize the widget | What you change | Where | | --- | --- | | Your name, logo, colors, font and legal links | **Customize** > **Branding**. See [Brand the Hosted Connect Widget](https://docs.scalekit.com/agentkit/advanced/branding/) | | The app name, overview and description for one connection | **Connections** > your connection > **Widgets** | | The domain the widget is served from | [Custom domain](https://docs.scalekit.com/agentkit/advanced/custom-domain/) | | Which scopes users can choose | **Allow users to choose scopes** on the connection, as above | ## Common problems ### This link has expired The user opened the authorization link after it expired. Links last five minutes by default. [Create a new link](https://docs.scalekit.com/agentkit/tools/authorize/) and send it when the user is ready to connect. ### This connected account no longer exists The connected account was deleted after the link was created. Create the connected account again, then create a new link. ### Authorization failed The app didn't issue a token after the user approved access. The usual causes are on the user's side at the app: an administrator restricted the app with an access policy, the app isn't approved in the user's organization, or the user's account lacks permission to authorize it. The user asks their administrator to allow the app, then opens a new link. Expand the banner to see the error, its description and the auth request ID. Include them when you contact support. ### Session expired or not available The user opened the link with **Scalekit users only** user verification, and isn't signed in to Scalekit in that browser. In production, use **Custom user verifier** instead. See [Verify users](https://docs.scalekit.com/agentkit/user-verification/). ### The permissions available for this connection have changed The connection's scopes changed while the user had the widget open. The user reloads the page and chooses again. ## Next - [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/): Create the authorization link that opens the widget. - [Brand the Hosted Connect Widget](https://docs.scalekit.com/agentkit/advanced/branding/): Use your name, logo, colors and domain. --- Source: https://docs.scalekit.com/agentkit/user-verification.md # Verify users Confirm that the person who authorized an AgentKit connection is the user your app meant, with a verification callback in production or Scalekit users only. User verification applies to OAuth-based connectors only. For API key, basic auth, and key pair connectors, the user provides credentials directly. No OAuth flow, no verification step needed. For OAuth connectors, **user verification** confirms that the user who completed the OAuth consent is the same user your app intended to connect, before Scalekit activates the connected account. It stops an authorization link from activating the wrong account, for example when a link is forwarded or phished. Choose a mode in **AgentKit** > **Settings** > **User Verification**: - **Custom user verifier** (recommended): your server confirms that the authorizing user matches the user your app intended to connect. Use it in production. - **Scalekit users only**: Scalekit checks that the authorizing user is invited to your Scalekit workspace and signed in to the dashboard. No code required. Use it for development and testing, when every user is on your team. - **None**: no verification. The connected account activates as soon as OAuth completes, so anyone who opens an authorization link can activate it. New environments start with **None**, because your agent may run somewhere that can't confirm which user is signed in. Switch to **Custom user verifier** before you onboard real users. > note: Scalekit users only is for testing > > In this mode, the user authorizing the connection must already be signed in to the Scalekit dashboard. No verify route or API calls are needed in your code. Switch to **Custom user verifier** before onboarding real users. > Image: User Verification settings in the Scalekit dashboard, with the Custom user verifier, Scalekit users only and None modes With **Custom user verifier**, your application implements the verify step. End users never interact with Scalekit directly. When the user finishes OAuth, Scalekit redirects to your verify URL with `auth_request_id` and `state` params. Your route reads the user from your session, calls Scalekit's verify API with the `auth_request_id` and the original `identifier`, and if they match, the connected account activates. ** Review the verification sequence** ```d2 title="Connected account user verification sequence: authorization link, OAuth consent, authorization code, and redirect to your app for verification" title: "Connected account user verification" { near: top-center shape: text style.font-size: 18 } shape: sequence_diagram Your app Scalekit Provider End user Your app -> Scalekit: POST authorization link\n(identifier, user_verify_url, state) Scalekit -> Your app: Authorization link URL Your app -> End user: Deliver link\n(email, in-app, …) End user -> Scalekit: Open authorization link Scalekit -> Provider: OAuth consent screen Provider -> Scalekit: Authorization code Scalekit -> Scalekit: Store tokens\n(pending verification) Scalekit -> End user: Redirect to user_verify_url\n(auth_request_id, state) End user -> Your app: GET /user/verify\n(?auth_request_id, state) Your app -> Your app: Validate state,\nread user from session Your app -> Scalekit: POST verify\n(auth_request_id, identifier) Scalekit -> Scalekit: Match identifier,\nactivate connected account Scalekit -> Your app: post_user_verify_redirect_url Your app -> End user: Redirect to your app ``` ## Implement verification in your app If you haven't installed the SDK yet, see the [quickstart](https://docs.scalekit.com/agentkit/quickstart/). ### Generate the authorization link Pass these fields when creating the authorization link: | Field | Description | |---|---| | `identifier` | **Required.** Your user's ID in your system, such as `user_123`. Use a stable internal ID, not an email address: a user can change their email, and an old address can be reassigned to someone else, so the identifier would no longer point to the same person. An email is also easy to guess, which makes misuse easier. Scalekit stores it and checks that the verify call sends the same value. | | `user_verify_url` | **Required.** Your callback URL; Scalekit redirects the user here after OAuth completes. | | `state` | **Recommended.** A random value to prevent CSRF. | > note: How to use state > > Generate a cryptographically random value per flow, store it in a secure HTTP-only cookie, and validate it against the `state` query param on callback. Discard the request if they don't match; this prevents an attacker from sending crafted verify URLs to your users. **Python** ```python import secrets # Generate a state value to prevent CSRF state = secrets.token_urlsafe(32) # Store state in a secure, HTTP-only cookie to validate on callback response = scalekit_client.actions.get_authorization_link( connection_name=connector, identifier=user_id, user_verify_url="https://app.yourapp.com/user/verify", state=state, ) ``` **Node.js** ```typescript import crypto from 'node:crypto'; // Generate a state value to prevent CSRF const state = crypto.randomUUID(); // Store state in a secure, HTTP-only cookie to validate on callback const { link } = await scalekit.actions.getAuthorizationLink({ identifier: userId, connectionName: connector, userVerifyUrl: 'https://app.yourapp.com/user/verify', state, }); ``` ### Handle the verification callback After OAuth completes, Scalekit redirects to your `user_verify_url`: ```http GET https://app.yourapp.com/user/verify?auth_request_id=req_xyz&state= ``` Validate `state` against your cookie, then call Scalekit's verify endpoint server-side. > caution: Never trust query params for identity > > Read the user's identity from your own session, not from the URL. Use `state` for session correlation only. **Python** ```python # 1. Validate state from query param matches state in cookie # 2. Read user identity from your session, not from the URL response = scalekit_client.actions.verify_connected_account_user( auth_request_id=auth_request_id, identifier=user_id, # must match what was stored at link creation ) # On success: redirect to response.post_user_verify_redirect_url ``` **Node.js** ```typescript // 1. Validate state from query param matches state in cookie // 2. Read user identity from your session, not from the URL const { postUserVerifyRedirectUrl } = await scalekit.actions.verifyConnectedAccountUser({ authRequestId: auth_request_id, identifier: userId, // must match what was stored at link creation }); // On success: redirect to postUserVerifyRedirectUrl ``` On success, the connected account is activated. Redirect the user using `post_user_verify_redirect_url`. ## Common scenarios **Why does creating the authorization link fail with `user_verify_url is required when user_verify_mode is b2b`?** The environment is set to **Custom user verifier**, and the request for the authorization link didn't include `user_verify_url`. The call fails with a `400` before any link is created. Pass `user_verify_url` and `state` when you [generate the authorization link](#generate-the-authorization-link), or, for internal testing, switch the mode to **Scalekit users only**. **Why does authorization fail when no verification redirect URL is configured?** The authorization flow fails with a `failed_to_exchange_token` error (`user_verify_url not configured for verification redirect`) when the environment is set to **Custom user verifier**, but the flow started without a `user_verify_url`. Scalekit completes the OAuth exchange but has nowhere to redirect the user for verification. Resolve it in one of two ways: - **In production**, pass `user_verify_url` when you generate the authorization link, and implement the [verification callback](#handle-the-verification-callback) at that URL. - **In development or internal testing**, set the mode to **Scalekit users only** in **AgentKit** > **Settings** > **User Verification**. This mode needs no `user_verify_url` and no verify route, as long as every authorizing user is signed in to your Scalekit dashboard. **Why does the connection page say "Session expired or not available"?** The environment uses **Scalekit users only**, and the person authorizing isn't signed in to the Scalekit dashboard. Sign in to the dashboard in the same browser and start again, or switch to **Custom user verifier** for users who aren't on your team. See [`session_not_found`](https://docs.scalekit.com/agentkit/troubleshooting/#session-expired-or-not-available-session_not_found). ## Next - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and ask users to reconnect. - [Troubleshoot connection and OAuth errors](https://docs.scalekit.com/agentkit/troubleshooting/): Find the error your user saw and how to fix it. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Get an authorization link](https://docs.scalekit.com/agentkit/reference/authorization/get-an-authorization-link.md) - `POST` [Verify the user](https://docs.scalekit.com/agentkit/reference/authorization/verify-the-user.md) --- Source: https://docs.scalekit.com/agentkit/connected-accounts.md # Manage connected accounts Manage AgentKit connected accounts: check an account's status, detect when a user must reconnect, list and delete accounts, and request more OAuth scopes. A **connected account** is the per-user record that holds a user's credentials and tracks their authorization state for a specific connection. Your code creates it, with `get_or_create_connected_account` or when it requests an authorization link for the user. The user activates it by authorizing. ## Account states | State | Meaning | |---|---| | `ACTIVE` | Last known good: tool calls should work. A call can still move it to `EXPIRED` if the user revoked access. | | `EXPIRED` | Tokens expired or were revoked and couldn't be refreshed; the user must authorize again | | `PENDING_AUTH` | The user hasn't finished authorizing, or is authorizing again | | `PENDING_VERIFICATION` | OAuth complete; user identity verification still required before activation | | `DISCONNECTED` | Account was manually disconnected | See [Troubleshoot connection and OAuth errors](https://docs.scalekit.com/agentkit/troubleshooting/#connected-account-status) for what to do in each state. ## Check account status Use `get_or_create_connected_account` as the safe default when a user may be connecting for the first time. When the account already exists and you only need its status, use `get_connected_account_details` in Python, which returns the account without its credentials. In Node.js, use `getConnectedAccount`. **Python** ```python response = actions.get_or_create_connected_account( connection_name="github-connect", identifier="user_123" ) connected_account = response.connected_account print(f"Status: {connected_account.status}") ``` **Node.js** ```typescript const response = await actions.getOrCreateConnectedAccount({ connectionName: 'github-connect', identifier: 'user_123', }); const connectedAccount = response.connectedAccount; console.log('Status:', connectedAccount?.status); ``` ## Handle inactive accounts When a connected account isn't `ACTIVE`, generate a new authorization link and send it to the user. The link opens the [Hosted Connect Widget](https://docs.scalekit.com/agentkit/hosted-connect-widget/), a Scalekit-hosted page that adapts to the connection's auth type: - **OAuth connectors**: presents the provider's OAuth consent screen - **API key, basic auth, or other connectors**: presents a form to collect the required credentials Your code is the same regardless of connector type. Scalekit determines the right flow based on the connection configuration. **Python** ```python if connected_account.status != "ACTIVE": link_response = actions.get_authorization_link( connection_name="github-connect", identifier="user_123" ) # Redirect or send link_response.link to the user ``` **Node.js** ```typescript import { ConnectorStatus } from '@scalekit-sdk/node'; if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { const linkResponse = await actions.getAuthorizationLink({ connectionName: 'github-connect', identifier: 'user_123', }); // Redirect or send linkResponse.link to the user } ``` > tip: Customize the widget > > By default, the Hosted Connect Widget is served from your Scalekit environment domain. Use a custom domain to serve it from your own domain. See [Custom domain](https://docs.scalekit.com/agentkit/advanced/custom-domain/). ## Detect when a user must authorize again A connected account can leave the `ACTIVE` state on its own, with no action from you or the user. When that happens, the next tool call fails until the user authorizes again. To catch it early, [listen for the `connected_account.status_updated` webhook](https://docs.scalekit.com/agentkit/account-events/) instead of waiting for a failed call. ### Common causes OAuth connected accounts most often move to `EXPIRED` because: - **The provider revoked the refresh token.** A password change, an admin-initiated token revocation, or a provider security policy invalidates the refresh token, so Scalekit can no longer obtain new access tokens. - **The refresh token expired.** Providers cap refresh-token lifetimes (for example, 30 or 180 days), and the expiry is rarely surfaced in advance. - **No refresh token was issued.** When the connection's scopes don't request offline access, the provider returns only a short-lived access token and no refresh token to renew it. - **The provider hit a per-user token limit.** Some providers keep only a fixed number of refresh tokens per user and app, and silently drop the oldest ones when a user reconnects repeatedly. API key, bearer token and basic auth accounts have no token to refresh. They move to `EXPIRED` when the app rejects the stored credential with a 401 on a tool call, for example after the key was deleted or rotated at the provider. In every case, the user authorizes again; for API key and basic auth connections, the authorization link collects the new credential. When no refresh token was issued, first add the provider's offline-access scope to the connection, or the account expires again soon after. Offline scopes don't raise a provider's per-user token limit: to stay under it, send a new authorization link only when the account isn't `ACTIVE`. ### Subscribe to status changes Scalekit sends a `connected_account.status_updated` webhook when an account's status changes, with the old and new status. When `status` becomes `EXPIRED` or `DISCONNECTED`, [create a new authorization link](#handle-inactive-accounts) and ask the user to reconnect. [Listen for account events](https://docs.scalekit.com/agentkit/account-events/) shows how to add an endpoint, verify the signature, and handle each event. ## List connected accounts **Python** ```python list_response = actions.list_connected_accounts( connection_name="github-connect", ) for account in list_response.connected_accounts: print(account.identifier, account.status) ``` **Node.js** ```typescript import { ConnectorStatus } from '@scalekit-sdk/node'; const listResponse = await actions.listConnectedAccounts({ connectionName: 'github-connect', }); for (const account of listResponse.connectedAccounts) { console.log(account.id, account.identifier, ConnectorStatus[account.status]); } ``` ## Delete a connected account Deleting a connected account removes the user's credentials and authorization state. The user must authorize again to reconnect. **Python** ```python actions.delete_connected_account( connection_name="github-connect", identifier="user_123", ) ``` **Node.js** ```typescript await actions.deleteConnectedAccount({ connectionName: 'github-connect', identifier: 'user_123', }); ``` ## Update OAuth scopes Scopes apply to OAuth connectors only. For non-OAuth connectors (API key, basic auth, and similar), generate a new authorization link and the Hosted Connect Widget collects the new credentials. To request additional OAuth scopes from an existing connected account: 1. Update the connection's scopes in **AgentKit** > **Connections** > **Edit**. 2. Generate a new authorization link for the user. 3. The user completes the OAuth consent screen, approving the updated scopes. 4. Scalekit updates the connected account with the new token set. ## Next - [Listen for account events](https://docs.scalekit.com/agentkit/account-events/): Get a webhook when an account expires or is disconnected. - [Use built-in tools](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/): Call a tool as the user with execute_tool. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Create a connected account](https://docs.scalekit.com/agentkit/reference/connected-accounts/create-a-connected-account.md) - `GET` [List connected accounts](https://docs.scalekit.com/agentkit/reference/connected-accounts/list-connected-accounts.md) - `POST` [Delete a connected account](https://docs.scalekit.com/agentkit/reference/connected-accounts/delete-a-connected-account.md) - `POST` [Get an authorization link](https://docs.scalekit.com/agentkit/reference/authorization/get-an-authorization-link.md) --- Source: https://docs.scalekit.com/agentkit/account-events.md # Listen for account events Get a webhook when an AgentKit connected account changes. Verify the signature in Python or Node.js, and ask users to reconnect when an account expires. Scalekit sends a webhook to your server when a connected account changes: when a user finishes authorizing, when a token can't be refreshed, or when an account expires or is disconnected. Listen for these events to ask users to reconnect before their next tool call fails, instead of polling every account's status. ## Before you start - A server with a public HTTPS URL that can receive `POST` requests. While you develop, a tunnel to your local server works. - The Scalekit SDK installed, as in the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/). It verifies webhook signatures. - The **Admin** or **Developer** role, to create an endpoint and read its signing secret. See [Team members and roles](https://docs.scalekit.com/agentkit/team-members/). ## Events | Event | Sent when | | --- | --- | | `connected_account.created` | A connected account is created | | `connected_account.updated` | A connected account's details are updated | | `connected_account.deleted` | A connected account is deleted | | `connected_account.magic_link_generated` | An authorization link is created for the account | | `connected_account.oauth_tokens_fetched` | The app returned tokens after the user approved access | | `connected_account.oauth_succeeded` | The user finished authorizing, including user verification when it's on | | `connected_account.token_refresh_succeeded` | Scalekit refreshed the account's access token | | `connected_account.token_refresh_failed` | Scalekit couldn't refresh the access token | | `connected_account.status_updated` | The account's status changed, for example from `ACTIVE` to `EXPIRED` | To know when a user needs to act, `connected_account.status_updated` is the one to handle. It's sent only when the status actually changes, not when an account is created, and it carries the old and new status. `connected_account.token_refresh_failed` arrives before the status changes, so its `status` often still reads `ACTIVE`, and it doesn't say why the refresh failed. Use it as an early warning, and wait for `status_updated` before you ask the user to reconnect. ## Payloads Every event has the same envelope. The account's details are in `data`: ```json title="connected_account.status_updated" { "spec_version": "1", "id": "evt_101652975398683158", "type": "connected_account.status_updated", "occurred_at": "2026-09-26T06:31:34.895815554Z", "environment_id": "env_88640229614813449", "object": "ConnectedAccount", "data": { "id": "ca_133400349586228019", "identifier": "user_123", "connection_id": "conn_133400101014995480", "connection_name": "github-connect", "provider": "GITHUB", "authorization_type": "OAUTH", "status": "EXPIRED", "old_status": "ACTIVE" } } ``` | Field | Meaning | | --- | --- | | `id` | The event's ID. The same event delivered twice has the same ID | | `type` | The event name from the table above | | `occurred_at` | When the change happened | | `data.id` | The connected account's ID | | `data.identifier` | Your app's ID for the user | | `data.connection_name` | The connection, as named in **AgentKit** > **Connections**. It can be missing from `status_updated`, so read it as optional and fall back to `data.connection_id` | | `data.status` | The status after the change: `ACTIVE`, `PENDING_AUTH`, `PENDING_VERIFICATION`, `EXPIRED` or `DISCONNECTED` | | `data.old_status` | The status before the change. Only in `status_updated` | Events other than `status_updated` have no `old_status`. They also include `token_expires_at` and `last_used_at`, which are empty strings when Scalekit has no value. In webhooks, `status` is a string, such as `ACTIVE`, in both Python and Node.js. For every event's full schema, see [Connected account events](https://docs.scalekit.com/agentkit/reference/events/) in the REST reference. 1. ## Add a webhook endpoint In the Scalekit dashboard, select the environment, then open **Developers** > **Webhooks** and select **Add Webhook**. Enter a **Display Name** and your **Endpoint URL**, choose the events under **Choose events to subscribe**, and save. Each environment has its own endpoints. Add one in Production too when you go live. 2. ## Copy the signing secret Open the endpoint and select **Show** under **Signing secret**. It starts with `whsec_`. Store it with your other server secrets, for example as `SCALEKIT_WEBHOOK_SECRET`, and never expose it to a browser. 3. ## Verify each request and handle the event Scalekit signs every request with the secret. Verify the signature against the raw request body before you trust the payload, so a forged request can't prompt the wrong user to reconnect. The SDK checks the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers, and rejects requests older than five minutes. **Python** ```python title="webhooks.py" import json import os from fastapi import FastAPI, HTTPException, Request from scalekit import ScalekitClient scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) app = FastAPI() seen_event_ids = set() # use your database in production @app.post("/webhooks/scalekit") async def scalekit_webhook(request: Request): body = await request.body() headers = {k.lower(): v for k, v in request.headers.items()} try: scalekit_client.verify_webhook_payload( os.environ["SCALEKIT_WEBHOOK_SECRET"], headers, body ) except Exception: raise HTTPException(status_code=400, detail="invalid signature") event = json.loads(body) if event["id"] in seen_event_ids: return {"ok": True} seen_event_ids.add(event["id"]) data = event["data"] if event["type"] == "connected_account.status_updated": # connection_name can be missing; fall back to connection_id connection = data.get("connection_name") or data.get("connection_id") if data.get("status") in ("EXPIRED", "DISCONNECTED"): # Ask this user to reconnect, for example with a new authorization link print(f"{data.get('identifier')}: reconnect {connection}") elif data.get("status") == "ACTIVE": print(f"{data.get('identifier')}: {connection} is connected") return {"ok": True} ``` **Node.js** ```ts title="webhooks.ts" import express from 'express'; import { ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const app = express(); const seenEventIds = new Set(); // use your database in production // Read the raw body: the signature covers the exact bytes Scalekit sent app.post('/webhooks/scalekit', express.raw({ type: 'application/json' }), (req, res) => { const body = req.body.toString('utf8'); try { scalekit.verifyWebhookPayload( process.env.SCALEKIT_WEBHOOK_SECRET!, req.headers as Record, body, ); } catch { return res.status(400).send('invalid signature'); } const event = JSON.parse(body); if (seenEventIds.has(event.id)) return res.json({ ok: true }); seenEventIds.add(event.id); const { data } = event; if (event.type === 'connected_account.status_updated') { // connection_name can be missing; fall back to connection_id const connection = data?.connection_name ?? data?.connection_id; if (data?.status === 'EXPIRED' || data?.status === 'DISCONNECTED') { // Ask this user to reconnect, for example with a new authorization link console.log(`${data?.identifier}: reconnect ${connection}`); } else if (data?.status === 'ACTIVE') { console.log(`${data?.identifier}: ${connection} is connected`); } } res.json({ ok: true }); }); app.listen(3000); ``` To ask the user to reconnect, create an authorization link for `data.identifier` and `data.connection_name`, as shown in [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/). ## Respond quickly and handle repeats - **Return a `2xx` status fast.** Do slow work, such as sending an email, after you respond or in a background job. Scalekit retries deliveries that fail or time out. - **Verify before you queue.** Verify the signature in the handler that receives the request, then queue the verified event. Don't verify in a queue worker: the SDK rejects requests more than 5 minutes old, and a queued event can wait longer than that. - **Expect the same event more than once.** Retries and resends repeat an event with the same `id`. Store the IDs you've handled, and skip a repeat. - **Don't rely on order.** Events can arrive out of order. Compare `occurred_at`, or read the account's current status (`get_connected_account_details` in Python, `getConnectedAccount` in Node.js) before you act on an old event. ## Check it worked In your Development environment, create an authorization link for a test user and complete it. Your endpoint receives events including `connected_account.magic_link_generated`, `connected_account.oauth_succeeded` and a `connected_account.status_updated` whose `status` is `ACTIVE`. The endpoint's page in **Developers** > **Webhooks** lists every delivery and its result. Resend a delivery from there to test your handler again. ## Common problems ### The SDK raises `Missing required headers` The `webhook-id`, `webhook-timestamp` and `webhook-signature` headers didn't reach the SDK. In Python, pass the headers with lowercase keys. Check that a proxy in front of your server doesn't strip them. ### The SDK raises `Invalid signature` or `Invalid Signature` Python raises `Invalid signature`, and Node.js raises `Invalid Signature`. The body you verified isn't byte-for-byte what Scalekit sent, or the secret is from another endpoint or environment. Verify the raw body before any JSON parsing, and copy the secret again from the endpoint's page. ### The SDK raises `Message timestamp too old` The request is more than five minutes old, or your server's clock is off. Sync the clock, and don't queue requests before you verify them. ### My endpoint receives nothing Check that the endpoint is in the environment where the change happened, that it subscribes to the event, and that its URL is reachable from the internet. The delivery list on the endpoint's page shows failed attempts and their responses. ## Next - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and why accounts expire. - [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/): Send the user a new authorization link to reconnect. --- Source: https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools.md # Use built-in tools Find the tools a user can call, run one with execute_tool, and pass the tool list to your model. Python, Node.js and cURL. Every connector ships prebuilt tools, such as `github_user_repos_list` or `slack_send_message`. Each has a JSON Schema for its input and returns structured output. Your code names the tool and the user; Scalekit adds that user's credentials, calls the app's API and returns the result. This page finds a tool, runs it, and hands the tool list to your model. ## Before you start - An `ACTIVE` connected account for the user. [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) shows how to get one. - The connection name from **AgentKit** > **Connections**. These examples use `github-connect`, the GitHub connection new environments include. - A client set up as in the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/): `actions` in Python, `scalekit` in Node.js. For cURL, get an access token and keep it in `TOKEN`: ```bash TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \ -d grant_type=client_credentials \ -d client_id="$SCALEKIT_CLIENT_ID" \ -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token) ``` 1. ## Find the right tool Search by the job you want done. Pass the user's identifier to see, for each tool, whether that user can call it now. **Python** ```python from scalekit.v1.tools.tools_pb2 import ToolReadinessState results, _ = actions.tools.search_tools( query="list my GitHub repositories", identifier="user_123", top_k=5, ) for tool in results.tools: ready = [ c.connected_account_id for c in tool.connections if c.readiness_state == ToolReadinessState.TOOL_READINESS_STATE_READY ] print(tool.name, round(tool.score, 2), "ready" if ready else "not connected") ``` **Node.js** ```ts import { ToolReadinessState } from '@scalekit-sdk/node'; const { tools } = await scalekit.tools.searchTools('list my GitHub repositories', { identifier: 'user_123', topK: 5, }); for (const tool of tools) { const ready = tool.connections.filter((c) => c.readinessState === ToolReadinessState.READY); console.log(tool.name, tool.score.toFixed(2), ready.length ? 'ready' : 'not connected'); } ``` **cURL** ```bash curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/tools:search" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"query": "list my GitHub repositories", "identifier": "user_123", "top_k": 5}' | jq '.tools[] | {name, score, connections}' ``` Each result has a `score` (higher is closer) and a `connections` list with a `readiness_state` per connection: - `READY`: the user has an `ACTIVE` account on that connection. Call the tool now. - `NEEDS_CONNECTION`: the account exists but isn't active. [Send a new authorization link](https://docs.scalekit.com/agentkit/tools/authorize/). - `NEEDS_REAUTH`: the token expired. Send a new authorization link. A connector the user has never connected isn't in `connections` at all. To get the tools of specific connections instead, list the scoped tools. Pass the connections your agent works with, such as the ones the user picked in your app, and get the tools of those connections in one list: - The user needs a connected account on each connection you pass. Otherwise the call fails. - Pass one connection per connector. Two connections of the same connector, such as two GitHub connections, fail with `INVALID_ARGUMENT`. - Pages hold up to 100 tools. Pass `next_page_token` back until it's empty. The samples list the tools of `github-connect` and of a Gmail connection named `gmail`. Change `gmail` to a connection in your environment, or remove it. **Python** ```python from google.protobuf.json_format import MessageToDict definitions = [] page_token = None while True: page, _ = actions.tools.list_scoped_tools( identifier="user_123", filter={"connection_names": ["github-connect", "gmail"]}, page_size=100, page_token=page_token, ) definitions += [MessageToDict(t.tool)["definition"] for t in page.tools] page_token = page.next_page_token if not page_token: break print(len(definitions), [d["name"] for d in definitions[:5]]) ``` **Node.js** ```ts const definitions = []; let pageToken: string | undefined; do { const page = await scalekit.tools.listScopedTools('user_123', { filter: { connectionNames: ['github-connect', 'gmail'] }, pageSize: 100, pageToken, }); for (const t of page.tools) { if (t.tool?.definition) definitions.push(t.tool.definition); } pageToken = page.nextPageToken || undefined; } while (pageToken); console.log(definitions.length, definitions.slice(0, 5).map((d) => d.name)); ``` **cURL** ```bash curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/api/v1/tools/scoped" \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode "identifier=user_123" \ --data-urlencode "filter.connection_names=github-connect" \ --data-urlencode "filter.connection_names=gmail" \ --data-urlencode "page_size=100" | jq '{names: [.tools[].tool.definition.name], next_page_token}' ``` 2. ## Call the tool Pass the tool name, its input, and the user. Identify the user with `identifier` plus the connection name, or with `connected_account_id` alone, which you get from a search result's `connections` or from `get_connected_account`. When you pass `connected_account_id`, Scalekit ignores `identifier`. **Python** ```python from scalekit.common.exceptions import ScalekitServerException try: result = actions.execute_tool( tool_name="github_user_repos_list", identifier="user_123", connection_name="github-connect", tool_input={"per_page": 5, "sort": "updated"}, ) print(result.execution_id) print(result.data) except ScalekitServerException as e: print("Tool call failed:", e.error_code, e.message) ``` **Node.js** ```ts import { ScalekitServerException } from '@scalekit-sdk/node'; try { const result = await scalekit.actions.executeTool({ toolName: 'github_user_repos_list', identifier: 'user_123', connector: 'github-connect', toolInput: { per_page: 5, sort: 'updated' }, }); console.log(result.executionId); console.log(result.data); } catch (err) { if (!(err instanceof ScalekitServerException)) throw err; console.error('Tool call failed:', err.errorCode, err.message); } ``` **cURL** ```bash curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/execute_tool" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "tool_name": "github_user_repos_list", "connector": "github-connect", "identifier": "user_123", "params": {"per_page": 5, "sort": "updated"} }' | jq '{execution_id, data}' ``` 3. ## Read the result The response wraps the app's output. Read the tool's fields from `data`, and keep `execution_id` to find the call in your logs. Some apps report an error in a successful (`2xx`) response. Then the call still returns, and `data` holds the app's error. Slack, for example, returns: ```json { "ok": false, "error": "missing_scope", "needed": "users:read" } ``` Check `data` for the app's error shape before you use it. When the app answers with an error status instead, such as `401`, `403`, `429` or another `4xx` or `5xx`, the call fails with an error whose message starts with `tool execution failed`. See [Common problems](#common-problems). 4. ## Give the tools to your model Tool definitions carry more than a model needs. Keep `name`, `description` and `input_schema`, a JSON Schema object for the tool's input. Anthropic's API takes it as `input_schema` unchanged, and OpenAI's takes it as a function's `parameters`. This continues from the list in step 1. **Python** ```python llm_tools = [ { "name": d["name"], "description": d.get("description", ""), "input_schema": d.get("input_schema", {"type": "object", "properties": {}}), } for d in definitions ] read_only = [d["name"] for d in definitions if d.get("annotations", {}).get("read_only_hint")] print(len(llm_tools), "tools,", len(read_only), "read-only") ``` **Node.js** ```ts const llmTools = definitions.map((d) => ({ name: String(d.name), description: String(d.description ?? ''), input_schema: d.input_schema ?? { type: 'object', properties: {} }, })); const readOnly = definitions.filter( (d) => (d.annotations as { read_only_hint?: boolean } | undefined)?.read_only_hint, ); console.log(llmTools.length, 'tools,', readOnly.length, 'read-only'); ``` **cURL** ```bash curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/api/v1/tools/scoped" \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode "identifier=user_123" \ --data-urlencode "filter.connection_names=github-connect" \ --data-urlencode "page_size=100" | jq '[.tools[].tool.definition | {name, description, input_schema}]' ``` Each definition's `annotations` include `read_only_hint` and `destructive_hint`. Use them to start an agent with read-only tools, or to ask the user before a destructive call. When the model asks for a tool, call it as in step 2 and send `data` back as the tool result. LangChain and Google ADK get native tool objects from `actions.langchain.get_tools` and `actions.google.get_tools`. [Choose a framework](https://docs.scalekit.com/agentkit/examples/) has a complete agent for each framework. ## Check it worked The call returns an `execution_id` and a `data` object with the app's response, such as a list of repositories for `github_user_repos_list`. Search results for the user show `READY` for the connection you called. ## Common problems ### `failed to get tool: ` That tool name doesn't exist in your environment. Use a name from search or from the scoped list, exactly as returned. ### `connection not found for the given key` The connection name doesn't match **AgentKit** > **Connections** exactly. Connection names can differ between environments, so read them from configuration rather than hard-coding them. ### `connected account not found` No connected account exists for this identifier on this connection. Check that you pass the same identifier you used when the user authorized, or [authorize the user](https://docs.scalekit.com/agentkit/tools/authorize/) first. ### `connected account is not active` The user hasn't finished authorizing, or the account expired or was disconnected. Send a new [authorization link](https://docs.scalekit.com/agentkit/tools/authorize/) and wait until the status is `ACTIVE`. ### `missing or invalid parameter` The input doesn't match the tool's `input_schema`. Check required fields and types in the tool's definition. ### The call returns, but `data` holds an error such as `missing_scope` The app refused the call in a `2xx` response. For a missing scope, add the scope to the connection, then have the user authorize again so their token includes it. ### `tool execution failed - unauthorized access` The app answered `401`: it rejected the user's token. Check the connected account's status, and if the user revoked access, send a new [authorization link](https://docs.scalekit.com/agentkit/tools/authorize/). ### `tool execution failed - forbidden access` The app answered `403`. The user's token is usually missing a scope the tool needs, or the user lacks permission in the app. Add the scope to the connection, then have the user authorize again. ### `tool execution failed - rate limited` The app answered `429`. Wait before you retry, and slow down calls for this user. ### `tool execution failed - ` The app answered with another error status. The rest of the message is the app's own error. Check the input against the tool's `input_schema` and the app's API reference. ## Next - [Call any API](https://docs.scalekit.com/agentkit/tools/custom-tools/): Reach an endpoint no built-in tool covers, with the user's credentials. - [Choose a framework](https://docs.scalekit.com/agentkit/examples/): A complete agent in LangChain, OpenAI, Anthropic and more. To serve these tools to an MCP client instead, [create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/). ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Execute a tool](https://docs.scalekit.com/agentkit/reference/tools/execute-a-tool.md) - `GET` [List a user's scoped tools](https://docs.scalekit.com/agentkit/reference/tools/list-scoped-tools.md) - `POST` [Search tools](https://docs.scalekit.com/agentkit/reference/tools/search-tools.md) --- Source: https://docs.scalekit.com/agentkit/tools/custom-tools.md # Call any API Call any endpoint of a connector's API as the user through the Scalekit API proxy, then wrap it as a tool for your agent. Python, Node.js and cURL. When no [built-in tool](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/) does what you need, call the app's API directly through the Scalekit proxy. You send the path, method and body; Scalekit adds the user's credentials, calls the app at its base URL and returns the raw response. Then you can wrap the call as your own tool. The proxy is on for most connectors. Search the built-in tools first: they come with schemas and structured output, and cover most of each app's API. ## Before you start - An `ACTIVE` connected account for the user. [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) shows how to get one. - The connection name from **AgentKit** > **Connections**. These examples use a Slack connection named `slack`. - A client set up as in the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/): `actions` in Python, `scalekit` in Node.js. For cURL, get an access token and keep it in `TOKEN`: ```bash TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \ -d grant_type=client_credentials \ -d client_id="$SCALEKIT_CLIENT_ID" \ -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token) ``` - The app's API reference for the endpoint you want: | Connector | API reference | | --- | --- | | Gmail | [Gmail API](https://developers.google.com/gmail/api/reference/rest) | | Slack | [Slack API methods](https://api.slack.com/methods) | | GitHub | [GitHub REST API](https://docs.github.com/en/rest) | | Salesforce | [Salesforce REST API](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/) | | HubSpot | [HubSpot API](https://developers.hubspot.com/docs/api/overview) | 1. ## Check the connected account is active The proxy only works for an `ACTIVE` connected account. Check before you call, and send a new authorization link if it isn't. **Python** ```python account = actions.get_connected_account_details( connection_name="slack", identifier="user_123", ).connected_account if account.status != "ACTIVE": raise SystemExit(f"Slack is {account.status}. Send the user a new authorization link.") ``` **Node.js** ```ts import { ConnectorStatus } from '@scalekit-sdk/node'; const { connectedAccount: account } = await scalekit.actions.getConnectedAccount({ connectionName: 'slack', identifier: 'user_123', }); if (account?.status !== ConnectorStatus.ACTIVE) { throw new Error('Slack is not ACTIVE. Send the user a new authorization link.'); } ``` **cURL** ```bash curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/api/v1/connected_accounts/details" \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode "connector=slack" \ --data-urlencode "identifier=user_123" | jq -r '.connected_account.status' ``` 2. ## Call the endpoint Pass the path from the app's API reference, without the base URL. Query parameters go in `query_params`, and a JSON body goes in `body`. This reads the custom profile fields of the user's Slack workspace, which no built-in Slack tool covers: **Python** ```python from scalekit.common.exceptions import ScalekitServerException try: response = actions.request( connection_name="slack", identifier="user_123", method="GET", path="/api/team.profile.get", query_params={"visibility": "visible"}, ) print(response.status_code, response.json()) except ScalekitServerException as e: print("Request failed:", e.error_code, e.message) ``` **Node.js** ```ts import { ScalekitServerException } from '@scalekit-sdk/node'; try { const response = await scalekit.actions.request({ connectionName: 'slack', identifier: 'user_123', method: 'GET', path: '/api/team.profile.get', queryParams: { visibility: 'visible' }, }); console.log(response.status, response.data); } catch (err) { if (!(err instanceof ScalekitServerException)) throw err; console.error('Request failed:', err.errorCode, err.message); } ``` **cURL** ```bash curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/proxy/api/team.profile.get" \ -H "Authorization: Bearer $TOKEN" \ -H "connection_name: slack" \ -H "identifier: user_123" \ --data-urlencode "visibility=visible" ``` A write works the same way with a body. This sets a field on the user's Slack profile: **Python** ```python response = actions.request( connection_name="slack", identifier="user_123", method="POST", path="/api/users.profile.set", body={"profile": {"title": "Support engineer"}}, ) print(response.status_code, response.json()) ``` **Node.js** ```ts const response = await scalekit.actions.request({ connectionName: 'slack', identifier: 'user_123', method: 'POST', path: '/api/users.profile.set', body: { profile: { title: 'Support engineer' } }, }); console.log(response.status, response.data); ``` **cURL** ```bash curl -sS -X POST "$SCALEKIT_ENVIRONMENT_URL/proxy/api/users.profile.set" \ -H "Authorization: Bearer $TOKEN" \ -H "connection_name: slack" \ -H "identifier: user_123" \ -H "Content-Type: application/json" \ -d '{"profile": {"title": "Support engineer"}}' ``` The proxy returns the app's response as is: its status code, headers and body. In Python, `request` returns a `requests` response (`status_code`, `json()`) for every status, so check `status_code`. In Node.js, it returns an Axios response (`status`, `data`) for a `2xx` status, and throws a `ScalekitServerException` for any other status, so wrap the call in `try`/`catch`. 3. ## Wrap it as a tool Give your model a tool that matches what the agent needs to do, not the shape of the app's endpoint. Keep the input schema small, validate model input before you send it, and return only the fields the model needs. **Python** ```python list_profile_fields_tool = { "name": "slack_list_profile_fields", "description": "List the custom profile fields defined in the user's Slack workspace, " "such as Title or Pronouns. Returns each field's ID, label and type.", "input_schema": {"type": "object", "properties": {}}, } def slack_list_profile_fields(identifier: str) -> dict: response = actions.request( connection_name="slack", identifier=identifier, method="GET", path="/api/team.profile.get", ) data = response.json() if not data.get("ok"): return {"error": data.get("error")} fields = data.get("profile", {}).get("fields", []) return {"fields": [{"id": f["id"], "label": f["label"], "type": f["type"]} for f in fields]} print(slack_list_profile_fields("user_123")) ``` **Node.js** ```ts const listProfileFieldsTool = { name: 'slack_list_profile_fields', description: "List the custom profile fields defined in the user's Slack workspace, " + "such as Title or Pronouns. Returns each field's ID, label and type.", input_schema: { type: 'object', properties: {} }, }; type ProfileField = { id: string; label: string; type: string }; async function slackListProfileFields(identifier: string) { const response = await scalekit.actions.request({ connectionName: 'slack', identifier, method: 'GET', path: '/api/team.profile.get', }); const data = response.data as { ok: boolean; error?: string; profile?: { fields?: ProfileField[] } }; if (!data.ok) return { error: data.error }; const fields = data.profile?.fields ?? []; return { fields: fields.map(({ id, label, type }) => ({ id, label, type })) }; } console.log(listProfileFieldsTool.name, await slackListProfileFields('user_123')); ``` Pass the tool definition to your model with the built-in tools, and run the function when the model calls it. ## Check it worked The call returns the app's own response. For Slack, a `200` with `"ok": true` and the fields you asked for. Slack reports errors in the body with `"ok": false`, while most other APIs use a `4xx` status. ## Common problems ### `proxy not enabled for provider` The proxy is off for that connector. Contact [support@scalekit.com](mailto:support@scalekit.com) to turn it on. ### `authorization details are missing for connected account identifier` No active connected account matches this identifier and connection. Check both values, and [authorize the user](https://docs.scalekit.com/agentkit/tools/authorize/) if the account isn't `ACTIVE`. ### Node.js: `request()` throws a `401` on the first call Make another SDK call before your first `request()`, such as the account check in step 1. The Node.js client fetches its access token on its first regular SDK call, and `request()` then sends that token. In a long-running process, if `request()` later throws a `401` because the token expired, make another SDK call and retry. ### The app returns `401` or `403` The user's token is missing a scope the endpoint needs. Add the scope to the connection, then have the user authorize again. ### The app only accepts traffic from known IP addresses Add Scalekit's [outbound IP addresses](https://docs.scalekit.com/reference/outbound-ip-addresses/) for your region to the app's allowlist. ## Next - [Choose a framework](https://docs.scalekit.com/agentkit/examples/): Add your tools to an agent in LangChain, OpenAI, Anthropic and more. - [Create a connector](https://docs.scalekit.com/agentkit/bring-your-own-connector/overview/): Add an app that isn't in the catalog. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `GET` [Get a connected account](https://docs.scalekit.com/agentkit/reference/connected-accounts/get-a-connected-account.md) - `GET` [Get a connected account's credentials](https://docs.scalekit.com/agentkit/reference/connected-accounts/get-connected-account-credentials.md) --- Source: https://docs.scalekit.com/agentkit/mcp/overview.md # How Virtual MCP servers work A Virtual MCP server exposes the tools you choose, for one user at a time, to any MCP client. How servers, session tokens and tool calls fit together. A **Virtual MCP server** is a URL that exposes tools you choose, from one or more connections, to any MCP client. You create one server per agent role, such as a support assistant, and all your users share it. Before each run, you mint a **session token** for one user. The agent connects to the server URL with that token and can call only the tools you chose, as that user. Use a Virtual MCP server when your agent already speaks MCP. If you build the agent loop yourself, you can call tools from your code instead: see [Use built-in tools](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/). Every AgentKit connection also has its own MCP server, with all of that connection's tools and nothing to create. [Use a connection's MCP server](https://docs.scalekit.com/agentkit/mcp/connection-mcp-servers/) compares the two. Session tokens work the same way on both. ## Why use one - **The agent sees only the tools it needs.** A Gmail connection has dozens of tools. A summarizer needs one. The server exposes only the tools you list, so the agent can't call the rest. - **Smaller prompts.** Every tool on an MCP server goes into the model's context. Fewer tools means fewer tokens on every call. - **One definition, isolated users.** All users share the server's URL. Each run's session token is bound to one user and that user's connected accounts, so one user's run can't act with another user's credentials. - **Read-only runs from the same server.** Mint a [read-only token](https://docs.scalekit.com/agentkit/mcp/read-only-tokens/) and the agent gets only the tools that don't change anything, without a second server. ## How it works | When | You | What you get | | --- | --- | --- | | Once per agent role | [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/) with the connections and tools the agent needs | A server ID and a fixed `mcp_server_url` | | Before each run | [Check the user's connected accounts](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/check-connected-accounts-for-a-server/) are `ACTIVE`, then [mint a session token](https://docs.scalekit.com/agentkit/mcp/session-tokens/) | A bearer token for one user and one server | | During the run | Connect any MCP client to the URL with `Authorization: Bearer ` | The server's tools, called as that user | ```d2 title="Create a Virtual MCP server once per agent role, mint a session token for each user's run, and let the MCP client call tools through Scalekit as that user" direction: right App: "Your app" Client: "MCP client\n(your agent)" Scalekit: { label: "Scalekit" Server: "Virtual MCP server\n(the tools you chose)" Account: "The user's\nconnected account" } Apps: "The user's apps\n(Gmail, Slack, ...)" App -> Scalekit.Server: "1. Create once per agent role" App -> Scalekit.Server: "2. Mint a session token per run" App -> Client: "3. Server URL + token" Client -> Scalekit.Server: "4. tools/list, tools/call" Scalekit.Server -> Scalekit.Account: "5. That user's credentials" Scalekit.Account -> Apps: "6. API call" ``` In the API and SDKs, a Virtual MCP server is called an **MCP configuration**, for example `create_config` in Python and `createConfig` in Node.js. The [Virtual MCP servers API reference](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/) lists every operation. ## Session tokens A session token authorizes one user on one server: - It lasts one hour unless you set `expiry`, which must be between 60 seconds and 24 hours. - It can't be refreshed or extended. Mint a new one instead. Minting a new token doesn't revoke earlier ones. - It only works on the server it was minted for. Any other server URL returns `401`. - It can be full or read-only. A [read-only token](https://docs.scalekit.com/agentkit/mcp/read-only-tokens/) sees and calls only the server's read-only tools. - Check the user's connected accounts before the run. A tool call fails until the account for its connection is `ACTIVE`. [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/) covers when to mint for one-off, scheduled, concurrent and long-running agents. ## What the agent sees `tools/list` returns the tools you chose for the server. Tools from a connection the user hasn't authorized don't work until they do. `tools/list` also includes a `connect_account` tool. When the user still needs to connect an app, the agent calls it to get an authorization link, and shows the link to the user. A tool call returns the same output as [`execute_tool`](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/#read-the-result): the app's response under `data`. If the app reports an error in a successful (`2xx`) response, that error is in `data`. If the app answers with an error status, the tool call returns an error result (`isError: true`) whose message starts with `tool execution failed`. ## Data flow 1. The MCP client sends a request to the server URL with the session token. 2. Scalekit checks the token against the server and the user, and finds the user's connected account for the tool's connection. 3. Scalekit calls the app's API with that account's credentials and returns the result to the client. Your agent never receives the user's OAuth token or API key. Responses pass through to the MCP client and aren't stored. See [what Scalekit stores](https://docs.scalekit.com/agentkit/security/#what-scalekit-stores). ## Next - [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/): Choose the tools and get the server URL. - [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/): Authorize a user's run and connect an MCP client. - [Use a connection's MCP server](https://docs.scalekit.com/agentkit/mcp/connection-mcp-servers/): Every tool of one connection, with no server to create. --- Source: https://docs.scalekit.com/agentkit/mcp/configure-mcp-server.md # Create a Virtual MCP server Create an AgentKit Virtual MCP server with only the tools an agent needs, then list, update or delete it, with examples in Python, Node.js and cURL. Create one Virtual MCP server for each agent role. It lists the connections and tools the agent can use, and gives you a fixed URL that every user of that agent shares. You do this once, not once per user. ## Before you start - A connection for each app the agent needs, in **AgentKit** > **Connections**. [Set up a connection](https://docs.scalekit.com/agentkit/connections/) if you haven't. These examples use `github-connect`, the GitHub connection new environments include. - A client set up as in the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/): `actions` in Python, `scalekit` in Node.js. For cURL, get an access token and keep it in `TOKEN`: ```bash TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \ -d grant_type=client_credentials \ -d client_id="$SCALEKIT_CLIENT_ID" \ -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token) ``` 1. ## Choose the tools Pick the smallest set of tools the agent needs. Get exact names from [tool search or the scoped tool list](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/#find-the-right-tool), or from each connector's page in the [connector catalog](https://docs.scalekit.com/agentkit/connectors/). If you leave out `tools` for a connection, the server gets every tool that connection has when you create it. Tools added to the connector later aren't included. 2. ## Create the server Give the server a name that's unique in the environment, and list the tools for each connection. **Python** ```python from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping response = actions.mcp.create_config( name="repo-assistant", description="Reads the user's GitHub repositories and issues", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="github-connect", tools=["github_user_repos_list", "github_issues_list"], ), ], ) config_id = response.config.id mcp_server_url = response.config.mcp_server_url print(config_id, mcp_server_url) ``` **Node.js** ```ts const { config } = await scalekit.actions.mcp.createConfig({ name: 'repo-assistant', description: "Reads the user's GitHub repositories and issues", connectionToolMappings: [ { connectionName: 'github-connect', tools: ['github_user_repos_list', 'github_issues_list'] }, ], }); const configId = config!.id; const mcpServerUrl = config!.mcpServerUrl; console.log(configId, mcpServerUrl); ``` **cURL** ```bash curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "repo-assistant", "description": "Reads the user'\''s GitHub repositories and issues", "connection_tool_mappings": [ {"connection_name": "github-connect", "tools": ["github_user_repos_list", "github_issues_list"]} ] }' | jq '.config | {id, mcp_server_url}' ``` 3. ## Save the ID and URL Store the server's `id` and `mcp_server_url` in your configuration. You pass the ID when you mint session tokens, and the URL to your agent. For cURL, keep them in `CONFIG_ID` and `MCP_SERVER_URL`. ## Check it worked List servers by name to see the new server and its URL: **Python** ```python response = actions.mcp.list_configs(filter_name="repo-assistant") for config in response.configs: print(config.id, config.name, config.mcp_server_url) ``` **Node.js** ```ts const { configs } = await scalekit.actions.mcp.listConfigs({ search: 'repo-assistant' }); for (const c of configs) console.log(c.id, c.name, c.mcpServerUrl); ``` **cURL** ```bash curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs" \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode "filter.name=repo-assistant" | jq '.configs[] | {id, name, mcp_server_url}' ``` Then [mint a session token](https://docs.scalekit.com/agentkit/mcp/session-tokens/) and list the server's tools from an MCP client. ## Choose how tools are exposed A server exposes its tools in one of two modes, set with `tool_exposure`: - `DIRECT`: the agent sees every tool you map to the server. A server can map up to 25 connections, and two connections can't map tools with the same name. - `META`: the agent sees five tools, `search_available_tools`, `list_available_tools`, `get_tool_details`, `execute_tool` and `connect_account`, and uses them to find and run the tools you mapped. A server can map more than 25 connections, and tool names can repeat across connections. Choose `META` when an agent needs more connections or tools than it can list at once. If you leave `tool_exposure` out, the server uses `DIRECT`. The response's `tool_exposure` shows the server's mode. **Python** ```python response = actions.mcp.create_config( name="support-agent", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="github-connect", tools=["github_user_repos_list", "github_issues_list"], ), ], tool_exposure="META", ) ``` **Node.js** ```ts const { config } = await scalekit.actions.mcp.createConfig({ name: 'support-agent', connectionToolMappings: [ { connectionName: 'github-connect', tools: ['github_user_repos_list', 'github_issues_list'], }, ], toolExposure: 'META', }); ``` **cURL** ```bash curl -sS -X POST "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "support-agent", "connection_tool_mappings": [ {"connection_name": "github-connect", "tools": ["github_user_repos_list", "github_issues_list"]} ], "tool_exposure": "META" }' | jq '.config | {id, name, tool_exposure}' ``` To switch an existing server, pass `tool_exposure` to the update call. Leave it out to keep the current mode. After a switch, MCP clients must list the server's tools again; session tokens you already minted keep working. A switch to `DIRECT` fails while the server maps more than 25 connections or two of its connections map tools with the same name: remove mappings first. ### How an agent uses a META server The agent finds a tool, reads its input schema, and runs it as the user: 1. `list_available_tools` with no arguments returns each connection on the server with its provider, `tool_count` and `readiness`. With a `connection_name`, it returns that connection's tools a page at a time. 2. `search_available_tools` finds tools by keywords, such as `create issue`. Each match names its `connection_name`. 3. `get_tool_details` returns one tool's `input_schema`. The agent calls it before `execute_tool`. 4. `execute_tool` runs the tool with `tool_name`, `connection_name` and `input`, using the user's own connected account. `readiness` tells the agent whether the user can run a connection's tools now: | `readiness` | Meaning | | --- | --- | | `ready` | The user's connected account is `ACTIVE`. | | `needs_reauth` | The user's connected account is `EXPIRED`, so the user must authorize the app again. | | `needs_connection` | The user has no `ACTIVE` or `EXPIRED` account for this connection, for example they never finished authorizing it. | If `execute_tool` returns `not_connected` or `reauth_needed`, the error names the `connection_name`. The agent calls `connect_account` with that name, shows the user the authorization link, and retries after the user finishes. Other errors are `invalid_argument`, `not_found`, `forbidden` (a [read-only token](https://docs.scalekit.com/agentkit/mcp/read-only-tokens/) calling a tool that writes) and `internal_error`. Because the agent checks readiness itself, you can [mint a session token](https://docs.scalekit.com/agentkit/mcp/session-tokens/) for a META server before the user has connected every app. ## Change or remove a server To change the tools, send the full list for each connection. It replaces the old list. **Python** ```python actions.mcp.update_config( config_id=config_id, connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="github-connect", tools=["github_user_repos_list", "github_issues_list", "github_issue_get"], ), ], ) ``` **Node.js** ```ts await scalekit.actions.mcp.updateConfig({ configId, connectionToolMappings: [ { connectionName: 'github-connect', tools: ['github_user_repos_list', 'github_issues_list', 'github_issue_get'], }, ], }); ``` **cURL** ```bash curl -sS -X PUT "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs/$CONFIG_ID" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "connection_tool_mappings": [ {"connection_name": "github-connect", "tools": ["github_user_repos_list", "github_issues_list", "github_issue_get"]} ] }' | jq '.config.connection_tool_mappings' ``` Change a server when no agent run is using it, because a run can lose a tool mid-conversation. For a larger change, create a new server and point your agent at its URL. Deleting a server stops its URL from working immediately, including for session tokens already minted. **Python** ```python actions.mcp.delete_config(config_id=config_id) ``` **Node.js** ```ts await scalekit.actions.mcp.deleteConfig(configId); ``` **cURL** ```bash curl -sS -X DELETE "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs/$CONFIG_ID" \ -H "Authorization: Bearer $TOKEN" ``` ## Common problems ### `invalid tool ''` The tool name doesn't exist on that connection. Copy names from tool search or the scoped tool list. ### `connection '' not found` The connection name doesn't match **AgentKit** > **Connections** exactly. ### `config with name '' already exists` Server names are unique in an environment. Choose another name, or update the existing server. ### `a DIRECT virtual MCP server supports at most 25 connections; use META for more` The server maps more than 25 connections. Set `tool_exposure` to `META`, or map fewer connections. ### `validation failed due to tool overlap for config ''` Two connections on a `DIRECT` server map tools with the same name. Remove one of the mappings, or set `tool_exposure` to `META`. A switch from `META` to `DIRECT` fails with `cannot switch config '' to DIRECT: its connections define overlapping tools` for the same reason. ## Next - [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/): Authorize a user's run and connect an MCP client. - [Choose a framework](https://docs.scalekit.com/agentkit/examples/): Connect the server to CrewAI, Mastra, Claude Managed Agents and more. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/create-a-virtual-mcp-server.md) - `GET` [List Virtual MCP servers](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/list-virtual-mcp-servers.md) - `PUT` [Update a Virtual MCP server](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/update-a-virtual-mcp-server.md) - `DELETE` [Delete a Virtual MCP server](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/delete-a-virtual-mcp-server.md) --- Source: https://docs.scalekit.com/agentkit/mcp/connection-mcp-servers.md # Use a connection's MCP server Every AgentKit connection has its own MCP server with all of the connection's tools. Find its URL, mint a session token for a user and connect any MCP client. Every AgentKit [connection](https://docs.scalekit.com/agentkit/concepts/) has its own MCP server. It serves all of that connection's tools, and there is nothing to create: once the connection exists, so does its server. Mint a session token for a user, and any MCP client can call the connection's tools as that user. ## Choose a connection MCP server or a Virtual MCP server | | Connection MCP server | [Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/overview/) | | --- | --- | --- | | Tools | Every tool of one connection | The tools you choose, from one or more connections | | Setup | None | Create it once per agent role | | URL | `/mcp/v3/connections/` | The `mcp_server_url` returned when you create it | | Session token | Minted for the connection | Minted for the server | Use a connection MCP server when an agent works with one app and should see what that app offers. Use a Virtual MCP server when an agent needs a few tools from several apps, or you want to keep the tool list short. Both accept [read-only tokens](https://docs.scalekit.com/agentkit/mcp/read-only-tokens/). ## Before you start - An AgentKit [connection](https://docs.scalekit.com/agentkit/connections/), and its name, such as `gmail`. The URL uses the name exactly as stored, including case. - The user's `identifier`: your app's ID for that user, the same value you passed when they authorized the connection. [Choose an identifier](https://docs.scalekit.com/agentkit/concepts/#choose-an-identifier) explains why it shouldn't be an email address. - A client set up as in the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/): `actions` in Python, `scalekit` in Node.js. For cURL, get an access token and keep it in `TOKEN`: ```bash TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \ -d grant_type=client_credentials \ -d client_id="$SCALEKIT_CLIENT_ID" \ -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token) ``` 1. ## Get the server URL In the dashboard, go to **AgentKit** > **Connections**, open the connection, and copy **MCP Server URL** from the **Configuration** tab. It has this form: ```text https:///mcp/v3/connections/gmail ``` You can also build it from your environment URL and the connection name. The path is case-sensitive, so use the name exactly as it appears in the dashboard. 2. ## Mint a session token Mint a token for the user before each run. Set `expiry` a little above how long the run takes: between 60 seconds and 24 hours, one hour by default. **Python** ```python from datetime import timedelta session = actions.mcp.create_session_token( connection_name="gmail", identifier="user_123", expiry=timedelta(minutes=15), ) session_token = session.token ``` **Node.js** ```ts const session = await scalekit.actions.mcp.createSessionToken({ connectionName: 'gmail', identifier: 'user_123', expirySeconds: 900, }); const sessionToken = session.token; ``` **cURL** ```bash SESSION_TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/connections/gmail/tokens" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"identifier": "user_123", "expiry": "900s"}' | jq -r .token) ``` The token works only on this connection's server. To limit the agent to tools that don't change anything, add `access_level="READ_ONLY"` (Python) or `accessLevel: 'READ_ONLY'` (Node.js): see [Limit an agent to read-only tools](https://docs.scalekit.com/agentkit/mcp/read-only-tokens/). 3. ## Connect the MCP client Give your MCP client the server URL and the header `Authorization: Bearer `. Most frameworks take them as the server's URL and headers. [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/#connect-the-mcp-client) shows the MCP SDK clients in Python and Node.js, and [Connect any MCP client](https://docs.scalekit.com/agentkit/mcp/connect-any-client/) has the HTTP contract for everything else. ## Check it worked List the tools with the token: ```bash curl -sS "$SCALEKIT_ENVIRONMENT_URL/mcp/v3/connections/gmail" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' | sed -n 's/^data: //p' | jq -r '.result.tools[].name' ``` You see the connection's tools, such as `gmail_fetch_mails`, and `connect_account`. If the user hasn't authorized the connection yet, the agent calls `connect_account` to get an authorization link for them, as in [Connect a user's account from the agent](https://docs.scalekit.com/agentkit/mcp/connect-any-client/#connect-a-users-account-from-the-agent). ## Common problems ### `404` when minting the token No active connection has that name in this environment. Check the name in **AgentKit** > **Connections**, and that the connection is enabled. ### `400` when minting the token The identifier or `expiry` is invalid, the connection isn't an AgentKit connection, or the user has no active account on the connection. In the last case, [send the user an authorization link](https://docs.scalekit.com/agentkit/tools/authorize/) and mint again once the account is `ACTIVE`. ### `401` with `invalid or expired bearer token` The token expired, was minted for a different server, or the URL's connection name differs from the stored name, for example in case. Copy the URL from the dashboard and mint a new token for this connection. ## Next - [Limit an agent to read-only tools](https://docs.scalekit.com/agentkit/mcp/read-only-tokens/): Mint a token that can only read, on any MCP server. - [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/): Decide when to mint for scheduled, concurrent and long-running agents. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Mint a session token](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-session-token.md) - `POST` [Mint a session token for a connection](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-connection-session-token.md) --- Source: https://docs.scalekit.com/agentkit/mcp/session-tokens.md # Mint session tokens Mint a session token so an agent can call a Virtual MCP server's tools as one user, and decide when to mint again for scheduled and long-running agents. A session token lets an MCP client call a Virtual MCP server's tools as one user. Mint one before each run: check that the user's connections are active, mint the token, and pass it to the agent as a bearer token. This page uses a Virtual MCP server. For a [connection's own MCP server](https://docs.scalekit.com/agentkit/mcp/connection-mcp-servers/), you mint the token for the connection instead; everything else is the same. ## Before you start - A [Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/) and its ID. For cURL, keep the ID in `CONFIG_ID` and the URL in `MCP_SERVER_URL`. - The user's `identifier`: your app's ID for that user, the same value you passed when they authorized their connections. Use an ID that's stable and hard to guess, not an email address. [Choose an identifier](https://docs.scalekit.com/agentkit/concepts/#choose-an-identifier) explains why. - A client set up as in the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/): `actions` in Python, `scalekit` in Node.js. For cURL, get an access token and keep it in `TOKEN`: ```bash TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \ -d grant_type=client_credentials \ -d client_id="$SCALEKIT_CLIENT_ID" \ -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token) ``` 1. ## Check the user's connections List the user's connected account for each connection on the server. For any that isn't `ACTIVE`, send the user its authorization link before the run. On a server whose `tool_exposure` is `META`, this check is optional: the agent sees each connection's readiness and can call `connect_account` to get the link. See [How an agent uses a META server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/#how-an-agent-uses-a-meta-server). **Python** ```python response = actions.mcp.list_mcp_connected_accounts( config_id=config_id, identifier="user_123", include_auth_link=True, ) not_ready = [a for a in response.connected_accounts if a.connected_account_status != "ACTIVE"] for account in not_ready: print(f"{account.connection_name} is {account.connected_account_status}: {account.authentication_link}") ``` **Node.js** ```ts const { connectedAccounts } = await scalekit.actions.mcp.listConnectedAccounts({ configId, identifier: 'user_123', includeAuthLink: true, }); const notReady = connectedAccounts.filter((a) => a.connectedAccountStatus !== 'ACTIVE'); for (const a of notReady) { console.log(`${a.connectionName} is ${a.connectedAccountStatus}: ${a.authenticationLink}`); } ``` **cURL** ```bash curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs/$CONFIG_ID/connected_accounts" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"identifier": "user_123", "include_auth_link": true}' | jq '.connected_accounts[] | select(.connected_account_status != "ACTIVE") | {connection_name, connected_account_status, authentication_link}' ``` If the user has no account yet for a connection, this call creates one in `PENDING_AUTH` and returns its link. 2. ## Mint the token Set `expiry` a little above how long the run takes. It must be between 60 seconds and 24 hours, and defaults to one hour. If the run only needs to read, add `access_level="READ_ONLY"` (Python) or `accessLevel: 'READ_ONLY'` (Node.js): see [Limit an agent to read-only tools](https://docs.scalekit.com/agentkit/mcp/read-only-tokens/). **Python** ```python from datetime import timedelta session = actions.mcp.create_session_token( mcp_config_id=config_id, identifier="user_123", expiry=timedelta(minutes=30), ) session_token = session.token print(session.expires_at) ``` **Node.js** ```ts const session = await scalekit.actions.mcp.createSessionToken({ mcpConfigId: configId, identifier: 'user_123', expirySeconds: 30 * 60, }); const sessionToken = session.token; console.log(new Date(Number(session.expiresAt?.seconds) * 1000)); ``` **cURL** ```bash SESSION_TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs/$CONFIG_ID/tokens" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"identifier": "user_123", "expiry": "1800s"}' | jq -r .token) ``` 3. ## Connect the MCP client Pass the server URL and `Authorization: Bearer ` to your MCP client. Most frameworks take them as the server's URL and headers. With the MCP SDKs, or directly over HTTP: **Python** ```python import asyncio import httpx2 from mcp import Client from mcp.client.streamable_http import streamable_http_client async def list_tools(): headers = {"Authorization": f"Bearer {session_token}"} async with httpx2.AsyncClient(headers=headers, timeout=30) as http_client: async with Client(streamable_http_client(mcp_server_url, http_client=http_client)) as mcp_client: result = await mcp_client.list_tools() print([tool.name for tool in result.tools]) asyncio.run(list_tools()) ``` **Node.js** ```ts import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; const transport = new StreamableHTTPClientTransport(new URL(mcpServerUrl), { requestInit: { headers: { Authorization: `Bearer ${sessionToken}` } }, }); const mcpClient = new Client({ name: 'repo-assistant', version: '1.0.0' }); await mcpClient.connect(transport); const { tools } = await mcpClient.listTools(); console.log(tools.map((tool) => tool.name)); await mcpClient.close(); ``` **cURL** ```bash curl -sS "$MCP_SERVER_URL" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' ``` The Python example uses `mcp` 2.x (`pip install "mcp>=2"`), and the Node.js example uses `@modelcontextprotocol/sdk`. For clients without an MCP SDK, [Connect any MCP client](https://docs.scalekit.com/agentkit/mcp/connect-any-client/) has the full HTTP contract, including `tools/call` and the response format. [Choose a framework](https://docs.scalekit.com/agentkit/examples/) shows the setup for CrewAI, Mastra and Claude Managed Agents. ## Choose when to mint A token can't be refreshed, so decide when your code mints a new one: | Your agent | When to mint | | --- | --- | | Runs once, for minutes | Once, right before the run. Set `expiry` above the longest run you expect. | | Chat session with a user | When the session starts. Mint again if the session outlives the token, or when a call returns `401`. | | Scheduled job, such as a daily briefing | At the start of each run. Don't store a token between runs. | | Many runs at once | One token per run. Tokens are independent, and minting a new one doesn't revoke the others. | | Long-running host or gateway | On a schedule, before `expires_at`. Give the host the new token, then reload it. A host with an expired token fails at its next tool call, not at startup. | A token lasts at most 24 hours, so a client that can't update its headers, such as a config file you edit by hand, stops working within a day. ## Check it worked `tools/list` returns the tools you chose for the server, and a tool call returns the app's response under `data`. ## Common problems ### `401` with `invalid or expired bearer token` The token expired, is for a different server, or was copied incompletely. Mint a new token for this server. ### `expiry must be between 60s and 24h` Set `expiry` between 60 seconds and 24 hours. In REST, send it in seconds, such as `"1800s"`. ### `unknown tool ""` with a read-only token A [read-only token](https://docs.scalekit.com/agentkit/mcp/read-only-tokens/) includes only the server's read-only tools. Mint a full token if the run needs that tool. ### A tool call fails because the account isn't connected The user's account for that connection isn't `ACTIVE`. Run the check in step 1 and send the authorization link. The agent can also call `connect_account` to get the link itself. ## Next - [Limit an agent to read-only tools](https://docs.scalekit.com/agentkit/mcp/read-only-tokens/): Mint a token that sees and calls only read-only tools. - [Connect any MCP client](https://docs.scalekit.com/agentkit/mcp/connect-any-client/): The HTTP contract for clients that don't use an MCP SDK. - [Choose a framework](https://docs.scalekit.com/agentkit/examples/): Run an agent against the server in your framework. - [Verify users](https://docs.scalekit.com/agentkit/user-verification/): Confirm who authorized each connection before production. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `GET` [List tools](https://docs.scalekit.com/agentkit/reference/tools/list-tools.md) - `POST` [Check a user's connected accounts for a server](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/check-connected-accounts-for-a-server.md) - `POST` [Mint a session token](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-session-token.md) - `POST` [Mint a session token for a connection](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-connection-session-token.md) --- Source: https://docs.scalekit.com/agentkit/mcp/read-only-tokens.md # Limit an agent to read-only tools Mint a read-only session token so an agent sees and calls only the tools that don't change anything, on a Virtual MCP server or a connection's MCP server. A read-only session token gives an agent only the tools that read data, such as listing, searching and fetching. Tools that send, create, update or delete are left out of the agent's tool list and refused if it calls them. You set this when you mint the token, for a [Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/overview/) or a [connection's MCP server](https://docs.scalekit.com/agentkit/mcp/connection-mcp-servers/), with no change to the server. ## Why use one - **Least privilege per run.** An agent that summarizes a user's inbox or answers questions about their CRM never needs to send an email or edit a record. A read-only token gives it only what the task needs. - **Prompt injection can't trigger a write.** Agents read content written by other people: emails, tickets, documents. If that content tells the agent to forward a thread or delete a file, the agent has no tool to do it with. Write tools aren't in its tool list, and a call to one is refused. - **One field, not a separate server.** You don't build and maintain a second, read-only server for each agent. The same server serves full and read-only runs, and the token decides. - **Different access per run.** Mint a read-only token for a scheduled digest and a full token for a run where the user has approved an action. Tokens are independent, so one run's access never changes another's. - **Same user isolation.** A read-only token is still bound to one user and one server, so the agent reads only that user's data, through their own connected accounts. ## How it works Tools are marked by what they do. On a connector's page, such as [Gmail](https://docs.scalekit.com/agentkit/connectors/gmail/), each tool is labeled **Read-only**, **Write** or **Destructive**. A tool without a read-only marking counts as a write. With a read-only token: | The agent | What happens | | --- | --- | | Lists tools (`tools/list`) | Only tools marked read-only are returned, plus `connect_account`, so the agent can still ask the user to connect an app. | | Calls a read-only tool | The call runs as the user, as with a full token. | | Calls any other tool | The call is refused with JSON-RPC error `-32602`, `unknown tool ""`. Nothing reaches the app. | On a Virtual MCP server, the token gets the read-only tools among the tools you chose for that server. On a connection's MCP server, it gets the read-only tools of that connection. Tools from an [MCP connector you bring](https://docs.scalekit.com/agentkit/bring-your-own-connector/overview/) are read-only when the upstream MCP server marks them that way, with `readOnlyHint`. A tool without that marking is left out of a read-only token. ## Before you start - A [Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/) and its ID, or a [connection's MCP server](https://docs.scalekit.com/agentkit/mcp/connection-mcp-servers/) and the connection name. - The user's `identifier` and a client set up as in [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/#before-you-start). 1. ## Mint a read-only token Add the access level when you mint the token. Use `READ_ONLY` in capitals: the value is case-sensitive. For a Virtual MCP server: **Python** ```python from datetime import timedelta session = actions.mcp.create_session_token( mcp_config_id=config_id, identifier="user_123", expiry=timedelta(minutes=15), access_level="READ_ONLY", ) session_token = session.token ``` **Node.js** ```ts const session = await scalekit.actions.mcp.createSessionToken({ mcpConfigId: configId, identifier: 'user_123', expirySeconds: 900, accessLevel: 'READ_ONLY', }); const sessionToken = session.token; ``` **cURL** ```bash SESSION_TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs/$CONFIG_ID/tokens" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"identifier": "user_123", "expiry": "900s", "access_level": "READ_ONLY"}' | jq -r .token) ``` For a connection's MCP server: **Python** ```python from datetime import timedelta session = actions.mcp.create_session_token( connection_name="gmail", identifier="user_123", expiry=timedelta(minutes=15), access_level="READ_ONLY", ) session_token = session.token ``` **Node.js** ```ts const session = await scalekit.actions.mcp.createSessionToken({ connectionName: 'gmail', identifier: 'user_123', expirySeconds: 900, accessLevel: 'READ_ONLY', }); const sessionToken = session.token; ``` **cURL** ```bash SESSION_TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/connections/gmail/tokens" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"identifier": "user_123", "expiry": "900s", "access_level": "READ_ONLY"}' | jq -r .token) ``` Leave the access level out, or set it to `FULL`, for a token with every tool the server serves. 2. ## Connect the agent Pass the token to the MCP client as `Authorization: Bearer `, exactly as with a full token. [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/#connect-the-mcp-client) shows the MCP SDK clients. ## Check it worked List the tools with the read-only token, then with a full one, and compare: ```bash curl -sS "$MCP_SERVER_URL" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' | sed -n 's/^data: //p' | jq -r '.result.tools[].name' ``` The read-only list has only tools that read, such as `gmail_fetch_mails`, plus `connect_account`. Tools such as `gmail_send_message` appear only with the full token. To see which tools a read-only token gets before you mint it, read each tool's `read_only_hint` from the tool list: **Python** ```python from google.protobuf.json_format import MessageToDict page, _ = actions.tools.list_scoped_tools( identifier="user_123", filter={"connection_names": ["gmail"]}, page_size=100, ) definitions = [MessageToDict(t.tool)["definition"] for t in page.tools] print([d["name"] for d in definitions if d.get("annotations", {}).get("read_only_hint") is True]) ``` **Node.js** ```ts const page = await scalekit.tools.listScopedTools('user_123', { filter: { connectionNames: ['gmail'] }, pageSize: 100, }); const readOnly = page.tools .map((t) => t.tool?.definition as { name?: string; annotations?: { read_only_hint?: boolean } } | undefined) .filter((d) => d?.annotations?.read_only_hint === true) .map((d) => d?.name); console.log(readOnly); ``` ## Common problems ### `unknown tool ""` when the agent calls a tool The tool isn't read-only, so a read-only token doesn't include it. If the agent needs it for this run, mint a full token. Otherwise, tell the agent in its instructions that it can only read. ### `400` with `access_level` when minting The value must be exactly `FULL` or `READ_ONLY`, in capitals. `read_only` and other spellings are rejected. ### `tools/list` returns only `connect_account` None of the tools on this server are read-only. On a Virtual MCP server, [add read-only tools](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/#change-or-remove-a-server), such as list and search tools, or mint a full token. ## Next - [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/): Decide when to mint for scheduled, concurrent and long-running agents. - [Security and compliance](https://docs.scalekit.com/agentkit/security/): What Scalekit protects, and what your app is responsible for. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `GET` [List a user's scoped tools](https://docs.scalekit.com/agentkit/reference/tools/list-scoped-tools.md) - `POST` [Mint a session token](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-session-token.md) - `POST` [Mint a session token for a connection](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-connection-session-token.md) --- Source: https://docs.scalekit.com/agentkit/mcp/connect-any-client.md # Connect any MCP client The HTTP contract of an AgentKit Virtual MCP server for clients that don't use the Scalekit SDK: URL, bearer token, transport, requests, responses, token lifetime and errors. A Virtual MCP server, or a [connection's MCP server](https://docs.scalekit.com/agentkit/mcp/connection-mcp-servers/), is a standard MCP endpoint, so any client that speaks streamable HTTP can call it: an agent runtime, a hosted platform, your own HTTP code or `curl`. Your backend needs the Scalekit API only to mint session tokens; the client never needs the SDK. This page is the contract that client works against. ## The contract | | | | --- | --- | | Endpoint | The server's `mcp_server_url`, from [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/), or a connection's MCP server URL, `/mcp/v3/connections/`. Send every request as `POST`. | | Authentication | `Authorization: Bearer `. A token is for one user on one server. [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/) shows how. | | Request headers | `Content-Type: application/json` and `Accept: application/json, text/event-stream` | | Transport | MCP streamable HTTP. Each response is a server-sent event stream with one `message` event, whose `data` line is the JSON-RPC response. | | Session | None to manage. `tools/list` and `tools/call` work without an `initialize` call first, and the server sends no `Mcp-Session-Id`. Clients that do send `initialize` and `notifications/initialized` get the normal responses. | | Methods | `initialize`, `tools/list` and `tools/call`. `GET` on the URL returns `405`: the server never opens a stream of its own. | | Tools | The tools you chose for the server, or every tool of the connection. A [read-only token](https://docs.scalekit.com/agentkit/mcp/read-only-tokens/) gets only the read-only ones. Also `connect_account`: when the user still needs to connect an app, it returns an authorization link for that connection. | | Token lifetime | 1 hour by default, from 60 seconds to 24 hours. A token can't be refreshed: mint a new one. Tokens are independent, so minting one doesn't revoke the others. | ## Call the server 1. ## Mint a session token Your backend mints the token for the user, as in [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/#mint-the-token), and hands the client the server URL and the token. Keep them in `MCP_SERVER_URL` and `SESSION_TOKEN`. 2. ## List the tools ```bash curl -sS "$MCP_SERVER_URL" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' ``` The response is one event. Its `data` line holds each tool's `name`, `description` and `inputSchema`: ```text event: message data: {"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"connect_account","description":"Connect (or RE-connect) the user's account…","inputSchema":{…}},{"name":"github_user_repos_list",…}]}} ``` 3. ## Call a tool Send the tool's `name` and `arguments` that match its `inputSchema`: ```bash curl -sS "$MCP_SERVER_URL" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "github_user_repos_list", "arguments": {"per_page": 5}} }' ``` The result is MCP text content. Its `text` is a JSON string, and the app's response is under `data`: ```text event: message data: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\n \"data\": [ … ]\n}"}]}} ``` To read it in a shell, keep the `data:` line and parse the text twice: ```bash … | sed -n 's/^data: //p' | jq -r '.result.content[0].text' | jq '.data' ``` ## Connect a user's account from the agent If a tool call fails because the user hasn't authorized that connection, or tools you expect are missing from `tools/list`, call `connect_account`: 1. Call it with no arguments. The response lists `connections_requiring_authorization` and `connections_already_authorized`, each with a status and a reason. Nothing is created, so this is always safe. 2. Call it again with `connection_name` set to one of the connections that needs authorization. The response has a time-limited `magic_link_url`. 3. Show the link to the user. The agent shouldn't open it itself. Once the user signs in, retry the tool call with the same token. A connection that uses an organization-wide credential has no user step: the response says `auth_mode` is `ORG_WIDE` and has no link. ## Renew the token The client can't renew a token. Before it expires, your backend mints a new one and the client switches to it. A host with an expired token fails at its next request, not at startup. [Choose when to mint](https://docs.scalekit.com/agentkit/mcp/session-tokens/#choose-when-to-mint) covers single runs, chat sessions, scheduled jobs, concurrent runs and long-running hosts. ## Errors A rejected token gets HTTP `401` with `{"error":"unauthorized","error_description":"invalid or expired bearer token"}`. Errors inside a request that got through come back as JSON-RPC errors in the `message` event. | What you see | Cause | Fix | | --- | --- | --- | | `401`, `invalid or expired bearer token` | No `Authorization` header, an expired token, a token minted for another server, or a token that was copied incompletely | Mint a new token for this server and user. | | `405` | A `GET` request | Send JSON-RPC requests as `POST`. | | JSON-RPC error `-32602` with `unknown tool` | The tool isn't on this server, the name is misspelled, or the token is read-only and the tool isn't | Use a name from `tools/list`. To add a tool, [update the server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/#change-or-remove-a-server). For a tool that writes, mint a full token. | | A tool call fails, or a connection's tools are missing | The user hasn't authorized that connection | Call `connect_account`, as above. | ## Next - [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/): Mint the token on your backend, and decide when to mint again. - [Choose a framework](https://docs.scalekit.com/agentkit/examples/): Set up CrewAI, Mastra or Claude Managed Agents against the server. --- Source: https://docs.scalekit.com/agentkit/triggers/overview.md # How triggers work How AgentKit triggers notify your app when something changes in a user's connected app: webhook and polling triggers, and how events are delivered. A **trigger** tells your app when something happens in an app your users have connected, such as a new lead in Salesforce, a new activity in Pipedrive or a message that mentions your app in Slack. Scalekit sends each event to your server as a signed HTTP request, so your agent can act on it without polling the app itself. This page explains the model: the two kinds of triggers, what you subscribe, and how events are delivered. To start receiving events, go to [Set up a trigger](https://docs.scalekit.com/agentkit/triggers/set-up/). > note: Triggers are in beta > > [Contact us](https://docs.scalekit.com/support/contact-us/) to turn triggers on for your environment. ## How it works You set triggers up in the dashboard, and your server receives the events: 1. **Add a delivery endpoint.** A URL on your server that receives trigger events. One endpoint can receive events from every connection in the environment, or only from the connections you choose. 2. **Subscribe.** On a [connection](https://docs.scalekit.com/agentkit/concepts/)'s **Triggers** tab, subscribe a connected account, or the whole connection, to a trigger such as **Lead created**. Each subscription can set filters, such as a lead source or a Slack channel, and for polling triggers how often to check. 3. **Scalekit detects the change.** It either receives the event from the app or finds it by checking the app on a schedule. Which one depends on the trigger: see [Two kinds of triggers](#two-kinds-of-triggers). 4. **Your server receives a signed request.** Scalekit sends one `POST` per event to each delivery endpoint that covers the connection. Your server verifies the signature, returns a `2xx` status and hands the event to your agent. ## Two kinds of triggers Each trigger is either a webhook trigger or a polling trigger. The **Triggers** tab tags each one **Webhook** or **Polled**, and shows whether you subscribe it **Per user** or **Per connection**. | | Webhook triggers | Polling triggers | | --- | --- | --- | | How Scalekit learns about a change | The app sends the event to Scalekit | Scalekit checks the app on an interval and sends one event per new or changed record | | How fast events arrive | Within seconds | Up to one interval after the change, plus a few seconds | | What you subscribe | Usually the whole connection, once | Each connected account, separately. The account must be `ACTIVE` | | Setup in the app | Register a Scalekit URL and copy the app's signing secret into Scalekit | None | | Examples | Slack **App mentioned**, **Message posted** | Salesforce **Lead created**, Pipedrive **Activity created or updated** | ### Webhook triggers The app pushes events to a webhook URL that Scalekit generates for the connection. You register that URL in your own app's settings at the provider and paste the provider's signing secret into Scalekit, so it can reject forged requests. Each connector's page shows where these settings are, as in [Slack triggers](https://docs.scalekit.com/agentkit/connectors/slack/#triggers). The provider sends every event your app at the provider subscribes to. Scalekit delivers only the events that match a trigger subscription and its filters, and drops the rest, so subscribe your app only to the events your triggers use. A connection-wide trigger fires once per event for the whole connection, not once per user. The event names the connection but no connected account. Your app decides which of its users, if any, should act on it. ### Polling triggers Scalekit checks the app for each subscribed connected account, using that account's access. Each trigger has its own default and minimum interval, which the dashboard shows when you subscribe. - **Polling starts when you subscribe.** Older records aren't sent. A record created or changed shortly before you subscribed can still arrive in the first check. - **One event per record per check.** A record that changes several times between two checks produces one event, not one per change. - **The interval can be longer than you asked for.** When an environment has many polling subscriptions, Scalekit can check less often to share capacity. The subscription then shows **Throttled**, and events still arrive. - **Polling pauses when the account's access stops working.** If a user revokes access or the token can't be refreshed, the subscription shows **Credential issue — paused**. It resumes on its own once the user reconnects the account. ## Delivery Every event reaches your endpoint as an HTTP `POST` with a JSON body, signed with the endpoint's signing secret. The body has the same shape for both kinds of triggers: which trigger fired, the connection, the connected account for per-user triggers, and the app's own record in `payload`. [Receive trigger events](https://docs.scalekit.com/agentkit/triggers/receive-events/) shows how to verify the request and explains each field. - **At least once.** An event can arrive more than once, for example after a retry. Each event carries a `dedupe_key`; skip one you've already handled. - **Retried until your server accepts it.** A delivery that doesn't get a `2xx` response is retried with increasing delays. - **Not in order.** Events can arrive out of order. Use `occurred_at` when order matters. ## Next - [Set up a trigger](https://docs.scalekit.com/agentkit/triggers/set-up/): Add a delivery endpoint and subscribe to a polling or webhook trigger. - [Receive trigger events](https://docs.scalekit.com/agentkit/triggers/receive-events/): Verify each request in Python or Node.js and handle repeats. --- Source: https://docs.scalekit.com/agentkit/triggers/set-up.md # Set up a trigger Add a delivery endpoint, then subscribe connected accounts to a polling trigger or register a webhook trigger with the app. Everything happens in the Scalekit dashboard. Set up a trigger in the Scalekit dashboard so your server receives an event each time something changes in a user's connected app. You add a delivery endpoint once per environment, then subscribe to triggers on each connection. Polling triggers and webhook triggers need different setup: follow the section for the kind you picked. New to triggers? [How triggers work](https://docs.scalekit.com/agentkit/triggers/overview/) explains the two kinds and how events are delivered. > note: Triggers are in beta > > [Contact us](https://docs.scalekit.com/support/contact-us/) to turn triggers on for your environment. ## Before you start - Triggers turned on for the environment. - A [connection](https://docs.scalekit.com/agentkit/connections/) for the app, such as Salesforce or Slack. - The **Admin** or **Developer** role. See [Team members and roles](https://docs.scalekit.com/agentkit/team-members/). - A server with a public HTTPS URL that accepts `POST` requests. While you develop, a tunnel to your local server works. - For a polling trigger: a connected account in `ACTIVE` status on the connection. [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) shows how to create one. - For a webhook trigger: access to your own app's settings at the provider, such as your Slack app. ## Add a delivery endpoint A delivery endpoint is the URL on your server where Scalekit sends trigger events. An endpoint can receive events from every connection in the environment, or only from the connections you choose, so you can send different apps' events to different endpoints. 1. In the Scalekit dashboard, select the environment, then open **Developers** > **Trigger Delivery** and select **Add endpoint**. 2. Enter your **Endpoint URL**, for example `https://example.com/scalekit/triggers`, and an optional **Description**. 3. Under **Receives events from**, choose **All connections**, or **Only these connections** and pick the connections. You can change this later without registering the endpoint again. 4. Select **Save endpoint**. 5. In the endpoints table, select **Reveal** under **Signing secret**, then copy the secret. It starts with `whsec_`. Store it with your other server secrets, for example as `SCALEKIT_TRIGGER_SECRET`, and never expose it to a browser. Each environment has its own endpoints and secrets. Add an endpoint in Production too when you go live. ## Open the connection's triggers Open **AgentKit** > **Connections**, select the connection, then open the **Triggers** tab. It lists the triggers the connector offers, each tagged: - **Webhook** or **Polled**: how Scalekit learns about changes. - **Per user** or **Per connection**: whether you subscribe each connected account or the connection once. Select **Show details** on a trigger to see **Requires**, the scopes the connection must grant for the trigger to work, and for a polling trigger **Cadence**, its default and minimum interval. Above the list, a setup card shows the steps this connection still needs, such as generating a webhook URL or connecting an account. Once they're done, it shrinks to a **Setup complete** line with a **Manage** button. > caution: Some triggers need your own app > > If the connection uses Scalekit's shared app credentials and its triggers need your own app, the setup card's first step asks you to use your own app. Add your app's credentials to the connection, as in [Bring your own credentials](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/), then come back to the **Triggers** tab. ## Subscribe to a polling trigger Polling triggers need no setup in the app. Subscribe each connected account whose changes you want. 1. On the **Triggers** tab, select **Subscribe** next to the trigger, such as **Lead created** in Salesforce or **Activity created or updated** in Pipedrive. 2. Pick the connected account. To subscribe several at once, select **Subscribe several accounts** and pick them. An account that can't receive the trigger, for example because it isn't `ACTIVE`, shows why and isn't added. 3. Optional: fill in the trigger's filters, such as a lead source. Leave a filter empty to receive every matching record. Each distinct set of filter values is a separate subscription. 4. Optional: set **Check interval (seconds)**. It defaults to the trigger's default interval, and an interval below the trigger's minimum is refused. 5. Select **Subscribe**. The first check runs shortly after you subscribe, then once per interval. Records created or changed before you subscribed aren't sent, except ones changed shortly before, which the first check can pick up. ## Set up a webhook trigger Webhook triggers need three things from your app at the provider: Scalekit's webhook URL registered as the event destination, the provider's signing secret saved in Scalekit, and a subscription to the provider's events the trigger uses. The setup card on the **Triggers** tab walks you through them in order. Each provider names these settings differently: the connector's page lists where they are, as in [Slack triggers](https://docs.scalekit.com/agentkit/connectors/slack/#triggers). 1. **Generate the webhook URL.** In the setup card, select **Generate webhook URL**. The URL is unique to this connection. 2. **Paste the provider's signing secret.** Copy the signing secret from your app's settings at the provider. In the setup card, select **Set signing secret**, paste it and select **Save**. Save the secret before you register the URL. Some providers check the URL as soon as you paste it, and Scalekit needs the secret to answer that check. 3. **Register the URL with the provider.** Copy the URL from the setup card and paste it into your app's event or webhook settings. If the provider checks the URL, Scalekit answers for you. Then subscribe your app at the provider to the events the trigger needs. The connector's page lists them. Subscribe only to those: events that match no trigger subscription aren't delivered to your endpoint. 4. **Confirm the registration.** Back in the setup card, select **I've registered it**. Scalekit can't see the provider's settings, so this step is marked done only when you confirm. 5. **Subscribe.** Select **Subscribe** next to the trigger. A **Per connection** trigger has no account to pick. Optional: fill in the trigger's filters, such as a channel, to receive only matching events. Then select **Subscribe**. Events from the provider now reach your delivery endpoint within seconds. ### Replace the provider's signing secret When you rotate the signing secret at the provider, update it in Scalekit without dropping events: 1. Open the connection's **Triggers** tab, select **Manage** on the **Setup complete** line, then select **Replace signing secret**. 2. Paste the new secret and select **Save**. Scalekit now accepts events signed with either the previous or the new secret. The card shows when the previous secret stops being accepted. 3. Update the secret at the provider. 4. Select **Confirm rotation**, and confirm. From then on, Scalekit accepts only the new secret, and rejects events signed with the previous one. ## Check it worked There's no test event to send, so make a real change: - **Polling trigger:** create or change a matching record in the app, such as a new lead in Salesforce or a new activity in Pipedrive, in the subscribed user's account. Wait up to one interval. - **Webhook trigger:** do what the trigger watches for in the app, such as mentioning your app in a Slack channel it's in. Then open **Developers** > **Trigger Delivery**. Under **Recent delivery attempts**, the event appears with your server's response code. A `2xx` code means your server accepted it. To handle the request in your server, see [Receive trigger events](https://docs.scalekit.com/agentkit/triggers/receive-events/). ## Common problems ### An account can't be subscribed The account picker shows why next to the account. The common reasons: - **The account's credential has expired**: ask the user to reconnect, as in [Handle inactive accounts](https://docs.scalekit.com/agentkit/connected-accounts/#handle-inactive-accounts), then subscribe again. - **The account hasn't finished connecting**: the user hasn't completed the authorization link. Wait until the account is `ACTIVE`. - **The account's grant doesn't cover a scope the trigger requires**: add the scopes listed under **Requires** to the connection, then ask the user to reconnect so they grant the new scopes. ### No events arrive - Check that an endpoint covers the connection: the setup card on the **Triggers** tab shows how many delivery endpoints cover it. An endpoint set to **Only these connections** must include this connection. - Check **Recent delivery attempts** on **Developers** > **Trigger Delivery**. A non-`2xx` code or **No response** means the request reached your server, or tried to, and failed there. - For a polling trigger, wait a full interval after the change, and check that the change matches the subscription's filters. - For a webhook trigger, check that the provider shows the URL as verified and that your app at the provider subscribes to the right events. ### The provider says the webhook URL didn't verify Save the provider's signing secret in Scalekit first, then register the URL again. Scalekit answers the provider's check only once it has the secret. ## Next - [Receive trigger events](https://docs.scalekit.com/agentkit/triggers/receive-events/): Verify each request in Python or Node.js, respond fast and handle repeats. - [Manage subscriptions](https://docs.scalekit.com/agentkit/triggers/manage/): Read subscription status, change endpoints and remove a subscription. --- Source: https://docs.scalekit.com/agentkit/triggers/receive-events.md # Receive trigger events Verify and parse AgentKit trigger events in Python or Node.js, respond fast, skip repeats with dedupe_key, and read each field of the event. Each time a trigger fires, Scalekit sends your delivery endpoint an HTTP `POST` with the event as JSON. Your server verifies the signature, returns a `2xx` status quickly and hands the event to your agent. This page shows a handler in Python and Node.js, how to handle repeats and retries, and what each field in the event means. > note: Triggers are in beta > > [Contact us](https://docs.scalekit.com/support/contact-us/) to turn triggers on for your environment. ## Before you start - A delivery endpoint and its signing secret, from [Set up a trigger](https://docs.scalekit.com/agentkit/triggers/set-up/#add-a-delivery-endpoint). The code below reads the secret from `SCALEKIT_TRIGGER_SECRET`. - Your Scalekit API credentials in `SCALEKIT_ENVIRONMENT_URL`, `SCALEKIT_CLIENT_ID` and `SCALEKIT_CLIENT_SECRET`, as in the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/). The SDK client verifies the signature. ## Verify and handle each request Scalekit signs every request with the endpoint's secret. Verify the signature against the raw request body before you trust the event, so a forged request can't make your agent act. The SDK checks the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers and rejects requests more than five minutes old. Then parse the body as JSON. **Python** Install the dependencies, then run the server with `uvicorn triggers:app --port 3000`: ```bash pip install scalekit-sdk-python fastapi uvicorn ``` ```python title="triggers.py" import json import os from fastapi import FastAPI, Request, Response from scalekit import ScalekitClient from scalekit.common.exceptions import WebhookVerificationError scalekit_client = ScalekitClient( os.environ["SCALEKIT_ENVIRONMENT_URL"], os.environ["SCALEKIT_CLIENT_ID"], os.environ["SCALEKIT_CLIENT_SECRET"], ) app = FastAPI() handled = set() # use your database in production @app.post("/scalekit/triggers") async def scalekit_trigger(request: Request): body = await request.body() # the exact bytes Scalekit signed try: scalekit_client.verify_webhook_payload( os.environ["SCALEKIT_TRIGGER_SECRET"], request.headers, body ) except WebhookVerificationError: # A bad or missing signature, or a request older than five minutes return Response(status_code=400) event = json.loads(body) # Retries repeat an event. Key on dedupe_key plus the account you act as. key = (event["dedupe_key"], event["connected_account_id"]) if key in handled: return Response(status_code=204) if event["delivery_scope"] == "account": print(f"{event['trigger_type']} for {event['connected_account_id']}: {event.get('resource_id')}") else: # A connection-wide event names no account: your app decides who acts on it print(f"{event['trigger_type']} on {event['connection_id']}: {event.get('resource_id')}") # Start your agent in a background job here, then respond handled.add(key) return Response(status_code=204) ``` **Node.js** Install the dependencies, then save the code as `triggers.ts` and run it with `npx tsx triggers.ts`: ```bash npm install @scalekit-sdk/node express npm install --save-dev tsx @types/express ``` ```ts title="triggers.ts" import express from 'express'; import { ScalekitClient, WebhookVerificationError } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const app = express(); const handled = new Set(); // use your database in production // express.text keeps the body as the exact text Scalekit signed app.post('/scalekit/triggers', express.text({ type: 'application/json' }), (req, res) => { const body: string = req.body; try { scalekit.verifyWebhookPayload( process.env.SCALEKIT_TRIGGER_SECRET!, req.headers as Record, body, ); } catch (err) { if (!(err instanceof WebhookVerificationError)) throw err; // A bad or missing signature, or a request older than five minutes return res.sendStatus(400); } const event = JSON.parse(body); // Retries repeat an event. Key on dedupe_key plus the account you act as. const key = `${event.dedupe_key}:${event.connected_account_id}`; if (handled.has(key)) return res.sendStatus(204); if (event.delivery_scope === 'account') { console.log(`${event.trigger_type} for ${event.connected_account_id}: ${event.resource_id}`); } else { // A connection-wide event names no account: your app decides who acts on it console.log(`${event.trigger_type} on ${event.connection_id}: ${event.resource_id}`); } // Start your agent in a background job here, then respond handled.add(key); res.sendStatus(204); }); app.listen(3000); ``` **Any language** Without a Scalekit SDK, verify the request yourself before you parse the body: 1. Read the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers. Reject the request if one is missing. 2. Reject the request if `webhook-timestamp`, in Unix seconds, is more than five minutes from your server's current time. 3. Remove the `whsec_` prefix from the signing secret and base64-decode the rest. That's the key. 4. Compute an HMAC-SHA256 with that key over `{webhook-id}.{webhook-timestamp}.{raw body}`, and base64-encode the result. 5. `webhook-signature` holds one or more space-separated signatures, each written `v1,`. Accept the request if any `v1` signature equals your result. Compare in constant time. For example, with only Python's standard library: ```python title="verify_signature.py" import base64 import hashlib import hmac import time def verify_signature(body: bytes, headers: dict, secret: str) -> bool: """headers: the request headers, with lowercase names.""" msg_id = headers.get("webhook-id") timestamp = headers.get("webhook-timestamp") signatures = headers.get("webhook-signature") if not (msg_id and timestamp and signatures and timestamp.isdigit()): return False if abs(time.time() - int(timestamp)) > 5 * 60: return False key = base64.b64decode(secret.removeprefix("whsec_")) signed = f"{msg_id}.{timestamp}.".encode() + body expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode() for candidate in signatures.split(" "): version, _, signature = candidate.partition(",") if version == "v1" and hmac.compare_digest(signature, expected): return True return False ``` After the signature checks out, parse the body as JSON and read the fields in [Event fields](#event-fields). The signature covers the body byte for byte. Verify the raw body exactly as received: parsing the JSON first and serializing it again changes the bytes and fails verification. ## Respond quickly and handle repeats - **Return a `2xx` status fast.** Start the agent's work after you respond or in a background job. A response other than `2xx`, or no response within 30 seconds, counts as a failure and is retried. - **Verify before you queue.** Verify the signature in the handler that receives the request, then queue the verified event. A queued request can be older than five minutes by the time a worker reads it, and the SDK rejects it. - **Expect the same event more than once.** Delivery is at least once. Scalekit sends an event again when it doesn't get a `2xx` from your endpoint within 30 seconds, including when your server handled the event but its response was slow or didn't arrive. Each repeat carries the same `dedupe_key`. Store the keys you've handled and skip a repeat. When one connection-wide event makes several of your users act, store the key together with each user's account, so a retry after a partial failure skips only the ones that finished. - **Don't rely on order.** Events can arrive out of order. Use `occurred_at` when order matters. ### Retries Scalekit makes up to 8 attempts to deliver an event, with increasing delays between them. Any status outside `2xx` is a failure, including `4xx`, and each attempt waits up to 30 seconds for a response. | Attempt | Delay after the previous attempt | | ------- | -------------------------------- | | 1 | Immediately | | 2 | 5 seconds | | 3 | 5 minutes | | 4 | 30 minutes | | 5 | 2 hours | | 6 | 5 hours | | 7 | 10 hours | | 8 | 10 hours | If the eighth attempt fails, about 27 hours after the first, the delivery is marked as failed. Each attempt and your server's response code appear under **Recent delivery attempts** on **Developers** > **Trigger Delivery**. ## Event fields Every event has the same shape, for webhook and polling triggers alike. This event came from a Salesforce **Lead created** subscription: ```json title="salesforce_lead_created" { "version": "1", "trigger_type": "salesforce_lead_created", "subscription_id": "trgs_141020933185734981", "delivery_scope": "account", "connection_id": "conn_133400101014995480", "connected_account_id": "ca_133400349586228019", "resource_type": "salesforce_lead_created", "resource_id": "00Q5g00000AbCdEFGH", "occurred_at": "2026-10-10T09:14:03Z", "detection_mode": "poll", "payload_state": "full", "payload": { "attributes": { "type": "Lead", "url": "/services/data/v66.0/sobjects/Lead/00Q5g00000AbCdEFGH" }, "Id": "00Q5g00000AbCdEFGH", "Name": "Jane Doe", "FirstName": "Jane", "LastName": "Doe", "Email": "jane@example.com", "Phone": null, "Company": "Example Corp", "Title": "VP Sales", "Status": "Open - Not Contacted", "LeadSource": "Web", "OwnerId": "0055g00000XyZabCDE", "CreatedDate": "2026-10-10T09:14:03.000+0000" }, "dedupe_key": "ca_5F133400349586228019.trgs_5F141020933185734981.00Q5g00000AbCdEFGH", "correlation_id": "ca_5F133400349586228019.trgs_5F141020933185734981.00Q5g00000AbCdEFGH" } ``` | Field | Meaning | | --- | --- | | `version` | The event format's version, `"1"` | | `trigger_type` | The trigger that fired, such as `salesforce_lead_created` | | `subscription_id` | The subscription that produced the event. Tells apart events from two subscriptions on the same account | | `delivery_scope` | `account`: the event is about one connected account. `connection`: the event is about the whole connection | | `connection_id` | The connection the event came from. Always present | | `connected_account_id` | The connected account, for `account` events. An empty string for `connection` events: branch on `delivery_scope`, not on this field | | `resource_type` | The kind of record that changed | | `resource_id` | The app's ID for the record, such as a Salesforce lead ID. Can be missing | | `occurred_at` | When the change happened in the app, in UTC (RFC 3339). Can be missing | | `detection_mode` | `poll` for a polling trigger, `webhook` for a webhook trigger | | `payload_state` | `full`: `payload` holds the record. `reference`: the record couldn't be included, for example because it's too large, and `payload` is `null`. Fetch the record from the app with `resource_id` | | `payload` | The app's own record or event, as its API returns it, with the app's field names | | `dedupe_key` | The same on every delivery of this event. Use it to skip repeats | | `correlation_id` | Carry it into the work the event starts, to trace that work back to the event | These fields are the same for every trigger. Only `payload` differs: it holds the app's own data, so its fields depend on the trigger. For a polling trigger it's the record, as above. For a webhook trigger it's the request body the provider sent. Each connector's page describes what its triggers send, as in [Slack triggers](https://docs.scalekit.com/agentkit/connectors/slack/#triggers). The fields keep these snake_case names in every language. `occurred_at` is an RFC 3339 string in UTC. - **Treat `dedupe_key` and `correlation_id` as opaque strings.** Compare them; don't parse them. Their format can change. - **Expect new fields and values.** Later versions can add fields and new values for `delivery_scope`, `detection_mode` and `payload_state`. Ignore fields you don't know, and keep a default branch for unknown values. - **Read `payload` with the app's field names.** It's the app's own data, not a Scalekit format, so its shape differs per connector and trigger. ## Check it worked Make a change the trigger watches for, as in [Set up a trigger](https://docs.scalekit.com/agentkit/triggers/set-up/#check-it-worked). Your server prints a line such as `salesforce_lead_created for ca_133400349586228019: 00Q5g00000AbCdEFGH`, and **Recent delivery attempts** on **Developers** > **Trigger Delivery** shows the attempt with status `204`. ## Common problems ### `Missing required headers` The `webhook-id`, `webhook-timestamp` and `webhook-signature` headers didn't reach the SDK. Check that a proxy in front of your server doesn't strip them, and pass the request's headers object unchanged. ### `Invalid signature` Node.js spells it `Invalid Signature`. The body you verified isn't byte for byte what Scalekit sent, or the secret belongs to another endpoint or environment. Verify the raw body before any JSON parsing, and copy the secret again with **Reveal** on **Developers** > **Trigger Delivery**. ### `Message timestamp too old` The request is more than five minutes old, or your server's clock is off. Sync the clock, and verify requests before you queue them. ### My endpoint returns `2xx` but the event is handled twice A retry or a second delivery repeated the event. Skip events whose `dedupe_key`, together with the account you act as, you've already handled. ## Next - [Manage subscriptions](https://docs.scalekit.com/agentkit/triggers/manage/): Read subscription status and intervals, and remove a subscription. - [Call tools with the SDK](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/): Let your agent act on the event with the user's connected account. --- Source: https://docs.scalekit.com/agentkit/triggers/manage.md # Manage trigger subscriptions Check each trigger subscription's status and polling interval, fix accounts that need attention, remove subscriptions and change delivery endpoints in the dashboard. After you [set up a trigger](https://docs.scalekit.com/agentkit/triggers/set-up/), check its subscriptions in the dashboard to see whether events are flowing, which accounts need attention and how often polling runs. This page covers subscription statuses, intervals, removing a subscription and changing delivery endpoints. > note: Triggers are in beta > > [Contact us](https://docs.scalekit.com/support/contact-us/) to turn triggers on for your environment. ## See a connection's subscriptions Open **AgentKit** > **Connections**, select the connection, then open the **Triggers** tab. A summary line above the list shows the number of triggers and subscriptions, and how many subscriptions need attention, or **all active**. Each trigger row shows its number of subscriptions. Select the count to open the trigger's subscriptions. Filter them by **All**, **Needs attention** or **Active**, or search by the account's identifier. Subscriptions that need attention are listed first. ## Subscription statuses | Status | Means | What to do | | --- | --- | --- | | **Active** | The subscription receives events. | Nothing. | | **Throttled** | A polling subscription receives events, but Scalekit checks less often than you asked, because the environment has many polling subscriptions. | Nothing, if the slower interval is acceptable. Otherwise, remove subscriptions you don't use, and subscribe with longer intervals where speed doesn't matter. | | **Credential issue — paused** | Polling stopped because the account's access no longer works, for example because the user revoked it. | Ask the user to reconnect the account. Polling resumes on its own, and the first check after it resumes covers the time it was paused. | | **Paused** | The subscription is stopped and receives no events. | Remove it and subscribe again to receive events. | | **Needs attention** | A status this page doesn't list. | Contact support with the subscription's trigger and account. | **Active** and **Throttled** subscriptions receive events; the others don't. The summary and the **Needs attention** filter include every subscription that isn't **Active**, so a **Throttled** subscription shows there too, even though its events still arrive. ## Polling intervals Each polling subscription shows **Checking every** and the interval Scalekit actually uses. That's the interval you set when you subscribed, or the trigger's default, unless the subscription is **Throttled**: then it's longer. The interval sets how long a change can take to reach you: an event arrives up to one interval after the change, plus a few seconds. Each trigger has a minimum interval, shown under **Cadence** in the trigger's details. ## Accounts that need attention A subscription can be **Active** while its connected account can't receive the trigger. The subscription then shows a warning tag, and it won't fire until the user reconnects the account through your app's connect flow: | Tag | Means | | --- | --- | | **Needs re-authentication** | The account's credential no longer works. Send the user a new authorization link. | | **Disconnected** | The account was disconnected. Reconnect it with a new authorization link. | | **Authorized under a different app** | The user authorized with different app credentials than the connection uses now, for example before you switched to your own app. Ask the user to reconnect. | | **Awaiting confirmation** | The account works, but Scalekit hasn't confirmed which workspace or user at the app it belongs to, so it can't route this trigger's events to it. Ask the user to reconnect. | [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) shows how to send a new authorization link. To get told when an account's status changes, listen for [account events](https://docs.scalekit.com/agentkit/account-events/). ## Remove a subscription Removing a subscription stops its events right away. It can't be undone: to receive the events again, subscribe again. 1. On the connection's **Triggers** tab, select the trigger's subscription count. 2. Select **Remove** next to the subscription. 3. Check the confirmation, which names the account or the whole connection and the subscription's filters, and confirm. ## Change delivery endpoints Open **Developers** > **Trigger Delivery** to see the environment's delivery endpoints, which connections each one receives events from and whether it's enabled. - **Edit an endpoint** to change its URL, description or the connections it receives events from. - **Delete an endpoint** to stop sending events to it. It stops right away, deliveries still in progress to it are lost, and it can't be undone. - **Reveal** and copy an endpoint's signing secret when you set up a new server. Under **Recent delivery attempts**, each attempt shows when it ran, the endpoint and your server's response code, or **No response**. A failed attempt is retried, so a newer attempt for the same event can still succeed. ## Next - [Receive trigger events](https://docs.scalekit.com/agentkit/triggers/receive-events/): Verify each request, handle repeats and read the event fields. - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and why accounts expire. --- Source: https://docs.scalekit.com/agentkit/agent-gateway/overview.md # What is Agent Gateway Agent Gateway gives your team one MCP URL for Claude, ChatGPT, Cursor and other AI tools, with each person signed in and access set per connection. **Agent Gateway gives your team's AI tools governed access to the apps your company uses.** You enable it in the Scalekit dashboard and get one MCP endpoint URL. Each person adds that URL to their AI client, such as Claude, ChatGPT, Cursor or VS Code, and signs in with their own account. The tools they see come from the connections you add, filtered by the groups they belong to. > note: Available on request > > Agent Gateway is available on request. [Contact us](https://docs.scalekit.com/support/contact-us/) to enable it for your > workspace. Agent Gateway is for the admins who give their own team access to AI tools, such as IT or security teams. Everything is set up in the dashboard, with no code. Building an agent into your own product for your customers? See [AgentKit](https://docs.scalekit.com/agentkit/overview/). ## Choose AgentKit or Agent Gateway Both products use the same [connectors](https://docs.scalekit.com/agentkit/connectors/). They differ in who builds the agent and who uses it. | | AgentKit | Agent Gateway | | --- | --- | --- | | You want to | Let **your product's** agent act in **your customers'** apps | Give **your team's** AI tools access to **your company's** apps | | Who sets it up | Developers, in code | Admins, in the dashboard | | Who signs in | Your customers, through your app | Your team, with their own accounts | | The agent | One you build and host | Existing AI clients, such as Claude, ChatGPT, Cursor or VS Code | | How tools are reached | SDK, REST API or a [Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/overview/) | One MCP endpoint URL for the whole team | | Access control | Your code chooses the tools for each call | Groups and per-connection policies | **Use AgentKit** when you're building a product, such as an inbox assistant or a sales copilot, and your users connect their accounts inside your app. Start with the [AgentKit quickstart](https://docs.scalekit.com/agentkit/quickstart/). **Use Agent Gateway** when people on your team already use AI tools and need them to read and act in company apps, with you deciding who can use which tools. Protecting an MCP server you built yourself? That's [MCP Auth](https://docs.scalekit.com/authenticate/mcp/quickstart/). ## How it works 1. **You enable the Gateway** in an environment. Scalekit creates its MCP endpoint and adds you as its first user. 2. **You add connections**, such as Gmail, Outlook, Slack or GitHub. These are the same [connections](https://docs.scalekit.com/agentkit/concepts/) AgentKit uses, set up once for the environment. 3. **Each person connects their AI client** to the MCP endpoint URL and signs in. The client asks for their consent the first time it acts for them. 4. **Each person connects their own app accounts.** The first time a tool needs an app the person hasn't connected, the AI client gives them a link to connect it. 5. **Groups decide which tools each person sees.** On each connection you choose which groups can use it, and whether they get every tool or only some. Tool calls through the Gateway appear under **Overview** and **Logs** in the dashboard. ## What you set up in the dashboard | Task | Where | Guide | | --------------------------------------- | ---------------------------------------- | ----------------------------------------------------- | | Enable the Gateway and get its URL | **Getting Started** | [Quickstart](https://docs.scalekit.com/agentkit/agent-gateway/quickstart/) | | Connect Claude, ChatGPT, Cursor and more | The URL from **Settings** | [Connect AI clients](https://docs.scalekit.com/agentkit/agent-gateway/connect-ai-clients/) | | Add users, SSO and SCIM | **Identity** | [Users and access](https://docs.scalekit.com/agentkit/agent-gateway/users-and-access/) | | Decide who can use which tools | **Groups**, and each connection's **Policies** tab | [Access policies](https://docs.scalekit.com/agentkit/agent-gateway/access-policies/) | | Review plans and limits | **Billing** | [Plans and limits](https://docs.scalekit.com/agentkit/agent-gateway/plans-and-limits/) | ## Next Enable the Gateway and connect your first AI client in the [Quickstart](https://docs.scalekit.com/agentkit/agent-gateway/quickstart/). --- Source: https://docs.scalekit.com/agentkit/agent-gateway/quickstart.md # Agent Gateway quickstart Enable Agent Gateway in the Scalekit dashboard, choose its connections, copy the MCP endpoint URL and connect your first AI client. In this quickstart you enable Agent Gateway in one environment, check which connections it serves and connect your first AI client. It takes a few minutes, and everything happens in the Scalekit dashboard. > note: Available on request > > Agent Gateway is available on request. [Contact us](https://docs.scalekit.com/support/contact-us/) to enable it for your > workspace. ## Before you start - **Agent Gateway enabled for your workspace.** [Contact us](https://docs.scalekit.com/support/contact-us/) if **Agent Gateway** isn't in the dashboard's product switcher. - **At least one connection** in the environment, such as Gmail or Outlook. You can also add one during setup. [Set up connections](https://docs.scalekit.com/agentkit/connections/) covers each connector's options. - **An AI client that supports remote MCP servers**, such as Claude, ChatGPT, Cursor or VS Code. 1. ## Open Agent Gateway In the Scalekit dashboard, open the environment that has your connections. Then open the product switcher at the top of the left navigation and choose **Agent Gateway**. Select **Getting Started** to open **Get started with Gateway**. 2. ## Enable the Gateway Select **Enable Gateway**. Scalekit creates the Gateway's MCP endpoint for this environment and adds you as its first user. 3. ## Add users This step is optional. To give teammates access now, add each person's email address. They get access straight away, with no invitation email. You can add people later under **Identity**. Select **Next** to continue. 4. ## Check your connections **Add connections** lists the environment's connections. Check that the apps you want are there, then select **Next**. To add an app, select **Add connection** and follow the connector's setup. Agents can only use tools from connections listed here. 5. ## Copy the MCP endpoint URL **Your Gateway is ready** shows the **MCP endpoint URL**. Select **Copy URL**. Under **Connect your agent**, pick your AI client to see how to add the URL to it. Some clients, such as Cursor, open with a single click. ## Check it worked 1. Add the MCP endpoint URL to your AI client, as in [Connect AI clients](https://docs.scalekit.com/agentkit/agent-gateway/connect-ai-clients/). 2. Sign in when the client opens the sign-in page, and approve the consent screen. 3. Ask the client to use one of your connections, for example "List my five most recent emails". If you haven't connected that app yet, the client gives you a link to connect it. Open it, sign in to the app and ask again. 4. In the dashboard, open **Logs**. The tool call appears there with its tool, connector and status. ## Next - [Connect AI clients](https://docs.scalekit.com/agentkit/agent-gateway/connect-ai-clients/): setup for Claude, ChatGPT, Cursor, VS Code, Claude Code, Windsurf and Grok Bot. - [Users and access](https://docs.scalekit.com/agentkit/agent-gateway/users-and-access/): add your team and sign them in with SSO. - [Access policies](https://docs.scalekit.com/agentkit/agent-gateway/access-policies/): choose which tools each group can use on each connection. --- Source: https://docs.scalekit.com/agentkit/agent-gateway/connect-ai-clients.md # Connect AI clients to Agent Gateway Add the Agent Gateway MCP endpoint URL to Claude, ChatGPT, Cursor, VS Code, Claude Code, Windsurf, Grok Bot or any MCP client, then sign in once. Each person on your team connects their own AI client to the Gateway's MCP endpoint URL, then signs in with their own account. This page shows how to add the URL to each client, and what happens the first time the client connects. > note: Available on request > > Agent Gateway is available on request. [Contact us](https://docs.scalekit.com/support/contact-us/) to enable it for your > workspace. ## Before you start - **The MCP endpoint URL.** Copy it from **Settings** in Agent Gateway, or from the last step of the [Quickstart](https://docs.scalekit.com/agentkit/agent-gateway/quickstart/). If your environment has a custom domain, the URL uses it. - **A Gateway user account** for the person connecting. You're added when you enable the Gateway. [Add more users](https://docs.scalekit.com/agentkit/agent-gateway/users-and-access/#add-users) under **Identity**. ## Add the URL to your client The dashboard shows the same steps under **Connect your agent** after setup. In the examples below, replace `` with your URL. ### Claude 1. Open Claude, then go to **Settings** > **Connectors**. 2. Select **Add custom connector**. 3. Paste the MCP endpoint URL and select **Add**. ### ChatGPT 1. Open ChatGPT, then go to **Settings** > **Apps** > **Advanced settings**, and turn on **Developer mode**. 2. Add a new connector and paste the MCP endpoint URL. ### Cursor In the dashboard, pick **Cursor** under **Connect your agent** and select **Open in Cursor**. Cursor opens and adds the Gateway as an MCP server named `gateway`. ### VS Code 1. Open the Command Palette: ⇧⌘P on macOS, Ctrl+Shift+P on Windows and Linux. 2. Run **MCP: Add Server**, choose **HTTP** and paste the MCP endpoint URL. 3. Name the server, for example `gateway`, and choose **Global** or **Workspace**. ### Claude Code Run this command in your terminal: ```bash claude mcp add gateway --transport http ``` ### Windsurf and other MCP clients Add the Gateway to the client's MCP server configuration as an HTTP server: ```json { "mcpServers": { "gateway": { "type": "http", "url": "" } } } ``` The client must support remote MCP servers that sign in with OAuth. ### Grok Bot 1. In a Grok Bot chat, ask it to add the Gateway as a custom server, for example: "Add a custom MCP server called gateway at ``". 2. Grok Bot shows a connect card. Select **Authorize** and sign in. ## Sign in the first time The first time a client connects, it opens a browser window: 1. **Sign in** with your Gateway user account. 2. **Approve the consent screen.** It lets the client act on your behalf through the Gateway. The client then lists the tools from the Gateway's connections that your groups allow. ## Connect your app accounts Each person connects their own account for each app, such as their own Gmail or Outlook. The first time a tool needs an app you haven't connected, the client returns a link instead of a result. Open the link, sign in to the app and approve access, then ask the client to try again. ## Review and revoke access Each user has their own account pages at `/ui/agents/get-started`. **Get started** there shows the MCP endpoint URL and the setup for each client, so you can send teammates that link. The same pages let each user review their own access: - **Connected apps** lists the apps you've connected. Select **Disconnect** to stop agents from using an app. An agent asks you to connect it again the next time it needs it. - **Authorized agents** lists the AI clients you've approved. Select **Revoke** to stop a client from getting new tokens. A token it already has keeps working until it expires. As an admin, you set how long access tokens last with **Access token lifetime** in **Settings**. ## Common problems ### The client can't register with the Gateway Most AI clients register themselves the first time they connect. In **Settings** > **Advanced configuration**, keep **Allow dynamic client registration** turned on. Some clients identify themselves with a metadata URL instead; for those, turn on **Allow Client ID Metadata Document (CIMD)**. ### The client connects but shows no tools, or fewer than expected Tools appear only for connections your groups can use. Open the connection under **Connections**, then its **Policies** tab, and select **Preview Access** to see which tools a user gets. See [Access policies](https://docs.scalekit.com/agentkit/agent-gateway/access-policies/). ### Requests return 404 The Gateway is disabled for the environment. Open **Settings** and select **Re-enable Gateway**. Users, registered clients and logs are kept while it's disabled. --- Source: https://docs.scalekit.com/agentkit/agent-gateway/users-and-access.md # Manage Agent Gateway users and access Add people to Agent Gateway, sign them in with SSO and SCIM, group them, and choose which connections and tools each group can use. Agent Gateway decides what each person's AI client can do in two steps: who can sign in, and which tools each group can use on each connection. You manage both in the dashboard, under **Identity**, **Groups** and each connection's **Policies** tab. > note: Available on request > > Agent Gateway is available on request. [Contact us](https://docs.scalekit.com/support/contact-us/) to enable it for your > workspace. ## Add users Only Gateway users can sign in to the Gateway. You become one when you enable it. To add someone else: 1. In Agent Gateway, open **Identity**, then the **Users** tab. 2. Select **Create User** and enter their email address. They get access straight away, with no invitation email. They can then [connect their AI client](https://docs.scalekit.com/agentkit/agent-gateway/connect-ai-clients/). ### Sign users in with your identity provider To manage users from your company's identity provider, such as Okta or Microsoft Entra ID, use the other two tabs in **Identity**: - **SSO**: users sign in to the Gateway through your identity provider. - **SCIM**: your identity provider syncs its users and groups to the Gateway. Synced groups appear under **Groups**. ## Group users and choose their tools **Groups** decide which connections and tools a person can use. Every user is in the built-in **Everyone** group. You can add manual groups, and groups synced from your identity provider through SCIM appear there too. On each connection's **Policies** tab, you grant groups all tools, only the tools you allow or all tools except the ones you block. [Access policies](https://docs.scalekit.com/agentkit/agent-gateway/access-policies/) walks through creating groups and grants with screenshots, explains how grants combine and has recipes for common setups. ## Next Create groups and choose which tools each one can use in [Access policies](https://docs.scalekit.com/agentkit/agent-gateway/access-policies/). --- Source: https://docs.scalekit.com/agentkit/agent-gateway/access-policies.md # Control which tools each group can use in Agent Gateway Create groups, grant them all tools, an allow-list or a deny-list on each connection, check what a user gets and follow recipes for common setups. This guide is for the admin who decides which tools people's AI clients can use through Agent Gateway. By the end, you'll have groups for your teams, a grant on each connection that says which tools each group gets, and a way to check what any user can use. > note: Available on request > > Agent Gateway is available on request. [Contact us](https://docs.scalekit.com/support/contact-us/) to enable it for your > workspace. ## Before you start - Agent Gateway is enabled for your environment, and you've added at least one connection. See the [Agent Gateway quickstart](https://docs.scalekit.com/agentkit/agent-gateway/quickstart/). - The people you want to group are Gateway users. See [Add users](https://docs.scalekit.com/agentkit/agent-gateway/users-and-access/#add-users). ## How access works Three things decide which tools a person sees in their AI client: | Piece | What it is | Where you set it | | --- | --- | --- | | **Group** | A set of Gateway users, such as `Sales` or `Contractors` | **Groups** | | **Grant** | Gives one group access to one connection, with all of its tools, only the tools you allow or all tools except the ones you block | The connection's **Policies** tab | | **Preview Access** | Shows the tools one user can use on a connection today | The connection's **Policies** tab | A person belongs to the built-in **Everyone** group, plus every directory and manual group they're a member of. On each connection, they get the tools from the grants of all their groups combined, minus any tool one of their groups blocks. ## Create groups 1. In Agent Gateway, open **Groups**. > Image: Gateway groups page listing the built-in Everyone group and two manual groups, Contractors and Sales, with their source and member count Each group has a source: | Source | Members | Editable | | --- | --- | --- | | **Built-in** | The **Everyone** group: every signed-in user | No | | **Directory** | A group synced from your identity provider through [SCIM](https://docs.scalekit.com/agentkit/agent-gateway/users-and-access/#sign-users-in-with-your-identity-provider) | No: change it in your identity provider | | **Manual** | The users you add | Yes | 2. Select **Create Manual Group**, enter a **Name**, such as `Sales`, and an optional **Description**, then select **Create Group**. > Image: Create manual group dialog with the name Sales and the description Account executives and sales engineers 3. To add people, open the group's **…** menu and select **Edit members**. Search for a user by email or name and select them. They're added straight away. To remove someone, select the delete icon next to their name. > Image: Sales group drawer with a search field for adding a member and one current member, alex@example.com To delete a manual group, open its **…** menu and select **Delete**. This removes its members, its grants and any tool settings tied to them, and can't be undone. ## Grant a group access to a connection 1. In Agent Gateway, open **Connections**, select the connection and open its **Policies** tab. The table lists each group with a grant and its access. > Image: Policies tab of a GitHub connection with two grants: Everyone with All tools and Contractors with a deny-list, plus Preview Access and Add Group buttons 2. Select **Add Group** and choose the group. 3. Choose its **Access**: | Access | The group can use | Use it to | | --- | --- | --- | | **All tools** | Every tool on the connection | Give a team the whole app | | **Only specific tools** | Only the tools you select (an allow-list) | Give the least access a team needs | | **All tools except…** | Every tool except the ones you select (a deny-list) | Keep a few risky tools away from a group, even if another of its groups allows them | 4. For an allow-list or a deny-list, select the tools. The count above the list shows how many you've selected. > Image: Edit Group Access drawer for the Sales group with Only specific tools selected and four Gmail read tools checked, 4 of 38 selected > Image: Add Group Access drawer for the Contractors group with All tools except selected and githubpat_repo_delete checked under Blocked tools 5. Select **Add Grant**. To change a grant, open its **…** menu, select **Edit**, change the access or the tools and select **Save Changes**. To remove it, select **Revoke** and confirm. Revoking removes the grant and its tool selection. > Image: Revoke Contractors confirmation: This removes the grant and any per-tool refinements tied to it, with Cancel and Revoke buttons ## How grants combine - **A connection with no grants is open to every user.** Once a connection has a grant, only the groups with a grant can use it. - **New connections start with `Everyone: All tools`.** A connection you add after enabling the Gateway gets that grant, so every user can use it until you edit or revoke it. - **A person gets the tools from all their groups' grants together.** If **Everyone** has **All tools**, each user has every tool their groups don't block. An allow-list for another group doesn't take tools away from its members. - **A blocked tool stays blocked.** If any of a person's groups has a deny-list that blocks a tool, they can't use it, even if **Everyone** or another group allows it. - **A tool missing from an allow-list isn't blocked.** Another of the person's groups can still grant it. ## Check what a user gets On the connection's **Policies** tab, select **Preview Access** and pick a user. You see the groups that contribute to their access and every tool they can use on this connection today. > Image: Preview Access for priya@example.com on a GitHub connection: contributing groups Everyone and Contractors, and 112 permitted tools A user with no permitted tools has no grant on this connection through any of their groups: > Image: Preview Access for priya@example.com on a Gmail connection: contributing groups Everyone and Contractors, 0 permitted tools, and No tools permitted under the current policy set After each change, check Preview Access for one person in each group the change affects. ## Recipes ### Limit a connection to one team For example, only the sales team should use Gmail. 1. On the Gmail connection's **Policies** tab, add a grant for `Sales` with **All tools**, or with **Only specific tools** for less. 2. Revoke the **Everyone** grant. 3. In **Preview Access**, check one member of `Sales`, who gets the tools, and one person outside it, who gets none. Add the `Sales` grant before you revoke **Everyone**. If you revoke first, the connection has no grants and is open to every user again. ### Keep contractors read-only on a shared app For example, everyone keeps full GitHub access, but contractors can only read. 1. On the GitHub connection, keep **Everyone: All tools**. 2. Add a grant for `Contractors` with **All tools except…** and select the tools that change something, such as `githubpat_repo_delete`, `githubpat_issue_create` and `githubpat_pull_request_merge`. 3. In **Preview Access**, check a contractor. The tools you blocked are missing, and everything else is there. An allow-list for `Contractors` wouldn't work here: contractors are also in **Everyone**, which already grants every tool. Only a deny-list takes tools away. ### Block one risky tool for everyone For example, nobody should delete repositories through an AI client. 1. On the connection's **Policies** tab, open the **Everyone** grant's **…** menu and select **Edit**. 2. Choose **All tools except…**, select the tool, such as `githubpat_repo_delete`, and select **Save Changes**. Every user loses the tool, whatever their other groups allow. ### Roll out a new connection to a pilot group A new connection starts with **Everyone: All tools**, so every user sees its tools as soon as you add it. 1. Right after you add the connection, add a grant for your pilot group, such as `Pilot`, then revoke the **Everyone** grant. 2. When the pilot ends, add **Everyone** back with **All tools**, or with a deny-list for the tools you want to keep restricted. ## Next See what each plan includes in [Plans and limits](https://docs.scalekit.com/agentkit/agent-gateway/plans-and-limits/). --- Source: https://docs.scalekit.com/agentkit/agent-gateway/plans-and-limits.md # Agent Gateway plans and limits What the Free, Growth and Enterprise plans of Agent Gateway include: users, tool calls, MCP connectors, access control, approvals, audit logs and support. Agent Gateway has three plans: Free, Growth and Enterprise. Each plan sets how many users, tool calls and MCP connectors you get, which access controls you can use and what support comes with it. See your workspace's plan and usage under **Billing** in Agent Gateway. > note: Available on request > > Agent Gateway is available on request. [Contact us](https://docs.scalekit.com/support/contact-us/) to enable it for your > workspace. ## Compare plans | | Free | Growth | Enterprise | | --- | --- | --- | --- | | Price | $0/month | $99/month | Custom | | Users | Up to 5, with no additional users | 5 included, then $10 per additional user per month | Custom | | Tool calls | 10,000 per month | 100,000 per month, then $0.50 per 1,000 | Custom | | MCP connectors | 5 | Unlimited, including custom MCP connectors | Unlimited, including custom MCP connectors | | Access control | Basic RBAC | Advanced RBAC and ABAC | Advanced RBAC and ABAC, plus custom policies such as approval chains | | Approvals for sensitive actions | — | Included | Included | | Identity provider integration (Okta, Microsoft Entra ID, Google Workspace) | — | Included | Included | | Audit log retention | 7 days | Contact us | Custom | | VPC or on-premises deployment | — | — | Included | | SIEM and SOC export | — | — | Included | | HIPAA BAA and custom DPA | — | — | Included | | Uptime and support SLA | — | — | 99.99% uptime SLA and a support SLA | | Support | Community and email | Private Slack | Dedicated rollout and a customer success manager | Every plan works with your team's existing AI clients. Prices and plan details are on the [Agent Gateway pricing page](https://www.scalekit.com/pricing/agent-gateway), which is the reference if anything here differs. ## Choose a plan - **Start on Free** to try the Gateway with a small team and up to five connectors. - **Move to Growth** when you need more users, tool calls or connectors, sign-in through your identity provider, or approvals. - **Talk to us about Enterprise** for custom policies, SIEM export, an uptime SLA, a HIPAA BAA or a self-hosted deployment. [Contact us](https://docs.scalekit.com/support/contact-us/). --- Source: https://docs.scalekit.com/agentkit/examples.md # Choose a framework Give your agent Gmail, Slack and other tools as each user: complete AgentKit examples for LangChain, LangGraph, OpenAI Agents SDK, Mastra and more. Each framework example builds a working agent that reads a user's Gmail inbox with Scalekit-authenticated tools. Claude Managed Agents also creates Google Calendar events, and Hermes works with GitHub, Gmail, Slack and other apps. ## No agent loop to build These platforms manage the agent harness for you. Pass a Scalekit skill or MCP URL, describe the task, and the platform handles tool discovery, execution, and session state. - [Claude Managed Agents](https://docs.scalekit.com/agentkit/examples/claude-managed-agents/): Anthropic runs the agent loop. Pass a Scalekit MCP URL, describe a task, and Claude handles tool discovery, execution, and retries. - [Hermes](https://docs.scalekit.com/agentkit/hermes/): Nous Research agent runtime. Install the Scalekit plugin and call a connection by name. - [Hermes example repo](https://github.com/scalekit-developers/hermes-agentkit-example): Companion repo for the Hermes how-to. Install hermes-delegated-auth and run one GitHub read as an opaque identifier. ## Build your own agent loop These integrations give you full control. Fetch Scalekit tool schemas, wire them into your framework, and run the tool-use loop yourself. - **[LangChain](https://docs.scalekit.com/agentkit/examples/langchain/)** (Python, SDK, native adapter): Scalekit returns native LangChain tool objects. No schema reshaping needed. - **[LangGraph](https://docs.scalekit.com/agentkit/examples/langgraph/)** (Python and Node.js, SDK or MCP): Wire Scalekit tools into a graph with a `ToolNode`. - **[Google ADK](https://docs.scalekit.com/agentkit/examples/google-adk/)** (Python, SDK, native adapter): Scalekit returns native ADK tool objects. No schema reshaping needed. - **[Anthropic](https://docs.scalekit.com/agentkit/examples/anthropic/)** (Python and Node.js, SDK, direct): Tool schemas use `input_schema`, which matches Anthropic's format exactly. - **[OpenAI](https://docs.scalekit.com/agentkit/examples/openai/)** (Python and Node.js, SDK, direct): Rename `input_schema` to `parameters` to match OpenAI's function format. - **[OpenAI Agents SDK](https://docs.scalekit.com/agentkit/examples/openai-agents-sdk/)** (Python and Node.js, SDK or MCP): Turn Scalekit tools into function tools, or connect a Virtual MCP server natively. - **[Vercel AI SDK](https://docs.scalekit.com/agentkit/examples/vercel-ai/)** (Node.js, SDK, `tool()` helper): Wrap tools with `tool()` and `jsonSchema()`. No manual schema conversion needed. - **[CrewAI](https://docs.scalekit.com/agentkit/examples/crewai/)** (Python, MCP): `MCPServerAdapter` connects to a Scalekit MCP URL. Tool discovery is automatic. - **[Mastra](https://docs.scalekit.com/agentkit/examples/mastra/)** (Node.js, MCP): Native MCP support via `@mastra/mcp`. Tool discovery is automatic. ## Working examples on GitHub ### [Connect LangChain agents to Gmail](https://github.com/scalekit-inc/sample-langchain-agent) Securely connect a LangChain agent to Gmail using Scalekit for authentication. Python example for tool authorization. ### [Connect Google GenAI agents to Gmail](https://github.com/scalekit-inc/google-adk-agent-example) Build a Google ADK agent that securely accesses Gmail tools. Python example demonstrating Scalekit auth integration. ### [Connect agents to Slack tools](https://github.com/scalekit-inc/python-connect-demos/tree/main/direct) Authorize Python agents to use Slack tools with Scalekit. Direct integration example for secure tool access. ### [Connect CrewAI agents to Gmail](https://github.com/scalekit-developers/crewai-scalekit-example) Multi-agent email triage crew using CrewAI with Scalekit-authenticated Gmail tools via MCP. ### [Meeting prep agent](https://github.com/scalekit-inc/meeting-prep-agent-example) Pulls context from Google Cal, Gmail, HubSpot, and Slack before each external meeting. Delivers a structured brief in under 60 seconds using delegated user identity. ### [Browse all agent auth examples](https://github.com/scalekit-developers/agent-auth-examples) A curated collection of working examples showing how to build agents that authenticate and access tools using Scalekit. --- Source: https://docs.scalekit.com/agentkit/examples/langchain.md # LangChain Build a LangChain agent with Scalekit-authenticated Gmail tools. Scalekit returns native LangChain tool objects; no schema reshaping needed. Build a LangChain agent that reads a user's Gmail inbox. Scalekit handles OAuth, token storage, and returns tools in native LangChain format. Your agent code needs no Scalekit-specific logic beyond initialization. Full code on GitHub ## Before you start - Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](https://docs.scalekit.com/agentkit/connections/). - Have the `.env` file from the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. - The samples load `.env` with `load_dotenv()` from `python-dotenv`. ## Install ```sh pip install scalekit-sdk-python python-dotenv langchain-openai ``` ## Initialize ```python import os import scalekit.client from dotenv import load_dotenv load_dotenv() scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) actions = scalekit_client.actions ``` ## Connect the user to Gmail ```python # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools connection_name = "gmail" identifier = "user_123" # your app's unique user ID response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=connection_name, identifier=identifier ) print("Authorize Gmail:", link.link) input("Press Enter after authorizing...") # Fetch the account again to pick up the new status response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": raise RuntimeError( f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." ) ``` See [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) for production auth handling. ## Build and run the agent `actions.langchain.get_tools()` returns native `StructuredTool` objects. Bind them to your LLM and run the tool-calling loop: ```python from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, ToolMessage tools = actions.langchain.get_tools( identifier="user_123", connection_names=["gmail"], page_size=100, # avoid missing tools when a connector has more than the default page ) tool_map = {t.name: t for t in tools} llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [HumanMessage("Fetch my last 5 unread emails and summarize them")] while True: response = llm.invoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: result = tool_map[tc["name"]].invoke(tc["args"]) messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"])) ``` > note: Multiple Gmail accounts > > If a user has multiple Gmail connections, pass the specific `connection_names` value from your Scalekit dashboard to scope tools to the right one. ## Use MCP instead LangChain connects to MCP servers with `langchain-mcp-adapters`. Pass the Virtual MCP server URL and a session token for this user: ```sh pip install "langchain-mcp-adapters>=0.3,<1" ``` ```python import asyncio import os from datetime import timedelta from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, ToolMessage # Returned by create_config when you created the Virtual MCP server config_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] mcp_url = os.environ["SCALEKIT_MCP_SERVER_URL"] # Mint a fresh session token for this user before each agent run mcp_token = actions.mcp.create_session_token( mcp_config_id=config_id, identifier="user_123", expiry=timedelta(hours=1), ).token async def run(): client = MultiServerMCPClient( { "scalekit": { "transport": "streamable_http", "url": mcp_url, "headers": {"Authorization": f"Bearer {mcp_token}"}, } } ) tools = await client.get_tools() tool_map = {t.name: t for t in tools} llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [HumanMessage("Fetch my last 5 unread emails and summarize them")] while True: response = await llm.ainvoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: result = await tool_map[tc["name"]].ainvoke(tc["args"]) messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"])) asyncio.run(run()) ``` See [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/) and [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/) to create the server, check the user's connections and mint a token. ## Next - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and ask users to reconnect. - [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to finish before real users connect. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Mint a session token](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-session-token.md) - `POST` [Mint a session token for a connection](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-connection-session-token.md) --- Source: https://docs.scalekit.com/agentkit/examples/langgraph.md # LangGraph Build a LangGraph agent that reads Gmail as the user with AgentKit: wire Scalekit tools into a ToolNode graph, or connect to a Virtual MCP server. Build a LangGraph agent that reads a user's Gmail inbox. The graph has two nodes: the model, and a `ToolNode` that runs the Scalekit tools the model asks for. In Python, Scalekit returns tools as LangChain tool objects. In Node.js, you wrap each tool with LangChain's `tool()` helper. Using LangChain's agent loop without a graph? See [LangChain](https://docs.scalekit.com/agentkit/examples/langchain/). ## Before you start - Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](https://docs.scalekit.com/agentkit/connections/). - Have the `.env` file from the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. - The samples load `.env` with `load_dotenv()` from `python-dotenv` in Python, and `import 'dotenv/config'` in Node.js. ## Install **Python** ```sh pip install scalekit-sdk-python python-dotenv langgraph langchain-openai ``` **Node.js** ```sh npm install @scalekit-sdk/node dotenv @langchain/langgraph @langchain/core @langchain/openai ``` For Node.js, save the code in a `.mts` file, such as `agent.mts`, and run it with `npx tsx agent.mts`. The `.mts` extension makes the file an ES module, so it can use top-level `await`. ## Initialize **Python** ```python import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() scalekit_client = ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) actions = scalekit_client.actions ``` **Node.js** ```typescript import 'dotenv/config'; import { ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); ``` Later blocks reuse this setup. ## Connect the user to Gmail **Python** ```python # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools connection_name = "gmail" identifier = "user_123" # your app's unique user ID response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=connection_name, identifier=identifier ) print("Authorize Gmail:", link.link) input("Press Enter after authorizing...") # Fetch the account again to pick up the new status response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": raise RuntimeError( f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." ) ``` **Node.js** ```typescript import { createInterface } from 'node:readline/promises'; import { ConnectorStatus } from '@scalekit-sdk/node'; // Connect the user's Gmail account, and wait until it's ACTIVE before calling tools const connectionName = 'gmail'; const identifier = 'user_123'; // your app's unique user ID let { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName, identifier, }); if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { const { link } = await scalekit.actions.getAuthorizationLink({ connectionName, identifier }); console.log('Authorize Gmail:', link); const rl = createInterface({ input: process.stdin, output: process.stdout }); await rl.question('Press Enter after authorizing...'); rl.close(); // Fetch the account again to pick up the new status ({ connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName, identifier, })); } if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { throw new Error(`Gmail is not ACTIVE (status ${connectedAccount?.status}). Authorize it and run again.`); } ``` See [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) for production auth handling. ## Build and run the graph `build_graph` (`buildGraph` in Node.js) takes a list of tools and returns a compiled graph. The model node calls the LLM. When the LLM asks for tools, `tools_condition` routes to the `ToolNode`, which runs them and returns their results to the model. When the LLM answers without tool calls, the graph ends. Fetch the tools scoped to this user and run the graph. [Use built-in tools](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/) explains the scoped tool list, paging past 100 tools, and how to read `data` when the app returns an error. **Python** ```python from langchain_openai import ChatOpenAI from langgraph.graph import START, MessagesState, StateGraph from langgraph.prebuilt import ToolNode, tools_condition def build_graph(tools): llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) def call_model(state: MessagesState): return {"messages": [llm.invoke(state["messages"])]} builder = StateGraph(MessagesState) builder.add_node("agent", call_model) builder.add_node("tools", ToolNode(tools)) builder.add_edge(START, "agent") # Run the tools the model asked for, or end when it answers without tool calls builder.add_conditional_edges("agent", tools_condition) builder.add_edge("tools", "agent") return builder.compile() tools = actions.langchain.get_tools( identifier=identifier, connection_names=[connection_name], page_size=100, # up to 100 tools per page; page with page_token for more ) graph = build_graph(tools) result = graph.invoke( {"messages": [("user", "Fetch my last 5 unread emails and summarize them")]} ) print(result["messages"][-1].content) ``` **Node.js** ```typescript import { ChatOpenAI } from '@langchain/openai'; import { tool, type StructuredToolInterface } from '@langchain/core/tools'; import { MessagesAnnotation, START, StateGraph } from '@langchain/langgraph'; import { ToolNode, toolsCondition } from '@langchain/langgraph/prebuilt'; function buildGraph(tools: StructuredToolInterface[]) { const llm = new ChatOpenAI({ model: 'gpt-4o' }).bindTools(tools); return new StateGraph(MessagesAnnotation) .addNode('agent', async (state) => ({ messages: [await llm.invoke(state.messages)] })) .addNode('tools', new ToolNode(tools)) .addEdge(START, 'agent') // Run the tools the model asked for, or end when it answers without tool calls .addConditionalEdges('agent', toolsCondition) .addEdge('tools', 'agent') .compile(); } const { tools: scopedTools } = await scalekit.tools.listScopedTools(identifier, { filter: { connectionNames: [connectionName] }, pageSize: 100, // up to 100 tools per page; page with nextPageToken for more }); // Tool definitions are typed as generic JSON, so read each field explicitly const definitions = scopedTools.flatMap((t) => (t.tool?.definition ? [t.tool.definition] : [])); const tools = definitions.map((definition) => tool( async (input) => { const result = await scalekit.actions.executeTool({ toolName: String(definition.name), identifier, connector: connectionName, toolInput: input as Record, }); return JSON.stringify(result.data); }, { name: String(definition.name), description: String(definition.description ?? ''), schema: definition.input_schema as Record, }, ), ); const graph = buildGraph(tools); const result = await graph.invoke({ messages: [{ role: 'user', content: 'Fetch my last 5 unread emails and summarize them' }], }); console.log(result.messages.at(-1)?.content); ``` When the app itself rejects a request, the tool doesn't raise: the app's error response comes back as the tool's result, and the model reads it like any other result. If a tool raises an error instead, such as a network failure, the two languages differ: - **Node.js:** `ToolNode` returns the error to the model. - **Python:** `ToolNode` returns only invalid-argument errors to the model, and raises the rest. To return every error to the model, create the node with `ToolNode(tools, handle_tool_errors=True)`. ## Check it worked The script prints a summary of the user's unread emails. `result["messages"]` (`result.messages` in Node.js) holds the whole run: the user message, the model's tool calls, a tool message with each Gmail response, and the final answer. ## Use MCP instead To give the graph the tools of a [Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/overview/), load them with LangChain's MCP adapters and pass them to the same `build_graph`. Pass the server URL and a session token for this user. **Python** ```sh pip install "langchain-mcp-adapters>=0.3,<1" ``` ```python import asyncio import os from datetime import timedelta from langchain_mcp_adapters.client import MultiServerMCPClient # Returned by create_config when you created the Virtual MCP server config_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] mcp_url = os.environ["SCALEKIT_MCP_SERVER_URL"] # Mint a fresh session token for this user before each agent run mcp_token = actions.mcp.create_session_token( mcp_config_id=config_id, identifier=identifier, expiry=timedelta(hours=1), ).token async def run(): client = MultiServerMCPClient( { "scalekit": { "transport": "streamable_http", "url": mcp_url, "headers": {"Authorization": f"Bearer {mcp_token}"}, } } ) graph = build_graph(await client.get_tools()) result = await graph.ainvoke( {"messages": [("user", "Fetch my last 5 unread emails and summarize them")]} ) print(result["messages"][-1].content) asyncio.run(run()) ``` **Node.js** ```sh npm install @langchain/mcp-adapters ``` ```typescript import { MultiServerMCPClient } from '@langchain/mcp-adapters'; // Returned by createConfig when you created the Virtual MCP server const configId = process.env.SCALEKIT_MCP_CONFIG_ID!; const mcpUrl = process.env.SCALEKIT_MCP_SERVER_URL!; // Mint a fresh session token for this user before each agent run const session = await scalekit.actions.mcp.createSessionToken({ mcpConfigId: configId, identifier, expirySeconds: 60 * 60, }); const mcpClient = new MultiServerMCPClient({ servers: { scalekit: { transport: 'http', url: mcpUrl, headers: { Authorization: `Bearer ${session.token}` }, }, }, }); try { const mcpGraph = buildGraph(await mcpClient.getTools()); const mcpResult = await mcpGraph.invoke({ messages: [{ role: 'user', content: 'Fetch my last 5 unread emails and summarize them' }], }); console.log(mcpResult.messages.at(-1)?.content); } finally { await mcpClient.close(); } ``` See [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/) and [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/) to create the server, check the user's connections and mint a token. ## Next - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and ask users to reconnect. - [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to finish before real users connect. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Execute a tool](https://docs.scalekit.com/agentkit/reference/tools/execute-a-tool.md) - `GET` [List a user's scoped tools](https://docs.scalekit.com/agentkit/reference/tools/list-scoped-tools.md) - `POST` [Mint a session token](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-session-token.md) - `POST` [Mint a session token for a connection](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-connection-session-token.md) --- Source: https://docs.scalekit.com/agentkit/examples/google-adk.md # Google ADK Build a Google ADK agent with Scalekit-authenticated Gmail tools. Scalekit returns native ADK tool objects; no schema reshaping needed. Build a Google ADK agent that reads a user's Gmail inbox. Scalekit handles OAuth, token storage, and returns tools as native ADK tool objects compatible with any ADK agent. Full code on GitHub ## Before you start - Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](https://docs.scalekit.com/agentkit/connections/). - Have the `.env` file from the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/), and add your Gemini API key as `GOOGLE_API_KEY` to it. - The samples load `.env` with `load_dotenv()` from `python-dotenv`. ## Install ```sh pip install scalekit-sdk-python python-dotenv "google-adk>=2,<3" ``` ## Initialize ```python import os import asyncio import scalekit.client from dotenv import load_dotenv load_dotenv() scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) actions = scalekit_client.actions ``` ## Connect the user to Gmail ```python # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools connection_name = "gmail" identifier = "user_123" # your app's unique user ID response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=connection_name, identifier=identifier ) print("Authorize Gmail:", link.link) input("Press Enter after authorizing...") # Fetch the account again to pick up the new status response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": raise RuntimeError( f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." ) ``` See [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) for production auth handling. ## Build and run the agent `actions.google.get_tools()` returns native ADK tool objects. Pass them directly to a Google ADK `Agent`: ```python from google.adk.agents import Agent from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from google.genai import types tools = actions.google.get_tools( identifier="user_123", connection_names=["gmail"], page_size=100, # avoid missing tools when a connector has more than the default page ) agent = Agent( name="gmail_assistant", model="gemini-2.0-flash", instruction="You are a helpful Gmail assistant.", tools=tools, ) async def main(): session_service = InMemorySessionService() runner = Runner(agent=agent, app_name="gmail_app", session_service=session_service) session = await session_service.create_session(app_name="gmail_app", user_id="user_123") message = types.Content( role="user", parts=[types.Part(text="Fetch my last 5 unread emails and summarize them")], ) async for event in runner.run_async( user_id="user_123", session_id=session.id, new_message=message, ): if event.is_final_response() and event.content and event.content.parts: print(event.content.parts[0].text) asyncio.run(main()) ``` > note: Multiple Gmail accounts > > If a user has multiple Gmail connections, pass the specific `connection_names` value from your Scalekit dashboard to scope tools to the right one. ## Use MCP instead Google ADK connects to MCP servers with `McpToolset`. Pass the Virtual MCP server URL and a session token for this user: ```python import os from datetime import timedelta from google.adk.agents import Agent from google.adk.tools.mcp_tool import McpToolset, StreamableHTTPConnectionParams # Returned by create_config when you created the Virtual MCP server config_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] mcp_url = os.environ["SCALEKIT_MCP_SERVER_URL"] # Mint a fresh session token for this user before each agent run mcp_token = actions.mcp.create_session_token( mcp_config_id=config_id, identifier="user_123", expiry=timedelta(hours=1), ).token agent = Agent( name="gmail_assistant", model="gemini-2.0-flash", instruction="You are a helpful Gmail assistant.", tools=[ McpToolset( connection_params=StreamableHTTPConnectionParams( url=mcp_url, headers={"Authorization": f"Bearer {mcp_token}"}, ) ) ], ) ``` Run this agent with the same `Runner` loop as above. See [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/) and [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/) to create the server, check the user's connections and mint a token. ## Next - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and ask users to reconnect. - [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to finish before real users connect. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Mint a session token](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-session-token.md) - `POST` [Mint a session token for a connection](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-connection-session-token.md) --- Source: https://docs.scalekit.com/agentkit/examples/openai.md # OpenAI Build an OpenAI agent with Scalekit-authenticated tools. Convert Scalekit's tool schemas to OpenAI's function calling format in one step. Build an agent using OpenAI's GPT models that reads a user's Gmail inbox. Scalekit's tool schemas use `input_schema`: rename it to `parameters` and wrap it in OpenAI's function format. ## Before you start - Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](https://docs.scalekit.com/agentkit/connections/). - Have the `.env` file from the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. - The samples load `.env` with `load_dotenv()` from `python-dotenv` in Python, and `import 'dotenv/config'` in Node.js. ## Install **Python** ```sh pip install scalekit-sdk-python python-dotenv openai ``` **Node.js** ```sh npm install @scalekit-sdk/node openai dotenv ``` For Node.js, save the code in a `.mts` file, such as `agent.mts`, and run it with `npx tsx agent.mts`. The `.mts` extension makes the file an ES module, so it can use top-level `await`. ## Initialize **Python** ```python import os, json import scalekit.client from dotenv import load_dotenv from openai import OpenAI from google.protobuf.json_format import MessageToDict load_dotenv() scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) actions = scalekit_client.actions client = OpenAI() ``` **Node.js** ```typescript import 'dotenv/config'; import { ScalekitClient } from '@scalekit-sdk/node'; import OpenAI from 'openai'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const openai = new OpenAI(); ``` ## Connect the user to Gmail **Python** ```python # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools connection_name = "gmail" identifier = "user_123" # your app's unique user ID response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=connection_name, identifier=identifier ) print("Authorize Gmail:", link.link) input("Press Enter after authorizing...") # Fetch the account again to pick up the new status response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": raise RuntimeError( f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." ) ``` **Node.js** ```typescript import { createInterface } from 'node:readline/promises'; import { ConnectorStatus } from '@scalekit-sdk/node'; // Connect the user's Gmail account, and wait until it's ACTIVE before calling tools const connectionName = 'gmail'; const identifier = 'user_123'; // your app's unique user ID let { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName, identifier, }); if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { const { link } = await scalekit.actions.getAuthorizationLink({ connectionName, identifier }); console.log('Authorize Gmail:', link); const rl = createInterface({ input: process.stdin, output: process.stdout }); await rl.question('Press Enter after authorizing...'); rl.close(); // Fetch the account again to pick up the new status ({ connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName, identifier, })); } if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { throw new Error(`Gmail is not ACTIVE (status ${connectedAccount?.status}). Authorize it and run again.`); } ``` See [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) for production auth handling. ## Run the agent Fetch tools scoped to this user, convert to OpenAI's function format, then run the tool-calling loop. [Use built-in tools](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/) explains the scoped tool list, paging past 100 tools, and how to read `data` when the app returns an error. **Python** ```python # Fetch and convert tools to OpenAI format scoped_response, _ = actions.tools.list_scoped_tools( identifier="user_123", filter={"connection_names": ["gmail"]}, page_size=100, # up to 100 tools per page; page with next_page_token for more ) llm_tools = [ { "type": "function", "function": { "name": MessageToDict(t.tool).get("definition", {}).get("name"), "description": MessageToDict(t.tool).get("definition", {}).get("description", ""), "parameters": MessageToDict(t.tool).get("definition", {}).get("input_schema", {}), }, } for t in scoped_response.tools ] # Run the agent loop messages = [{"role": "user", "content": "Fetch my last 5 unread emails and summarize them"}] while True: response = client.chat.completions.create( model="gpt-4o", tools=llm_tools, messages=messages, ) message = response.choices[0].message if not message.tool_calls: print(message.content) break messages.append(message) for tc in message.tool_calls: result = actions.execute_tool( tool_name=tc.function.name, identifier="user_123", connection_name="gmail", tool_input=json.loads(tc.function.arguments), ) messages.append({ "role": "tool", "tool_call_id": tc.id, "content": str(result.data), }) ``` **Node.js** ```typescript // Fetch and convert tools to OpenAI format const { tools } = await scalekit.tools.listScopedTools('user_123', { filter: { connectionNames: ['gmail'] }, pageSize: 100, // up to 100 tools per page; page with nextPageToken for more }); // Tool definitions are typed as generic JSON, so read each field explicitly const definitions = tools.flatMap((t) => (t.tool?.definition ? [t.tool.definition] : [])); const llmTools: OpenAI.ChatCompletionTool[] = definitions.map((definition) => ({ type: 'function', function: { name: String(definition.name), description: String(definition.description ?? ''), parameters: definition.input_schema as OpenAI.FunctionParameters, }, })); // Run the agent loop const messages: OpenAI.ChatCompletionMessageParam[] = [ { role: 'user', content: 'Fetch my last 5 unread emails and summarize them' }, ]; while (true) { const response = await openai.chat.completions.create({ model: 'gpt-4o', tools: llmTools, messages, }); const message = response.choices[0].message; if (!message.tool_calls?.length) { console.log(message.content); break; } messages.push(message); for (const tc of message.tool_calls) { if (tc.type !== 'function') continue; // only function tools are registered const result = await scalekit.actions.executeTool({ toolName: tc.function.name, identifier: 'user_123', connector: 'gmail', toolInput: JSON.parse(tc.function.arguments), }); messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result.data) }); } } ``` ## Use the Responses API OpenAI's [Responses API](https://platform.openai.com/docs/api-reference/responses) is a stateful alternative to Chat Completions. Instead of managing conversation history yourself, you pass `previous_response_id` to continue a session. It takes tools in a flat shape, `{type, name, description, parameters}`, without the `function` wrapper that Chat Completions uses, so convert the tools first. > note: Using a proxy or gateway > > The Responses API works with an OpenAI API key. Many OpenAI-compatible proxies and gateways support only Chat Completions, so check yours before you switch, or use the Chat Completions loop above. **Python** ```python # Responses API tools are flat: no "function" wrapper. # strict=False because Scalekit schemas don't follow strict mode's rules. response_tools = [{"type": "function", **t["function"], "strict": False} for t in llm_tools] response = client.responses.create( model="gpt-4o", input="Fetch my last 5 unread emails and summarize them", tools=response_tools, ) while any(item.type == "function_call" for item in response.output): tool_results = [ { "type": "function_call_output", "call_id": item.call_id, "output": str(actions.execute_tool( tool_name=item.name, identifier="user_123", connection_name="gmail", tool_input=json.loads(item.arguments), ).data), } for item in response.output if item.type == "function_call" ] response = client.responses.create( model="gpt-4o", previous_response_id=response.id, input=tool_results, tools=response_tools, ) for item in response.output: if item.type == "message": print(item.content[0].text) ``` **Node.js** ```typescript // Responses API tools are flat: no `function` wrapper. // strict: false because Scalekit schemas don't follow strict mode's rules. const responseTools: OpenAI.Responses.FunctionTool[] = definitions.map((definition) => ({ type: 'function', name: String(definition.name), description: String(definition.description ?? ''), parameters: definition.input_schema as Record, strict: false, })); let response = await openai.responses.create({ model: 'gpt-4o', input: 'Fetch my last 5 unread emails and summarize them', tools: responseTools, }); while (response.output.some(item => item.type === 'function_call')) { const toolResults = await Promise.all( response.output .filter(item => item.type === 'function_call') .map(async item => { const result = await scalekit.actions.executeTool({ toolName: item.name, identifier: 'user_123', connector: 'gmail', toolInput: JSON.parse(item.arguments), }); return { type: 'function_call_output' as const, call_id: item.call_id, output: JSON.stringify(result.data), }; }) ); response = await openai.responses.create({ model: 'gpt-4o', previous_response_id: response.id, input: toolResults, tools: responseTools, }); } const message = response.output.find(item => item.type === 'message'); if (message?.type === 'message') { for (const part of message.content) { if (part.type === 'output_text') console.log(part.text); } } ``` ## Use MCP instead If you prefer the MCP approach, connect your OpenAI agent via the [Vercel AI SDK + MCP](https://docs.scalekit.com/agentkit/examples/vercel-ai#use-mcp-instead) or LangChain's MCP client with a Scalekit-generated URL. See [Virtual MCP servers](https://docs.scalekit.com/agentkit/mcp/overview/) for the URL setup. ## Next - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and ask users to reconnect. - [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to finish before real users connect. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Execute a tool](https://docs.scalekit.com/agentkit/reference/tools/execute-a-tool.md) - `GET` [List a user's scoped tools](https://docs.scalekit.com/agentkit/reference/tools/list-scoped-tools.md) --- Source: https://docs.scalekit.com/agentkit/examples/openai-agents-sdk.md # OpenAI Agents SDK Build an OpenAI Agents SDK agent that reads Gmail as the user with AgentKit: turn Scalekit tools into function tools, or connect a Virtual MCP server. Build an agent with the [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) that reads a user's Gmail inbox. Each Scalekit tool becomes an Agents SDK function tool that runs the call through Scalekit as the user, and the SDK's runner handles the tool-calling loop. Calling the Chat Completions or Responses API directly instead? See [OpenAI](https://docs.scalekit.com/agentkit/examples/openai/). ## Before you start - Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](https://docs.scalekit.com/agentkit/connections/). - Have the `.env` file from the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. - The samples load `.env` with `load_dotenv()` from `python-dotenv` in Python, and `import 'dotenv/config'` in Node.js. ## Install **Python** ```sh pip install scalekit-sdk-python python-dotenv openai-agents ``` **Node.js** ```sh npm install @scalekit-sdk/node dotenv @openai/agents ``` For Node.js, save the code in a `.mts` file, such as `agent.mts`, and run it with `npx tsx agent.mts`. The `.mts` extension makes the file an ES module, so it can use top-level `await`. ## Initialize **Python** ```python import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() scalekit_client = ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) actions = scalekit_client.actions ``` **Node.js** ```typescript import 'dotenv/config'; import { ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); ``` Later blocks reuse this setup. ## Connect the user to Gmail **Python** ```python # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools connection_name = "gmail" identifier = "user_123" # your app's unique user ID response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=connection_name, identifier=identifier ) print("Authorize Gmail:", link.link) input("Press Enter after authorizing...") # Fetch the account again to pick up the new status response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": raise RuntimeError( f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." ) ``` **Node.js** ```typescript import { createInterface } from 'node:readline/promises'; import { ConnectorStatus } from '@scalekit-sdk/node'; // Connect the user's Gmail account, and wait until it's ACTIVE before calling tools const connectionName = 'gmail'; const identifier = 'user_123'; // your app's unique user ID let { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName, identifier, }); if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { const { link } = await scalekit.actions.getAuthorizationLink({ connectionName, identifier }); console.log('Authorize Gmail:', link); const rl = createInterface({ input: process.stdin, output: process.stdout }); await rl.question('Press Enter after authorizing...'); rl.close(); // Fetch the account again to pick up the new status ({ connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName, identifier, })); } if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { throw new Error(`Gmail is not ACTIVE (status ${connectedAccount?.status}). Authorize it and run again.`); } ``` See [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) for production auth handling. ## Run the agent Fetch the tools scoped to this user and turn each one into a function tool. The tool's JSON Schema is passed through as is, with strict mode off. When the model calls a tool, the function runs it with `execute_tool` (`executeTool` in Node.js) for this user and returns the app's response. [Use built-in tools](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/) explains the scoped tool list, paging past 100 tools, and how to read `data` when the app returns an error. **Python** ```python import asyncio import json from agents import Agent, FunctionTool, Runner from google.protobuf.json_format import MessageToDict scoped_response, _ = actions.tools.list_scoped_tools( identifier=identifier, filter={"connection_names": [connection_name]}, page_size=100, # up to 100 tools per page; page with next_page_token for more ) definitions = [MessageToDict(t.tool).get("definition", {}) for t in scoped_response.tools] def scalekit_tool(definition): async def run_tool(ctx, args_json: str) -> str: # execute_tool is a blocking call, so run it off the event loop result = await asyncio.to_thread( actions.execute_tool, tool_name=definition["name"], identifier=identifier, connection_name=connection_name, tool_input=json.loads(args_json or "{}"), ) return json.dumps(result.data) return FunctionTool( name=definition["name"], description=definition.get("description", ""), params_json_schema=definition.get("input_schema", {"type": "object", "properties": {}}), on_invoke_tool=run_tool, strict_json_schema=False, # Scalekit schemas don't follow strict mode's rules ) agent = Agent( name="Gmail assistant", instructions="Answer questions about the user's Gmail inbox.", model="gpt-4o", tools=[scalekit_tool(definition) for definition in definitions], ) result = Runner.run_sync(agent, "Fetch my last 5 unread emails and summarize them") print(result.final_output) ``` **Node.js** ```typescript import { Agent, run, tool } from '@openai/agents'; const { tools: scopedTools } = await scalekit.tools.listScopedTools(identifier, { filter: { connectionNames: [connectionName] }, pageSize: 100, // up to 100 tools per page; page with nextPageToken for more }); // Tool definitions are typed as generic JSON, so read each field explicitly const definitions = scopedTools.flatMap((t) => (t.tool?.definition ? [t.tool.definition] : [])); const tools = definitions.map((definition) => tool({ name: String(definition.name), description: String(definition.description ?? ''), parameters: definition.input_schema as any, strict: false, // Scalekit schemas don't follow strict mode's rules execute: async (input) => { const result = await scalekit.actions.executeTool({ toolName: String(definition.name), identifier, connector: connectionName, toolInput: input as Record, }); return JSON.stringify(result.data); }, }), ); const agent = new Agent({ name: 'Gmail assistant', instructions: "Answer questions about the user's Gmail inbox.", model: 'gpt-4o', tools, }); const result = await run(agent, 'Fetch my last 5 unread emails and summarize them'); console.log(result.finalOutput); ``` If a tool function raises an error, the Agents SDK tells the model the call failed, so the model can retry or explain the failure. When the app itself rejects a request, `execute_tool` doesn't raise: the app's error response comes back in `data`, and the model reads it like any other result. ## Check it worked The script prints a summary of the user's unread emails. To see each tool call and the Gmail response it returned, read `result.new_items` (`result.newItems` in Node.js). ## Use MCP instead The Agents SDK connects to MCP servers natively. Point it at a [Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/overview/) with a session token for this user, and the agent discovers the server's tools on its own. Add this below the code above. It reuses the setup and, in Node.js, the `Agent` and `run` imports. **Python** ```python import asyncio import os from datetime import timedelta from agents import Agent, Runner from agents.mcp import MCPServerStreamableHttp # Returned by create_config when you created the Virtual MCP server config_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] mcp_url = os.environ["SCALEKIT_MCP_SERVER_URL"] # Mint a fresh session token for this user before each agent run mcp_token = actions.mcp.create_session_token( mcp_config_id=config_id, identifier=identifier, expiry=timedelta(hours=1), ).token async def run(): async with MCPServerStreamableHttp( name="scalekit", params={"url": mcp_url, "headers": {"Authorization": f"Bearer {mcp_token}"}}, ) as server: agent = Agent( name="Gmail assistant", instructions="Answer questions about the user's Gmail inbox.", model="gpt-4o", mcp_servers=[server], ) result = await Runner.run(agent, "Fetch my last 5 unread emails and summarize them") print(result.final_output) asyncio.run(run()) ``` **Node.js** ```typescript import { MCPServerStreamableHttp } from '@openai/agents'; // Returned by createConfig when you created the Virtual MCP server const configId = process.env.SCALEKIT_MCP_CONFIG_ID!; const mcpUrl = process.env.SCALEKIT_MCP_SERVER_URL!; // Mint a fresh session token for this user before each agent run const session = await scalekit.actions.mcp.createSessionToken({ mcpConfigId: configId, identifier, expirySeconds: 60 * 60, }); const mcpServer = new MCPServerStreamableHttp({ name: 'scalekit', url: mcpUrl, requestInit: { headers: { Authorization: `Bearer ${session.token}` } }, }); await mcpServer.connect(); try { const mcpAgent = new Agent({ name: 'Gmail assistant', instructions: "Answer questions about the user's Gmail inbox.", model: 'gpt-4o', mcpServers: [mcpServer], }); const mcpResult = await run(mcpAgent, 'Fetch my last 5 unread emails and summarize them'); console.log(mcpResult.finalOutput); } finally { await mcpServer.close(); } ``` See [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/) and [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/) to create the server, check the user's connections and mint a token. ## Next - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and ask users to reconnect. - [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to finish before real users connect. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Execute a tool](https://docs.scalekit.com/agentkit/reference/tools/execute-a-tool.md) - `GET` [List a user's scoped tools](https://docs.scalekit.com/agentkit/reference/tools/list-scoped-tools.md) - `POST` [Mint a session token](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-session-token.md) - `POST` [Mint a session token for a connection](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-connection-session-token.md) --- Source: https://docs.scalekit.com/agentkit/examples/anthropic.md # Anthropic Build an Anthropic agent with Scalekit-authenticated tools. Scalekit returns tool schemas in Anthropic's native format; no conversion needed. Build an agent using Anthropic's Claude that reads a user's Gmail inbox. Scalekit returns tool schemas with `input_schema`, the exact format Anthropic's tool use API expects. ## Before you start - Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](https://docs.scalekit.com/agentkit/connections/). - Have the `.env` file from the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/), and add your Anthropic API key as `ANTHROPIC_API_KEY` to it. - The samples load `.env` with `load_dotenv()` from `python-dotenv` in Python, and `import 'dotenv/config'` in Node.js. ## Install **Python** ```sh pip install scalekit-sdk-python python-dotenv anthropic ``` **Node.js** ```sh npm install @scalekit-sdk/node @anthropic-ai/sdk dotenv ``` For Node.js, save the code in a `.mts` file, such as `agent.mts`, and run it with `npx tsx agent.mts`. The `.mts` extension makes the file an ES module, so it can use top-level `await`. ## Initialize **Python** ```python import os import scalekit.client from dotenv import load_dotenv import anthropic from google.protobuf.json_format import MessageToDict load_dotenv() scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) actions = scalekit_client.actions client = anthropic.Anthropic() ``` **Node.js** ```typescript import 'dotenv/config'; import { ScalekitClient } from '@scalekit-sdk/node'; import Anthropic from '@anthropic-ai/sdk'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const anthropic = new Anthropic(); ``` ## Connect the user to Gmail **Python** ```python # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools connection_name = "gmail" identifier = "user_123" # your app's unique user ID response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=connection_name, identifier=identifier ) print("Authorize Gmail:", link.link) input("Press Enter after authorizing...") # Fetch the account again to pick up the new status response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": raise RuntimeError( f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." ) ``` **Node.js** ```typescript import { createInterface } from 'node:readline/promises'; import { ConnectorStatus } from '@scalekit-sdk/node'; // Connect the user's Gmail account, and wait until it's ACTIVE before calling tools const connectionName = 'gmail'; const identifier = 'user_123'; // your app's unique user ID let { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName, identifier, }); if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { const { link } = await scalekit.actions.getAuthorizationLink({ connectionName, identifier }); console.log('Authorize Gmail:', link); const rl = createInterface({ input: process.stdin, output: process.stdout }); await rl.question('Press Enter after authorizing...'); rl.close(); // Fetch the account again to pick up the new status ({ connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName, identifier, })); } if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { throw new Error(`Gmail is not ACTIVE (status ${connectedAccount?.status}). Authorize it and run again.`); } ``` See [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) for production auth handling. ## Run the agent Fetch tools scoped to this user, then run the full Claude tool-use loop. [Use built-in tools](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/) explains the scoped tool list, paging past 100 tools, and how to read `data` when the app returns an error. **Python** ```python # Fetch tools scoped to this user scoped_response, _ = actions.tools.list_scoped_tools( identifier="user_123", filter={"connection_names": ["gmail"]}, page_size=100, # up to 100 tools per page; page with next_page_token for more ) llm_tools = [ { "name": MessageToDict(t.tool).get("definition", {}).get("name"), "description": MessageToDict(t.tool).get("definition", {}).get("description", ""), "input_schema": MessageToDict(t.tool).get("definition", {}).get("input_schema", {}), } for t in scoped_response.tools ] # Run the agent loop messages = [{"role": "user", "content": "Fetch my last 5 unread emails and summarize them"}] def ask(): return client.messages.create( model="claude-sonnet-4-6", max_tokens=4096, tools=llm_tools, messages=messages, ) response = ask() while response.stop_reason == "tool_use": tool_results = [] for block in response.content: if block.type == "tool_use": result = actions.execute_tool( tool_name=block.name, identifier="user_123", connection_name="gmail", tool_input=block.input, ) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": str(result.data), }) messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": tool_results}) response = ask() # end_turn is the normal finish. Others, such as max_tokens or refusal, end early. if response.stop_reason != "end_turn": print(f"Stopped early: {response.stop_reason}") print("".join(block.text for block in response.content if block.type == "text")) ``` **Node.js** ```typescript // Fetch tools scoped to this user const { tools } = await scalekit.tools.listScopedTools('user_123', { filter: { connectionNames: ['gmail'] }, pageSize: 100, // up to 100 tools per page; page with nextPageToken for more }); // Tool definitions are typed as generic JSON, so read each field explicitly const llmTools: Anthropic.Tool[] = tools.flatMap((t) => { const definition = t.tool?.definition; if (!definition) return []; return [{ name: String(definition.name), description: String(definition.description ?? ''), input_schema: definition.input_schema as Anthropic.Tool.InputSchema, }]; }); // Run the agent loop const messages: Anthropic.MessageParam[] = [ { role: 'user', content: 'Fetch my last 5 unread emails and summarize them' }, ]; const ask = () => anthropic.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 4096, tools: llmTools, messages, }); let response = await ask(); while (response.stop_reason === 'tool_use') { const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type === 'tool_use') { const result = await scalekit.actions.executeTool({ toolName: block.name, identifier: 'user_123', connector: 'gmail', toolInput: block.input as Record, }); toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(result.data) }); } } messages.push({ role: 'assistant', content: response.content }); messages.push({ role: 'user', content: toolResults }); response = await ask(); } // end_turn is the normal finish. Others, such as max_tokens or refusal, end early. if (response.stop_reason !== 'end_turn') console.log(`Stopped early: ${response.stop_reason}`); for (const block of response.content) { if (block.type === 'text') console.log(block.text); } ``` ## Use MCP instead The Messages API can connect to a Scalekit Virtual MCP server through Anthropic's [MCP connector](https://docs.anthropic.com/en/docs/agents-and-tools/mcp-connector) (beta). Anthropic's API calls the MCP server for you, so there's no tool loop to write. Pass the server URL and a session token for this user. **Python** ```python from datetime import timedelta # Returned by create_config when you created the Virtual MCP server config_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] mcp_url = os.environ["SCALEKIT_MCP_SERVER_URL"] # Mint a fresh session token for this user before each agent run mcp_token = actions.mcp.create_session_token( mcp_config_id=config_id, identifier="user_123", expiry=timedelta(hours=1), ).token response = client.beta.messages.create( model="claude-sonnet-4-6", max_tokens=4096, messages=[{"role": "user", "content": "Fetch my last 5 unread emails and summarize them"}], mcp_servers=[ {"type": "url", "url": mcp_url, "name": "scalekit", "authorization_token": mcp_token} ], tools=[{"type": "mcp_toolset", "mcp_server_name": "scalekit"}], betas=["mcp-client-2025-11-20"], ) print("".join(block.text for block in response.content if block.type == "text")) ``` **Node.js** ```typescript // Mint the token on your backend with scalekit.actions.mcp.createSessionToken // (see Mint session tokens) and pass it in here. const mcpUrl = process.env.SCALEKIT_MCP_SERVER_URL!; // mcp_server_url from your Virtual MCP server const mcpToken = process.env.SCALEKIT_MCP_SESSION_TOKEN!; // minted on your backend for this user const response = await anthropic.beta.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 4096, messages: [{ role: 'user', content: 'Fetch my last 5 unread emails and summarize them' }], mcp_servers: [{ type: 'url', url: mcpUrl, name: 'scalekit', authorization_token: mcpToken }], tools: [{ type: 'mcp_toolset', mcp_server_name: 'scalekit' }], betas: ['mcp-client-2025-11-20'], }); for (const block of response.content) { if (block.type === 'text') console.log(block.text); } ``` Other MCP hosts, such as Claude Code, can connect to the same URL. Send the session token as an `Authorization: Bearer` header, and mint a new token when it expires (at most 24 hours). See [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/) and [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/) to create the server, check the user's connections and mint a token. ## Next - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and ask users to reconnect. - [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to finish before real users connect. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Execute a tool](https://docs.scalekit.com/agentkit/reference/tools/execute-a-tool.md) - `GET` [List a user's scoped tools](https://docs.scalekit.com/agentkit/reference/tools/list-scoped-tools.md) - `POST` [Mint a session token](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-session-token.md) - `POST` [Mint a session token for a connection](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-connection-session-token.md) --- Source: https://docs.scalekit.com/agentkit/examples/vercel-ai.md # Vercel AI SDK Build a Vercel AI SDK agent that reads Gmail as the user with AgentKit: wrap Scalekit tools with tool() and jsonSchema(), or connect to a Virtual MCP server. Build an agent using the Vercel AI SDK that reads a user's Gmail inbox. Wrap each Scalekit tool with `tool()` from the `ai` package, and pass its JSON Schema to the model with `jsonSchema()`. ## Before you start - Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](https://docs.scalekit.com/agentkit/connections/). - Have the `.env` file from the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. - The samples load `.env` with `import 'dotenv/config'`. ## Install ```sh npm install @scalekit-sdk/node ai@7 @ai-sdk/openai@4 dotenv ``` For Node.js, save the code in a `.mts` file, such as `agent.mts`, and run it with `npx tsx agent.mts`. The `.mts` extension makes the file an ES module, so it can use top-level `await`. ## Initialize ```typescript import 'dotenv/config'; import { ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); ``` ## Connect the user to Gmail ```typescript import { createInterface } from 'node:readline/promises'; import { ConnectorStatus } from '@scalekit-sdk/node'; // Connect the user's Gmail account, and wait until it's ACTIVE before calling tools const connectionName = 'gmail'; const identifier = 'user_123'; // your app's unique user ID let { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName, identifier, }); if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { const { link } = await scalekit.actions.getAuthorizationLink({ connectionName, identifier }); console.log('Authorize Gmail:', link); const rl = createInterface({ input: process.stdin, output: process.stdout }); await rl.question('Press Enter after authorizing...'); rl.close(); // Fetch the account again to pick up the new status ({ connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName, identifier, })); } if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { throw new Error(`Gmail is not ACTIVE (status ${connectedAccount?.status}). Authorize it and run again.`); } ``` See [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) for production auth handling. ## Run the agent Fetch the tools scoped to this user, wrap each one with the AI SDK's `tool()` helper, then let `generateText` run the tool-calling loop. [Use built-in tools](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/) explains the scoped tool list, paging past 100 tools, and how to read `data` when the app returns an error. ```typescript import { generateText, jsonSchema, stepCountIs, tool } from 'ai'; import { openai } from '@ai-sdk/openai'; const { tools: scopedTools } = await scalekit.tools.listScopedTools('user_123', { filter: { connectionNames: ['gmail'] }, pageSize: 100, // up to 100 tools per page; page with nextPageToken for more }); // Tool definitions are typed as generic JSON, so read each field explicitly const definitions = scopedTools.flatMap((t) => (t.tool?.definition ? [t.tool.definition] : [])); const tools = Object.fromEntries( definitions.map((definition) => { const name = String(definition.name); const schema = definition.input_schema ?? { type: 'object', properties: {} }; return [ name, tool({ description: String(definition.description ?? ''), inputSchema: jsonSchema>(schema as Parameters[0]), execute: async (args) => { const result = await scalekit.actions.executeTool({ toolName: name, identifier: 'user_123', connector: 'gmail', toolInput: args, }); return result.data; }, }), ]; }), ); const { text } = await generateText({ model: openai('gpt-4o'), tools, stopWhen: stepCountIs(5), prompt: 'Fetch my last 5 unread emails and summarize them', }); console.log(text); ``` ## Use MCP instead The Vercel AI SDK connects to MCP servers with `createMCPClient` from `@ai-sdk/mcp`. Pass the Virtual MCP server URL and a session token, and the client loads the server's tools for you. ```sh npm install @ai-sdk/mcp@2 ``` Mint the session token on your backend for each agent session with `scalekit.actions.mcp.createSessionToken`, as shown in [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/#mint-the-token), and pass it to this code. ```typescript import { createMCPClient } from '@ai-sdk/mcp'; import { generateText, stepCountIs } from 'ai'; import { openai } from '@ai-sdk/openai'; const mcpUrl = process.env.SCALEKIT_MCP_SERVER_URL!; // mcp_server_url from your Virtual MCP server const mcpToken = process.env.SCALEKIT_MCP_SESSION_TOKEN!; // minted on your backend for this user const mcpClient = await createMCPClient({ transport: { type: 'http', url: mcpUrl, headers: { Authorization: `Bearer ${mcpToken}` }, }, }); const tools = await mcpClient.tools(); const { text } = await generateText({ model: openai('gpt-4o'), tools, stopWhen: stepCountIs(5), prompt: 'Fetch my last 5 unread emails and summarize them', }); await mcpClient.close(); console.log(text); ``` See [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/) to create the server, and [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/) to check the user's connections and mint a token. ## Next - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and ask users to reconnect. - [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to finish before real users connect. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Execute a tool](https://docs.scalekit.com/agentkit/reference/tools/execute-a-tool.md) - `GET` [List a user's scoped tools](https://docs.scalekit.com/agentkit/reference/tools/list-scoped-tools.md) --- Source: https://docs.scalekit.com/agentkit/examples/mastra.md # Mastra Connect a Mastra agent to Scalekit tools over MCP, all in Node.js. Create a Virtual MCP server, mint a session token for the user, and run the agent. Connect a Mastra agent to Scalekit tools using MCP. Mastra has native MCP support via `@mastra/mcp`. Pass a Scalekit Virtual MCP server URL and a session token, and Mastra handles tool discovery automatically. > note: Why MCP for Mastra > > Mastra's tool system uses Zod schemas internally. The MCP path skips manual schema conversion. Mastra discovers tools and their schemas directly from the Scalekit MCP server. The whole example runs in Node.js on your server, in two files. `setup.mts` creates the Virtual MCP server once. `agent.mts` connects the user, mints a session token and runs the agent, every run. Run each with `npx tsx`, for example `npx tsx agent.mts`. ## Before you start - Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](https://docs.scalekit.com/agentkit/connections/). - Have the `.env` file from the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. - The samples load `.env` with `import 'dotenv/config'`. ## Install ```sh npm install @scalekit-sdk/node @mastra/core@1 @mastra/mcp@2 @ai-sdk/openai@4 dotenv ``` ## Initialize the Scalekit client Start both files with the client: ```typescript import 'dotenv/config'; import { ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); ``` ## Create the Virtual MCP server Create the server once per agent role, not once per user. Every user and every session reuses its `mcpServerUrl`. Server names are unique in an environment, so run this file once: a second run fails with `config with name 'gmail-user-tools' already exists`. ```typescript title="setup.mts" // Run once per agent role, not once per user const { config } = await scalekit.actions.mcp.createConfig({ name: 'gmail-user-tools', connectionToolMappings: [{ connectionName: 'gmail', tools: ['gmail_fetch_mails'] }], }); console.log('Virtual MCP server:', config!.id, config!.mcpServerUrl); ``` [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/) covers choosing tools, and listing, updating and deleting servers. ## Connect the user to Gmail The rest of the code goes in `agent.mts` and runs before each agent run. The Virtual MCP server only calls tools for users who have authorized the connection. Before each agent run, make sure the user's Gmail account is `ACTIVE`: ```typescript import { createInterface } from 'node:readline/promises'; import { ConnectorStatus } from '@scalekit-sdk/node'; // Connect the user's Gmail account, and wait until it's ACTIVE before calling tools const connectionName = 'gmail'; const identifier = 'user_123'; // your app's unique user ID let { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName, identifier, }); if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { const { link } = await scalekit.actions.getAuthorizationLink({ connectionName, identifier }); console.log('Authorize Gmail:', link); const rl = createInterface({ input: process.stdin, output: process.stdout }); await rl.question('Press Enter after authorizing...'); rl.close(); // Fetch the account again to pick up the new status ({ connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName, identifier, })); } if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { throw new Error(`Gmail is not ACTIVE (status ${connectedAccount?.status}). Authorize it and run again.`); } ``` See [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) for production auth handling. ## Mint a session token for the user The server URL is the same for everyone. The **session token** carries the user's identity. Mint a new token before each agent run, after the connect step: ```typescript title="agent.mts" import { ScalekitNotFoundException, ScalekitUnauthorizedException } from '@scalekit-sdk/node'; // Run before each agent session, after the connect step let mcpServerUrl: string; let sessionToken: string; try { const { configs } = await scalekit.actions.mcp.listConfigs({ search: 'gmail-user-tools' }); const server = configs.find((c) => c.name === 'gmail-user-tools'); if (!server) throw new Error('Run setup.mts to create the gmail-user-tools server first'); mcpServerUrl = server.mcpServerUrl; const session = await scalekit.actions.mcp.createSessionToken({ mcpConfigId: server.id, identifier, // the user who authorized Gmail above expirySeconds: 60 * 60, }); sessionToken = session.token; } catch (err) { if (err instanceof ScalekitUnauthorizedException) { // Scalekit client credentials are wrong or expired: fix the environment variables } else if (err instanceof ScalekitNotFoundException) { // The server was deleted: create it again, then retry } throw err; // don't start the agent without a token } ``` Don't start the agent when minting fails: every tool call would return `401`. See [Errors and rate limits](https://docs.scalekit.com/agentkit/reference/errors/) for what an error response contains. Set `expirySeconds` above the expected run time; [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/) covers when to mint again. > caution: Do not share one session token across users > > The server URL is safe to share, because every user gets the same one. The session token is not. Each token is scoped to one identifier, and any request carrying it runs as that user. Mint a token per user on your server, and never send it to client-side code. ## Build the agent Pass the server URL to `MCPClient`, and the user's session token as a bearer header in `requestInit`. Mastra fetches the tool list and schemas automatically. It names each tool after the server key, so `gmail_fetch_mails` appears to the model as `scalekit_gmail_fetch_mails`. ```typescript title="agent.mts" import { Agent } from '@mastra/core/agent'; import { MCPClient } from '@mastra/mcp'; import { openai } from '@ai-sdk/openai'; const mcp = new MCPClient({ servers: { scalekit: { url: new URL(mcpServerUrl), requestInit: { headers: { Authorization: `Bearer ${sessionToken}` } }, }, }, }); try { const tools = await mcp.listTools(); const agent = new Agent({ id: 'gmail-assistant', name: 'Gmail assistant', instructions: 'You are a helpful Gmail assistant.', model: openai('gpt-4o'), tools, }); const result = await agent.generate('Fetch my last 5 unread emails and summarize them'); console.log(result.text); } finally { await mcp.disconnect(); } ``` ## Next - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and ask users to reconnect. - [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to finish before real users connect. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/create-a-virtual-mcp-server.md) - `GET` [List Virtual MCP servers](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/list-virtual-mcp-servers.md) - `POST` [Mint a session token](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-session-token.md) - `POST` [Mint a session token for a connection](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-connection-session-token.md) --- Source: https://docs.scalekit.com/agentkit/examples/crewai.md # CrewAI Build a CrewAI agent with Scalekit-authenticated Gmail tools via MCP. CrewAI's MCPServerAdapter connects to a Scalekit MCP URL for automatic tool discovery. Build a CrewAI agent that reads a user's Gmail inbox. Scalekit handles OAuth, token storage, and exposes tools over MCP. CrewAI's `MCPServerAdapter` discovers the tools and their schemas from the server. Some schemas need a small patch, covered below. Full code on GitHub ## Before you start - Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](https://docs.scalekit.com/agentkit/connections/). - Have the `.env` file from the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. - Create a Virtual MCP server named `gmail-user-tools` that includes the Gmail tools. See [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/). - The samples load `.env` with `load_dotenv()` from `python-dotenv`. ## Install ```sh pip install crewai "crewai-tools[mcp]" scalekit-sdk-python python-dotenv ``` ## Initialize ```python import os from scalekit import ScalekitClient from dotenv import find_dotenv, load_dotenv load_dotenv(find_dotenv()) scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions ``` ## Connect the user to Gmail ```python # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools connection_name = "gmail" identifier = "user_123" # your app's unique user ID response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=connection_name, identifier=identifier ) print("Authorize Gmail:", link.link) input("Press Enter after authorizing...") # Fetch the account again to pick up the new status response = actions.get_or_create_connected_account( connection_name=connection_name, identifier=identifier ) if response.connected_account.status != "ACTIVE": raise RuntimeError( f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." ) ``` See [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) for production auth handling. ## Build and run the agent Get the Virtual MCP server URL and mint a session token, then pass both to `MCPServerAdapter`. CrewAI discovers all available Gmail tools from the MCP server: ```python from crewai import Agent, Crew, LLM, Task from crewai_tools import MCPServerAdapter from datetime import timedelta # Retrieve config_id by listing Virtual MCP servers filtered by name list_response = actions.mcp.list_configs(filter_name="gmail-user-tools") mcp_server_url = list_response.configs[0].mcp_server_url mcp_id = list_response.configs[0].id token_response = actions.mcp.create_session_token( mcp_config_id=mcp_id, identifier="user_123", expiry=timedelta(hours=1), ) with MCPServerAdapter({ "url": mcp_server_url, "headers": {"Authorization": f"Bearer {token_response.token}"}, "transport": "streamable-http", }) as tools: agent = Agent( role="Email Assistant", goal="Fetch and summarize the user's unread emails", backstory="You are a helpful assistant with access to the user's Gmail inbox.", tools=tools, llm=LLM( model=os.getenv("LLM_MODEL", "gpt-4o"), base_url=os.getenv("OPENAI_BASE_URL"), api_key=os.getenv("OPENAI_API_KEY"), ), verbose=True, ) task = Task( description="Fetch the last 5 unread emails and provide a brief summary of each.", expected_output="A list of 5 unread emails with subject, sender, and a one-sentence summary.", agent=agent, ) result = Crew(agents=[agent], tasks=[task]).kickoff() print(result) ``` > note: Nullable schema fields > > Some Scalekit tool schemas include nullable types (`{"type": ["string", "null"]}`) that CrewAI's schema converter doesn't handle out of the box. If you see a `TypeError` during tool parsing, apply the [schema patch](https://github.com/scalekit-developers/crewai-scalekit-example/blob/main/agent.py) at the top of your script. ## Multi-agent crew CrewAI's real strength is multi-agent orchestration. For a full example that splits email triage across three specialized agents (scanner, prioritizer, drafter), see the [CrewAI email triage cookbook](https://docs.scalekit.com/cookbooks/crewai-agentkit-email-triage/). ## Get the MCP server URL The code above reads `mcp_server_url` from a Virtual MCP server. Create one in the Scalekit dashboard under **AgentKit** > **Virtual MCP Servers**, or with the API. See [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/). ## Next - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and ask users to reconnect. - [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to finish before real users connect. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `GET` [List Virtual MCP servers](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/list-virtual-mcp-servers.md) - `POST` [Mint a session token](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-session-token.md) - `POST` [Mint a session token for a connection](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-connection-session-token.md) --- Source: https://docs.scalekit.com/agentkit/examples/claude-managed-agents.md # Claude Managed Agents Give Claude Managed Agents per-user tools with AgentKit: create a Virtual MCP server, keep each user's session token in their own Anthropic vault, and run it. Run a background agent that reads Gmail and creates Google Calendar events — without managing any agent loop. Anthropic handles tool discovery, execution, retries, and session state. You provide the task. > tip: Complete working demo on GitHub > > Browse the full source for this guide at [GitHub](https://github.com/scalekit-inc/python-connect-demos/tree/main/claude-managed-agents). Scalekit connects the agent to user-authorized tools via a [Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/overview/). Before each run, you mint a short-lived session token and store it in that user's own Anthropic vault. The agent accesses the MCP server using the vault credential. ## Before you start - Create Gmail and Google Calendar connections in **AgentKit** > **Connections**. The samples use the connection names `gmail` and `googlecalendar`, so change them if yours differ. See [Set up a connection](https://docs.scalekit.com/agentkit/connections/). - Have the `.env` file from the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/), and add to it your [Anthropic API key](https://platform.claude.com/settings/keys) with access to the Managed Agents beta as `ANTHROPIC_API_KEY`, and your Anthropic environment ID as `ANTHROPIC_ENVIRONMENT_ID`. - The samples load `.env` with `load_dotenv()` from `python-dotenv`. ## How it works The flow has three phases: 1. **Build** (one-time) — Create a Virtual MCP server and a Claude Managed Agent. Save `mcp_id` and `agent_id`. 2. **Authorize user for external connections** (once per user) — Authorize the user's Gmail and Google Calendar accounts. 3. **Run a session** (per agent run) — Check connections, mint a session token, store it in that user's own Anthropic vault, and start a session. Never share one vault across users: a session acts as whichever user's token is in its vault. ## Install ```sh pip install anthropic scalekit-sdk-python python-dotenv ``` ## Build Run this once to create your Virtual MCP server and Claude Managed Agent. Save the returned `mcp_id` and `agent_id` — you reuse them for every user and every session. ```python title="builder.py" import os import anthropic from scalekit import ScalekitClient from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping from dotenv import load_dotenv load_dotenv() anthropic_client = anthropic.Anthropic() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) GMAIL_TOOLS = ["gmail_fetch_mails"] GCAL_TOOLS = [ "googlecalendar_list_calendars", "googlecalendar_list_events", "googlecalendar_get_event_by_id", "googlecalendar_create_event", "googlecalendar_update_event", ] vmcp_response = scalekit_client.actions.mcp.create_config( name="email-calendar-demo", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="gmail", tools=GMAIL_TOOLS, ), McpConfigConnectionToolMapping( connection_name="googlecalendar", tools=GCAL_TOOLS, ), ], ) mcp_id = vmcp_response.config.id mcp_server_url = vmcp_response.config.mcp_server_url agent = anthropic_client.beta.agents.create( name="Email Meeting Manager", model="claude-haiku-4-5", system=( "You are an email and calendar assistant. When invoked, you will:\n" "1. Fetch the single most recent unread email from Gmail.\n" "2. Summarize it in 2-3 sentences.\n" "3. Create a Google Calendar event titled 'Action Required: ' " "with your summary as the description." ), mcp_servers=[ { "type": "url", "name": "email-calendar-mcp", "url": mcp_server_url, } ], tools=[ {"type": "agent_toolset_20260401", "default_config": {"enabled": True}}, { "type": "mcp_toolset", "mcp_server_name": "email-calendar-mcp", "default_config": { "enabled": True, "permission_policy": {"type": "always_allow"}, }, }, ], ) print("Add these to your .env file:") print(f"SCALEKIT_MCP_CONFIG_ID={mcp_id}") print(f"ANTHROPIC_AGENT_ID={agent.id}") ``` Run `python builder.py` once and add the two printed lines to your `.env` file. The next two scripts read them from there. The agent definition references the `mcp_server_url` but carries no auth credentials. Authentication is injected at runtime via the Anthropic vault. ## Authorize user for external connections Each user authorizes their Gmail and Google Calendar accounts once. All future agent sessions for that user reuse those connections. ```python title="executor_setup.py" import os from scalekit import ScalekitClient from dotenv import load_dotenv load_dotenv() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) mcp_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] # printed by builder.py identifier = "user_123" # your app's unique user ID accounts_response = scalekit_client.actions.mcp.list_mcp_connected_accounts( config_id=mcp_id, identifier=identifier, ) for account in accounts_response.connected_accounts: status = (account.connected_account_status or "").upper() if status != "ACTIVE": auth_response = scalekit_client.actions.get_authorization_link( identifier=identifier, connection_name=account.connection_name, ) print(f"{account.connection_name} needs auth: {auth_response.link}") else: print(f"✓ {account.connection_name} is ACTIVE") ``` Surface the auth link in your app UI or send it via email. Users only need to do this once. ## Run a session Run `executor.py` for each agent run. It stops before starting a session if any connection isn't `ACTIVE`, so the agent never runs with a tool it can't call. 1. ## Set up the clients and the run ```python title="executor.py" import json import os import sys from datetime import timedelta from pathlib import Path import anthropic from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() anthropic_client = anthropic.Anthropic() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) mcp_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] # printed by builder.py agent_id = os.environ["ANTHROPIC_AGENT_ID"] # printed by builder.py identifier = "user_123" # your app's unique user ID prompt = "Process my most recent unread email." # Each user gets their own vault, created on their first run. # This example keeps the IDs in a local file. In production, store them # in your database, keyed by the user. VAULTS_FILE = Path("vaults.json") vaults = json.loads(VAULTS_FILE.read_text()) if VAULTS_FILE.exists() else {} user_vault = vaults.get(identifier) ``` 2. ## Check that connections are active OAuth tokens for connected accounts can expire or be revoked, so check every connection before minting a token. If any isn't `ACTIVE`, stop and send the user back through `executor_setup.py`. ```python title="executor.py" accounts_response = scalekit_client.actions.mcp.list_mcp_connected_accounts( config_id=mcp_id, identifier=identifier, ) inactive = [ a.connection_name for a in accounts_response.connected_accounts if (a.connected_account_status or "").upper() != "ACTIVE" ] if inactive: sys.exit(f"Not ACTIVE: {', '.join(inactive)}. Run executor_setup.py to reauthorize.") ``` 3. ## Mint a session token and store it in the vault Mint a short-lived session token and store it in this user's Anthropic vault. Claude Managed Agents access the MCP server using the vault credential, not a direct bearer header, and the session acts as whichever user's token is in the vault. So give each user their own vault, and never put two users' tokens in the same one. ```python title="executor.py" configs_response = scalekit_client.actions.mcp.list_configs(filter_id=mcp_id) mcp_server_url = configs_response.configs[0].mcp_server_url token_response = scalekit_client.actions.mcp.create_session_token( mcp_config_id=mcp_id, identifier=identifier, expiry=timedelta(hours=1), ) token = token_response.token # Update this user's own credential, or create their vault on their first run. # Never reuse one vault across users: a session acts as whoever's token is in it. if user_vault: vault_id = user_vault["vault_id"] anthropic_client.beta.vaults.credentials.update( user_vault["credential_id"], vault_id=vault_id, auth={"type": "static_bearer", "token": token}, ) else: vault = anthropic_client.beta.vaults.create(display_name=f"email-calendar-{identifier}") vault_id = vault.id credential = anthropic_client.beta.vaults.credentials.create( vault_id, display_name="email-calendar-credential", auth={ "type": "static_bearer", "mcp_server_url": mcp_server_url, "token": token, }, ) vaults[identifier] = {"vault_id": vault_id, "credential_id": credential.id} VAULTS_FILE.write_text(json.dumps(vaults, indent=2)) ``` 4. ## Start the session Pass `vault_ids` to the session so the agent can authenticate against the MCP server. ```python title="executor.py" session = anthropic_client.beta.sessions.create( agent=agent_id, environment_id=os.environ["ANTHROPIC_ENVIRONMENT_ID"], vault_ids=[vault_id], ) with anthropic_client.beta.sessions.events.stream(session_id=session.id) as stream: anthropic_client.beta.sessions.events.send( session_id=session.id, events=[{"type": "user.message", "content": [{"type": "text", "text": prompt}]}], ) for event in stream: if event.type == "agent.message": for block in event.content: if block.type == "text": print(block.text, end="", flush=True) elif event.type == "agent.mcp_tool_use": print(f"\n→ {event.name}", flush=True) elif event.type == "session.status_terminated": break elif event.type == "session.status_idle": # The session also goes idle while it waits on you (requires_action). # Stop only when it's idle for another reason, such as end_turn. if event.stop_reason.type != "requires_action": break ``` ## Next - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and ask users to reconnect. - [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to finish before real users connect. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Get an authorization link](https://docs.scalekit.com/agentkit/reference/authorization/get-an-authorization-link.md) - `POST` [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/create-a-virtual-mcp-server.md) - `GET` [List Virtual MCP servers](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/list-virtual-mcp-servers.md) - `POST` [Check a user's connected accounts for a server](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/check-connected-accounts-for-a-server.md) - `POST` [Mint a session token](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-session-token.md) - `POST` [Mint a session token for a connection](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-connection-session-token.md) --- Source: https://docs.scalekit.com/agentkit/coding-agents.md # Set up AgentKit with your coding agent Add AgentKit to your codebase with Claude Code, Codex, GitHub Copilot CLI, Cursor or another coding agent: install the authstack plugin and paste one prompt. Install the authstack plugin into your coding agent and paste one prompt. The agent generates client initialization, connected account management, OAuth authorization, and token handling — no boilerplate required. ## Before you start - A Scalekit account at [app.scalekit.com](https://app.scalekit.com) - A connector configured under **AgentKit** > **Connections**. The prompt defaults to GitHub (`github-connect`). Name another connector if you want that instead. - Your API credentials from **Developers** > **Settings** > **API Credentials** ## Pick your coding agent **Recommended: one command** ```bash title="Terminal" npx @scalekit-inc/cli setup ``` For repeated use: `npm install -g @scalekit-inc/cli` then `scalekit setup`. The CLI installs the authstack plugin for your editor. Then load the shipped skill `integrate-agentkit`. Complete any browser OAuth prompt for the Scalekit MCP server. Done: the command exits 0 and `integrate-agentkit` is available. Then copy the AgentKit prompt below (or describe your goal naturally). **Per-tool details** After the CLI (or if you prefer tool-native flows): - Claude Code / Copilot: marketplace + plugin install is handled by the CLI. - Cursor / Codex: plugins are installed locally by the CLI. - Other agents (Cline, Amp, OpenCode and other agents that support skills): run `npx @scalekit-inc/cli skills install -y` to install the Scalekit skills to `~/.agents/skills`, or `npx skills add scalekit-inc/authstack --skill integrate-agentkit` for this skill only. Then copy the AgentKit prompt below. ## Verify the setup 1. **Set environment variables** — copy `SCALEKIT_CLIENT_ID`, `SCALEKIT_CLIENT_SECRET`, and `SCALEKIT_ENVIRONMENT_URL` from **Developers** > **Settings** > **API Credentials** in the dashboard. Done: all three names are set in `.env`. 2. **Trigger the authorization flow** — run the generated example and confirm the browser redirects to the connector's consent page. Done: the consent page opens. 3. **Call a tool** — after consent, run a read-only tool through Scalekit, such as `github_user_get_authenticated` for GitHub. Done: the tool call returns data from the connector. > caution: Review generated code before deploying > > Verify that token validation logic, error handling, and environment variable references match your application's requirements. The generated code is a foundation, not a finished implementation. ## Troubleshooting **The agent generated code for a connector I haven't configured yet** The plugin uses the connector name you provide in the prompt. If that connector isn't configured in your Scalekit Dashboard, creating the authorization link fails with `connection not found for the given key`. Fix: in the [Scalekit Dashboard](https://app.scalekit.com), go to **AgentKit** > **Connections** > **Create Connection**, finish the connection, then re-run the agent prompt with the exact connection name from the dashboard. **I want to swap connectors after the initial generation** Re-run the implementation prompt with the new connector name. The agent updates the connector reference in the client initialization and regenerates the tool calls. Existing connected accounts for the old connector are not affected. **The scaffolded code references an SDK version that doesn't match my lockfile** The plugin targets the latest stable Scalekit SDK. If your lockfile pins an older version, either upgrade the SDK (`npm install @scalekit-sdk/node@latest` or equivalent) or ask the agent to regenerate using your pinned version by adding "use SDK version X.Y.Z" to the prompt. --- Source: https://docs.scalekit.com/agentkit/hermes.md # Connect Hermes to AgentKit Install the Scalekit plugin from the Hermes catalog, paste three API credentials, and call GitHub, Gmail, Slack and other apps by connection name. [Hermes](https://hermes-agent.nousresearch.com/docs/) is the self-improving AI agent from Nous Research. Hermes runs where you put it: a laptop, a small VPS, or a GPU cluster. You reach Hermes from the command line, or from a messaging app such as Slack or Telegram, and Hermes also runs work on a cron schedule. Hermes ships with 60+ built-in tools, and the built-in tools stop at the edge of your machine. A Hermes agent that reads your Gmail, posts to your Slack, or opens a GitHub pull request needs an access token for each app. Every access token belongs to one user. Scalekit holds the connected account for that user, stores the access token, and refreshes it. Install the **Scalekit plugin** from the Hermes plugin catalog, paste three Scalekit values once, and Hermes calls your connections with four tools. Access tokens stay in Scalekit: the plugin's tools don't return them to the chat. To manage several users on one Hermes host yourself, use the [`hermes-delegated-auth` skill](#install-the-authstack-skill) instead. It's described later on this page. ```d2 title="Hermes agent calling third-party services such as Gmail through Scalekit AgentKit's OAuth handler and token vault" direction: right Hermes: "Hermes Agent" { style.font-size: 18 } Scalekit: { label: "Scalekit AgentKit" Auth: "OAuth handler" Vault: "Token vault" } Providers: { label: "Third-party services" Gmail: "Gmail" Slack: "Slack" Salesforce: "Salesforce" More: "200+ more" } Hermes -> Scalekit.Auth: "Execute tools" Scalekit.Vault -> Providers.Gmail Scalekit.Vault -> Providers.Slack Scalekit.Vault -> Providers.Salesforce Scalekit.Vault -> Providers.More ``` ## Prerequisites - [Hermes Agent](https://hermes-agent.nousresearch.com/docs/getting-started/installation) 0.20.0 or later, installed with the Hermes installer. Run `hermes update` to upgrade an older install. - A Scalekit account with AgentKit enabled: [sign up at app.scalekit.com](https://app.scalekit.com) - At least one connection in **AgentKit** > **Connections**. Create the connection before you ask Hermes to use the app. See [Set up a connection](https://docs.scalekit.com/agentkit/connections/). ## Install the Scalekit plugin The [Scalekit plugin](https://github.com/scalekit-inc/hermes-tools) is listed in the Hermes plugin catalog. It adds four tools to Hermes and installs no extra Python packages. Quit any open `hermes` chat before you start. 1. ### Check the Hermes version ```bash hermes --version ``` The version must be 0.20.0 or later. If it's older, run `hermes update`. 2. ### Install the plugin from the catalog ```bash hermes plugins install scalekit ``` Hermes installs the version of the plugin that the catalog lists. To see that entry first, run `hermes plugins info scalekit`. 3. ### Paste three API credentials Hermes asks for three values and saves them in `~/.hermes/.env`. Copy them from **Developers** > **Settings** > **API Credentials** in the Scalekit dashboard. If Hermes doesn't ask, the values are already in that file. | Prompt | What you paste | | --- | --- | | `SCALEKIT_CLIENT_ID` | Your client ID, such as `skc_1234` | | `SCALEKIT_CLIENT_SECRET` | Your client secret. This is your Scalekit secret, not a Gmail or Slack token. | | `SCALEKIT_ENVIRONMENT_URL` | Your environment URL, such as `https://your-env.scalekit.dev` | > caution: Keep the client secret private > > Anyone with `SCALEKIT_CLIENT_SECRET` can call your Scalekit environment. Keep it only in `~/.hermes/.env`, and never paste it into a chat. 4. ### Enable the plugin When Hermes asks `Enable 'scalekit' now?`, type `Y`. If you skipped that prompt, run `hermes plugins enable scalekit`. Start a new `hermes` chat so the tools load. If a Hermes gateway is already running, restart it with `hermes gateway restart`. 5. ### Copy a connection name In **AgentKit** > **Connections**, copy the name of a connection, such as `github-connect`. You give Hermes that name. Don't use the `conn_` ID. ## Try it Start a new `hermes` chat and send: ```txt List my Scalekit connections. ``` ```txt Who am I on GitHub? Use the connection github-connect. ``` Replace `github-connect` with your own connection name. ## Check it worked - **The list shows each connection** with its name, type and status, for example `github-connect`, `OAUTH`, `COMPLETED`. - **The first request for an app returns an authorization link.** Open the link and finish sign-in on the hosted page, then send the same request again. A scheduled job can't finish sign-in, so authorize each app in a normal chat first. - **The next request returns your GitHub login and profile URL.** The reply doesn't include the access token. The plugin creates an [identifier](https://docs.scalekit.com/agentkit/concepts/) (`usr_…`) on first use and stores it in `~/.hermes/plugin-data/scalekit/identifier`. You don't type it. Updating the plugin keeps the same identifier, so the apps you authorized stay `ACTIVE`. ## Use the four tools | Tool | What it does | | --- | --- | | `scalekit_list_connections` | Lists the connections in your Scalekit environment. | | `scalekit_authorize` | Returns an authorization link, or `ACTIVE` if the user already authorized the connection. | | `scalekit_execute_tool` | Runs one named tool once the connected account is `ACTIVE`. Until then, it returns an authorization link. | | `scalekit_proxy` | Calls one path of the app's API through Scalekit, for connectors with no named tool for the task. | ## Update the plugin ```bash hermes plugins update scalekit ``` Hermes moves the plugin to the version the catalog lists now. ## How the authstack skill works This section covers the `hermes-delegated-auth` skill. For the plugin, see [Install the Scalekit plugin](#install-the-scalekit-plugin). The skill runs the following loop when you name an app: 1. The skill looks up the Scalekit connection you already created for [GitHub](https://docs.scalekit.com/agentkit/connectors/github/), [Gmail](https://docs.scalekit.com/agentkit/connectors/gmail/), or another app. 2. The skill checks the user's connected account. `ACTIVE` means auth is complete and Scalekit holds a token. 3. The skill returns an authorization link when the connected account is not `ACTIVE`. 4. The skill fetches the tool schema, calls the tool, and returns the result. 5. The skill calls the Scalekit HTTP proxy when the connector has no named tool. An authorization link is a one-time URL. It opens the [Hosted Connect Widget](https://docs.scalekit.com/agentkit/hosted-connect-widget/), which shows OAuth consent or an API-key form. ## Install the authstack skill Use this skill instead of the plugin when you want to set the default identifier yourself in `~/.hermes/.env`. You also need [`uv`](https://docs.astral.sh/uv/) on your `PATH`. 1. ### Install the skill Install `hermes-delegated-auth` from authstack: ```bash hermes skills install scalekit-inc/authstack/kits/agentkit/host/hermes-delegated-auth ``` Confirm the skill is enabled: ```bash hermes skills list ``` ```txt │ hermes-delegated-auth │ │ local │ local │ enabled │ ``` Install the Python dependencies: ```bash cd "${HERMES_HOME:-$HOME/.hermes}/skills/hermes-delegated-auth" uv sync ``` New chats load `/hermes-delegated-auth`. Run `/reset` in a chat that is already open. `HERMES_HOME` changes the skill path. The commands on this page use `~/.hermes`. > caution: Install from the authstack path > > A raw `SKILL.md` URL leaves out `scripts/`, and the skill then fails at run time. The `integrate-agentkit-host` skill is for coding agents, not for Hermes. 2. ### Configure credentials Put only Scalekit client credentials in `~/.hermes/.env`. Scalekit stores and refreshes the provider tokens for GitHub, Gmail, and Slack. Provider tokens never belong in a Hermes file. ```bash title="~/.hermes/.env" SCALEKIT_CLIENT_ID=skc_your_client_id # Keep this secret: anyone who has it can call tools as any ACTIVE identifier. SCALEKIT_CLIENT_SECRET=your_client_secret SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.dev SCALEKIT_IDENTIFIER=usr_8f3a2c ``` | Parameter | Description | |-----------|-------------| | `SCALEKIT_CLIENT_ID` | Your Scalekit client ID | | `SCALEKIT_CLIENT_SECRET` | Your Scalekit client secret | | `SCALEKIT_ENVIRONMENT_URL` | Your Scalekit environment URL | | `SCALEKIT_IDENTIFIER` | Default user the host acts as | Copy the client ID, client secret, and environment URL from **Dashboard > Developers > Settings > API Credentials**. `SCALEKIT_IDENTIFIER` is not a dashboard credential. Pick an opaque id that only your system knows, for example `usr_8f3a2c`. Every `tool_exec.py` command needs `SCALEKIT_IDENTIFIER`, including `--list-connections`. > caution: Keep the identifier unguessable > > An email address or a first name is guessable. `SCALEKIT_CLIENT_SECRET` lets anyone call tools as any `ACTIVE` identifier, so a guessable identifier widens the damage. Add [user verification](https://docs.scalekit.com/agentkit/user-verification/) in a multi-user product so the wrong person cannot activate the slot. 3. ### Ask Hermes to act Start a chat and ask for a real action. Hermes acts as `SCALEKIT_IDENTIFIER` by default, and Hermes passes `--identifier` to the skill when you name another user in the prompt. **GitHub** ```txt You: Who am I on GitHub? ``` Hermes loads the skill and runs these steps: 1. Hermes looks up the GitHub connection. 2. Hermes checks that the connected account is `ACTIVE`, and returns an authorization link when the account is not. 3. Hermes fetches the tool schema. 4. Hermes calls the tool and returns your GitHub login. **Gmail** ```txt You: Show me my latest unread emails ``` Hermes runs these steps: 1. Hermes looks up the Gmail connection. 2. Hermes returns an authorization link when you have not authorized Gmail yet. 3. Hermes fetches the tool schema. 4. Hermes returns the mail. **Notion** ```txt You: Read my Notion page https://notion.so/My-Page-abc123 ``` Hermes runs these steps: 1. Hermes looks up the Notion connection. 2. Hermes returns an authorization link when you have not authorized Notion yet. 3. Hermes fetches the page tool schema. 4. Hermes returns the page content. **Slack (as a user)** ```txt You: As identifier usr_8f3a2c, list my unread Slack DMs ``` Hermes runs these steps: 1. Hermes looks up the Slack connection. 2. Hermes returns an authorization link when the Slack connected account is not `ACTIVE`. 3. Hermes fetches the Slack tool schema. 4. Hermes returns the direct messages (DMs) as `usr_8f3a2c`. Create the Slack connection with **User scope** for the prompt above. A Slack connection with **Bot scope** acts as your Slack app instead of acting as the person. See the [Slack connector](https://docs.scalekit.com/agentkit/connectors/slack/). **Google Calendar** ```txt You: Create an out-of-office event tomorrow on the calendar for usr_8f3a2c ``` Hermes runs these steps: 1. Hermes looks up the Google Calendar connection for `usr_8f3a2c`. 2. Hermes returns an authorization link when the connected account is not `ACTIVE`. 3. Hermes calls the create-event tool. 4. Scalekit writes the event with the `usr_8f3a2c` token. Name the skill in the prompt to force the same path: ```txt /hermes-delegated-auth who am I on GitHub? ``` ## Verify the authstack skill Confirm the following for `hermes-delegated-auth`. For the plugin, see [Check it worked](#check-it-worked). - The skill is listed (`hermes skills list` or `/skills`) as `hermes-delegated-auth` - The connected account is `ACTIVE` after you finish the authorization link - Hermes returns data from the provider (the GitHub login, or unread mail) Open the authorization link again and retry the same prompt if the account stays inactive. ## Use Slack as a bot or as a user Slack issues two kinds of token, and the token decides whose name appears on a message. A bot token posts as your Slack app. A user token posts as the person who authorized the app. Authorizing the Hermes gateway grants a bot token, so the gateway alone cannot post as a person. | Job | Use | |-----|-----| | Chat with the agent in a channel or DM | Hermes Slack **gateway**. Bot tokens (`xoxb-` + `xapp-`) in `~/.hermes/.env`. See the [Hermes Slack setup](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/slack). | | Read private history, post, or act as a user | Scalekit Slack connection created with **User scope**. See the [Slack connector](https://docs.scalekit.com/agentkit/connectors/slack/). | The gateway bot is a channel into the agent. The gateway bot is not `usr_8f3a2c`. Always-on hosts use both identities. The gateway hears the channel. Scalekit acts as the user. ## Run jobs on a schedule These commands attach the `hermes-delegated-auth` skill. With the Scalekit plugin, a scheduled job that reaches an app the user hasn't authorized returns the authorization link and stops, so authorize each app in a normal chat first. Attach the skill to a Hermes cron job. Each fire is a fresh session. The skill uses Scalekit client credentials. You do not mint a session token. ```bash hermes cron create "0 9 * * *" "List unread emails for usr_8f3a2c and post a 5-line summary" --skill hermes-delegated-auth ``` Or in chat: ```txt /cron add "every weekday at 9am" "List unread emails for usr_8f3a2c and post a 5-line summary" --skill hermes-delegated-auth ``` The connected account must already be `ACTIVE`. Cron cannot click an authorization link. Leave the gateway running on a headless host. A new authorization link can then land in Slack or Telegram. Do not put Gmail or Calendar refresh tokens in `~/.hermes/.env`. ## Choose how Hermes reaches Scalekit Pick the path that matches the host. **Scalekit plugin.** Install it from the Hermes catalog with `hermes plugins install scalekit`. Hermes saves three Scalekit values, the plugin creates the identifier, and you name a connection in the chat. **Authstack skill.** Install `hermes-delegated-auth` from authstack. You set `SCALEKIT_IDENTIFIER` yourself and can name another user per turn. **Virtual MCP in your app.** Your application mints a session token before each agent run and passes that token to the agent framework. Claude Managed Agents, Mastra, and CrewAI use this path. | Question | Scalekit plugin | Authstack skill | Virtual MCP in your app | |---|---|---|---| | Who runs the agent | Hermes | Hermes | your app | | How many end users | one stored identifier; a tool call can name another | many; switch with `--identifier` | many | | Credential on the host | Scalekit client ID and secret | Scalekit client ID and secret | none | | Anything expires? | no | no | yes, per run | | Who mints a replacement | nobody; there is no token | nobody; there is no token | your code, before each run | | Tool scoping | the full connector catalog | the full connector catalog | only the tools in the server | | Best for | getting started on your own Hermes | multi-user hosts and cron | short agent runs | For Virtual MCP in your app, see [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/). ## Avoid these mistakes - Do not put provider tokens in `~/.hermes/.env`. Put only Scalekit credentials there. - With the Scalekit plugin, let the plugin manage the identifier. Don't type a `usr_…` value. The `hermes-delegated-auth` skill still needs `SCALEKIT_IDENTIFIER`. - Do not set `SCALEKIT_IDENTIFIER` to an email or another guessable value. - Do not use the bundled Google Workspace skill (`~/.hermes/google_token.json`) if AgentKit owns the Google user. `~/.hermes/google_token.json` is one laptop login, not a per-user identifier. - Do not run `hermes mcp login` against Scalekit Virtual MCP (a Scalekit MCP URL plus a session token). Virtual MCP uses a static bearer session token, and a static bearer token is not MCP OAuth. - Do not treat the Slack bot token as “send as usr_8f3a2c”. - Do not rely on Hermes to sign in your end users. One gateway belongs to one operator, who holds the Scalekit credentials. That operator's host acts for many end users, each with their own identifier, passed with `--identifier`. Confirm who each user is in your own app with [user verification](https://docs.scalekit.com/agentkit/user-verification/), and mint per-end-user Virtual MCP session tokens in your application, not inside Hermes. ## Common scenarios **How do I authorize a new connection?** The authstack skill returns an authorization link if the account is not `ACTIVE`. The plugin returns one the first time a user asks for an app. Open the link, finish the flow on the hosted page, then return to Hermes and retry. The hosted page adapts to the connection. An OAuth connector asks for your consent. An API key connector, such as Snowflake, asks for the credential. Hermes never collects the credential in chat. **How do I switch users?** This answer is for the `hermes-delegated-auth` skill. The Scalekit plugin creates one identifier and keeps it across updates. Set `SCALEKIT_IDENTIFIER` in `~/.hermes/.env` as the default. Name another user in the prompt to override the default for one turn, for example `as identifier usr_8f3a2c`. Hermes then passes `--identifier usr_8f3a2c` to the skill, and Scalekit scopes the tools to that user's connected account. **Why do I see "connection not found"?** 1. Confirm the connection exists in **AgentKit** > **Connections** 2. Confirm the connection is complete, not a draft 3. Confirm `SCALEKIT_ENVIRONMENT_URL` matches the environment that holds the connection Use the connection name, for example `github-connect`. Don't use the `conn_…` id. **The connected account is not ACTIVE** Check the state in **AgentKit** > **Connected accounts**: | State | What you do | |-------|-------------| | `PENDING_AUTH` | Open the authorization link and finish OAuth | | `PENDING_VERIFICATION` | Complete [user verification](https://docs.scalekit.com/agentkit/user-verification/) | | `EXPIRED` | Open a new authorization link. The token expired, or the user revoked access at the provider | | `DISCONNECTED` | The account was disconnected in Scalekit. Open a new authorization link, then retry | See [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/). **Can I use Virtual MCP with Hermes?** Use the Scalekit plugin or the `hermes-delegated-auth` skill on the Hermes host. The skill calls `execute_tool` with Scalekit client credentials and an identifier. No session token sits on the host for that skill, so nothing expires, and one host can serve many users and run cron jobs. When you want each run limited to a chosen set of tools, mint Virtual MCP session tokens in your own application and pass them to the agent, as in [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/). Mint a token per end user in your application, not inside Hermes. Do not set `auth: oauth` on a Scalekit Virtual MCP server. The server takes a bearer token, not MCP OAuth. **Why does `hermes mcp login` fail against Scalekit?** `hermes mcp login` performs MCP OAuth against a vendor's MCP server. A Scalekit Virtual MCP server does not use MCP OAuth. It takes a static URL plus a session token that **your application** mints with `create_session_token`. Set the bearer header in `config.yaml` instead. See [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/). ## Next - [Browse connectors](https://docs.scalekit.com/agentkit/connectors/): Every app AgentKit connects to, with its tools - [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/): Create a connected account and send the user an authorization link - [Example repo](https://github.com/scalekit-developers/hermes-agentkit-example): Runnable companion on scalekit-developers --- Source: https://docs.scalekit.com/agentkit/bring-your-own-connector/overview.md # When to add a connector Add your own connector when an app isn't in the AgentKit catalog. Scalekit still handles user authorization, stored credentials and the calls your agent makes. Add your own connector when the API or MCP server you need is not available in Scalekit's built-in catalog — custom connectors support any SaaS API, partner system, internal API, or remote MCP server while keeping authentication, authorization, and secure API access in Scalekit. Once the connector is created, you use the same flow as other connectors: create a connection, create or fetch a connected account, authorize the user, and perform tool calling. Custom connectors appear alongside built-in connectors when you create a connection in Scalekit: > Image: Custom connector shown alongside built-in connectors in the connector selection view ## Why add your own connector Adding your own connector lets you: - Extend beyond the built-in connector catalog without inventing a separate auth stack - Bring unsupported SaaS APIs, partner systems, internal APIs, and remote MCP servers into the same secure access model - Reuse connections, connected accounts, and user authorization instead of building one-off auth plumbing - Keep credential handling, authorization, and governed API access centralized in Scalekit - Move from connector definition to live upstream calls through the API proxy (REST) or tool calling (MCP) using the same runtime model as other integrations ## How adding your own connector works Adding your own connector uses the same model as built-in connectors: 1. Create a connector definition 2. Create a connection in Scalekit Dashboard 3. Create a connected account and authorize the user 4. Call tools — via the API proxy (`actions.request()`) for REST API connectors, or via MCP tool calling for MCP connectors Creating the connector definition tells Scalekit how to authenticate to the upstream API or MCP server. After that, connections, connected accounts, user authorization, and the call runtime work the same way as they do for built-in connectors. ## Next - [Create a connector](https://docs.scalekit.com/agentkit/bring-your-own-connector/create-connector/): Define how Scalekit authenticates to the app. - [Call your connector](https://docs.scalekit.com/agentkit/bring-your-own-connector/making-tool-calls/): Make API or MCP tool calls through your connector. --- Source: https://docs.scalekit.com/agentkit/bring-your-own-connector/create-connector.md # Create a connector Create a custom AgentKit connector for an app that isn't in the catalog: pick OAuth, API key, bearer or basic auth, build the definition and manage it by API. Create a custom connector to bring an unsupported API or MCP server into Scalekit's secure access model. This guide walks you through building the connector payload, creating the connector, and managing it over its lifecycle - list, update, and delete - with the management API. Check out the examples **Prerequisites** You need three credentials from your Scalekit environment: - `SCALEKIT_ENVIRONMENT_URL` - the base URL of your Scalekit environment - `SCALEKIT_CLIENT_ID` - your environment's client ID - `SCALEKIT_CLIENT_SECRET` - your environment's client secret Find these in the Scalekit Dashboard under **Developers → Settings → API Credentials**. ## Create a connector Create a connector in the Scalekit Dashboard or with the management API. The dashboard provides a guided form for MCP connectors; the management API gives you scriptable control over every connector type and auth pattern. ### Create an MCP connector in the dashboard Add an MCP connector through a guided form - no payload required. 1. In the Scalekit Dashboard, switch to **AgentKit**. 2. Select **Connectors**. 3. Select **Create custom connector**. 4. Complete the **Add MCP connector** form: - **Display name**: a name for the connector, such as `Example MCP`. - **Description**: a short description of what the connector connects to. - **Icon URL** (optional): an icon for the connector. Must start with `https://`. For best results, use an 800×800px SVG image, such as `https://cdn.example.com/icon.svg`. - **Server URL**: the base URL of the MCP server. Must start with `https://`, such as `https://app.example.com/mcp`. - **Metadata** (optional): key-value pairs for the connector. Values must be plain strings; nested objects are not supported. For **Auth type**, choose how users authenticate when they connect their account: - **OAuth**: users authorize access through the provider's OAuth flow, and Scalekit handles the token exchange. Scalekit finds the server's authorization and token endpoints from its OAuth metadata. When you create a connection, Scalekit registers an OAuth client for you if the server advertises Dynamic Client Registration. If the server doesn't support it, or registration fails, enter the client ID and client secret of [your own OAuth app](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/) in the connection, and add the connection's redirect URI to that app. - **Bearer token**: users provide a long-lived token issued by the provider. - **API key**: users provide an API key issued by the provider. - **No authentication**: the server is public and requires no credentials. Use this only when the server intentionally allows unauthenticated access and exposes no user-specific data or privileged operations, since every user shares the same anonymous access. 5. Select **Save**. The connector is ready to use when you create a connection. #### Match the server's token endpoint auth method An OAuth connection for a custom connector has a **Token Endpoint Auth Method** setting next to its OAuth endpoints, in **AgentKit** > **Connections**. It sets how Scalekit sends the client ID and secret to the server's token endpoint, both when the user first connects and on every token refresh: | Option | How Scalekit sends the client credentials | | ------ | ----------------------------------------- | | **Default** | In an HTTP Basic `Authorization` header. If the server rejects it, Scalekit retries once with the client ID in the request body as well | | **Authorization Header (Basic Auth)** | In an HTTP Basic `Authorization` header (`client_secret_basic`) | | **Request Body (Post params)** | As `client_id` and `client_secret` in the request body (`client_secret_post`) | | **None (Public Client)** | Only the client ID, in the request body, with no secret (`none`) | Choose the method the server expects. Its OAuth metadata lists them in `token_endpoint_auth_methods_supported`. When the method doesn't match, the first connection may work while token refreshes fail, so connected accounts need re-authorizing within hours. See [MCP accounts need re-authorizing within hours](https://docs.scalekit.com/agentkit/troubleshooting/#mcp-accounts-need-re-authorizing-within-hours). ### Create a connector with the management API Build the connector payload using the reference and examples that follow, then create the connector with the management API. **Understand the connector payload** Supported auth types are `OAUTH`, `BASIC`, `BEARER`, and `API_KEY`. Use `OAUTH` when the upstream API or MCP server requires a user authorization flow and token exchange. Use `BASIC`, `BEARER`, or `API_KEY` when it accepts static credentials or long-lived tokens. MCP providers use the same four auth types as REST API providers, with `is_mcp: true` set in each `auth_patterns[]` entry. OAuth MCP connectors use a simplified `oauth_config: {"pkce_enabled": true}`. Scalekit finds the server's OAuth endpoints from its metadata and registers a client through Dynamic Client Registration when the server advertises it. Otherwise, enter your own OAuth app's client ID and client secret in the connection. Non-OAuth MCP connectors omit `oauth_config` entirely. MCP connectors can also use `NO_AUTH` for public servers that require no credentials - set `is_mcp: true`, use an empty `fields: []`, and omit `oauth_config`. Use `NO_AUTH` only when the upstream intentionally allows unauthenticated public access and exposes no user-specific data or privileged operations; every user of the connector shares the same anonymous access. The connector payload uses these common top-level fields: - `display_name`: Human-readable name for the custom connector - `description`: Short description of what the connector connects to - `auth_patterns`: Authentication options supported by the connector - `proxy_url`: Base URL the proxy should call for the upstream API (mandatory) - `proxy_enabled`: Whether the proxy is enabled for the connector (mandatory, should be true) `proxy_url` can also include templated fields when the upstream API requires account-specific values, for example `https://{{domain}}/api`. Within `auth_patterns`, the most common fields are: - `type`: The auth type, such as OAUTH, BASIC, BEARER, or API_KEY - `display_name`: Label shown for that auth option - `description`: Short explanation of the auth method - `fields`: Inputs collected for static auth providers such as BASIC, BEARER, and API_KEY. These usually store values such as `username`, `password`, `token`, `api_key`, `domain`, or `version`. - `account_fields`: Inputs collected for OAUTH connectors when account-scoped values are needed. This is typically used for values tied to a connected account, such as named path parameters. - `oauth_config`: OAuth-specific configuration, such as authorize and token endpoints - `auth_header_key_override`: Custom header name when the upstream does not use `Authorization`. For example, some APIs expect auth in a header such as `X-API-Key` instead of the standard `Authorization` header. - `auth_field_mutations`: Value transformations applied before the credential is sent. This is useful when the upstream expects a prefix, suffix, or default companion value, such as adding a token prefix or setting a fallback password value for Basic auth. - `is_mcp`: Set to `true` when the upstream is an MCP server. Tells Scalekit to route the connector through MCP tool calling instead of the HTTP proxy. Below are example payloads for API and MCP connectors across all supported auth patterns. > caution: Stateless MCP servers only > > Scalekit connects to **stateless MCP servers** only. Stateful MCP servers that require persistent sticky connections or MCP session IDs are not supported. **API Connector** **OAuth** ```json { "display_name": "My Asana", "description": "Connect to Asana. Manage tasks, projects, teams, and workflow automation", "auth_patterns": [ { "type": "OAUTH", "display_name": "OAuth 2.0", "description": "Authenticate with Asana using OAuth 2.0 for comprehensive project management", "fields": [], "oauth_config": { "authorize_uri": "https://app.asana.com/-/oauth_authorize", "token_uri": "https://app.asana.com/-/oauth_token", "user_info_uri": "https://app.asana.com/api/1.0/users/me", "available_scopes": [ { "scope": "profile", "display_name": "Profile", "description": "Access user profile information", "required": true }, { "scope": "email", "display_name": "Email", "description": "Access user email address", "required": true } ] } } ], "proxy_url": "https://app.asana.com/api", "proxy_enabled": true } ``` **Bearer** ```json { "display_name": "My Bearer Token Provider", "description": "Connect to an API that accepts a static bearer token", "auth_patterns": [ { "type": "BEARER", "display_name": "Bearer Token", "description": "Authenticate with a static bearer token", "fields": [ { "field_name": "token", "label": "Bearer Token", "input_type": "password", "hint": "Your long-lived bearer token", "required": true } ] } ], "proxy_url": "https://api.example.com", "proxy_enabled": true } ``` **Basic** ```json { "display_name": "My Freshdesk", "description": "Connect to Freshdesk. Manage tickets, contacts, companies, and customer support workflows", "auth_patterns": [ { "type": "BASIC", "display_name": "Basic Auth", "description": "Authenticate with Freshdesk using Basic Auth with username and password for comprehensive helpdesk management", "fields": [ { "field_name": "domain", "label": "Freshdesk Domain", "input_type": "text", "hint": "Your Freshdesk domain (e.g., yourcompany.freshdesk.com)", "required": true }, { "field_name": "username", "label": "API Key", "input_type": "text", "hint": "Your Freshdesk API Key", "required": true } ] } ], "proxy_url": "https://{{domain}}/api", "proxy_enabled": true } ``` **API Key** ```json { "display_name": "My Attention", "description": "Connect to Attention for AI insights, conversations, teams, and workflows", "auth_patterns": [ { "type": "API_KEY", "display_name": "API Key", "description": "Authenticate with Attention using an API Key", "fields": [ { "field_name": "api_key", "label": "Integration Token", "input_type": "password", "hint": "Your Attention API Key", "required": true } ] } ], "proxy_url": "https://api.attention.tech", "proxy_enabled": true } ``` **MCP Connector** **OAuth** ```json { "display_name": "GitHub MCP", "description": "Connect to GitHub MCP", "auth_patterns": [ { "description": "Authenticate with GitHub MCP using browser OAuth.", "display_name": "OAuth 2.1/DCR", "fields": [], "is_mcp": true, "oauth_config": { "pkce_enabled": true }, "type": "OAUTH" } ], "proxy_url": "https://api.githubcopilot.com/mcp/", "proxy_enabled": true } ``` **Bearer** ```json { "display_name": "Apify MCP", "description": "Connect to Apify MCP to run web scraping, browser automation, and data extraction Actors directly from your AI workflows.", "auth_patterns": [ { "description": "Authenticate with Apify using your API Token.", "display_name": "Apify Token", "fields": [ { "field_name": "token", "hint": "Your Apify API Token", "input_type": "password", "label": "Apify Token", "required": true } ], "is_mcp": true, "type": "BEARER" } ], "proxy_url": "https://mcp.apify.com", "proxy_enabled": true } ``` **Basic** ```json { "display_name": "My Internal MCP", "description": "Connect to an internal MCP server that authenticates with a username and password", "auth_patterns": [ { "type": "BASIC", "display_name": "Basic Auth", "description": "Authenticate with a username and password", "is_mcp": true, "fields": [ { "field_name": "username", "label": "Username", "input_type": "text", "hint": "Your username", "required": true }, { "field_name": "password", "label": "Password", "input_type": "password", "hint": "Your password", "required": true } ] } ], "proxy_url": "https://mcp.internal.example.com", "proxy_enabled": true } ``` **API Key** ```json { "display_name": "My API Key MCP", "description": "Connect to an MCP server that authenticates with a static API key", "auth_patterns": [ { "type": "API_KEY", "display_name": "API Key", "description": "Authenticate with a static API key", "is_mcp": true, "fields": [ { "field_name": "api_key", "label": "API Key", "input_type": "password", "hint": "Your API key", "required": true } ] } ], "proxy_url": "https://mcp.example.com", "proxy_enabled": true } ``` **No Auth** ```json { "display_name": "Public Docs MCP", "description": "Connect to a public MCP server that requires no credentials", "auth_patterns": [ { "type": "NO_AUTH", "display_name": "No Auth", "description": "Public server - no credentials required.", "is_mcp": true, "fields": [] } ], "proxy_url": "https://mcp.example.com", "proxy_enabled": true } ``` **Before submitting, review the final payload carefully:** - `display_name` and `description` - The selected auth `type` - Required `fields` and `account_fields` - OAuth endpoints and scopes, if the connector uses OAuth - `proxy_url` - Whether `is_mcp` is set to `true` for MCP providers **Generate an access token** All API requests require a short-lived access token. Generate one using your `SCALEKIT_CLIENT_ID` and `SCALEKIT_CLIENT_SECRET`: ```bash curl --location "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode "client_id=$SCALEKIT_CLIENT_ID" \ --data-urlencode "client_secret=$SCALEKIT_CLIENT_SECRET" ``` Use the `access_token` value from the response as `$env_access_token` in the `curl` commands below. Use the payload for your auth type as the request body in the create request: **cURL** ```bash title="Terminal" # $env_access_token and $SCALEKIT_CLIENT_SECRET are secrets - keep them server-side and out of source control. # --fail-with-body makes curl exit non-zero and print the error body on a non-2xx response. curl --fail-with-body --location "$SCALEKIT_ENVIRONMENT_URL/api/v1/custom-providers" \ --header "Authorization: Bearer $env_access_token" \ --header "Content-Type: application/json" \ --data '{...}' ``` **Python** The Python SDK builds the payload with typed request objects and authenticates using your client credentials - no separate access token step is needed. It covers MCP connector auth types: OAuth (via Dynamic Client Registration), Bearer, API key, and No Auth. The example below creates an OAuth MCP connector; swap the `AuthPattern` for the auth type you need. ```python title="create_connector.py" import os from scalekit import ScalekitClient from dotenv import load_dotenv from scalekit.actions.types import AuthPattern, OAuthConfig, CreateCustomProviderRequest from scalekit.common.exceptions import ScalekitException load_dotenv() # Load credentials from the environment. Keep SCALEKIT_CLIENT_SECRET server-side - # never commit it or expose it in client-side code. scalekit_client = ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) try: response = scalekit_client.actions.providers.create_custom_provider( CreateCustomProviderRequest( display_name="GitHub MCP", description="Connect to GitHub MCP", proxy_url="https://api.githubcopilot.com/mcp/", proxy_enabled=True, auth_patterns=[ AuthPattern( type="OAUTH", display_name="OAuth 2.1/DCR", description="Authenticate with GitHub MCP using browser OAuth.", is_mcp=True, oauth_config=OAuthConfig(), # pkce_enabled=True by default ) ], # Optional: icon_src="https://cdn.example.com/icon.svg", # Optional: metadata={"team": "platform"}, ) ) print("Created connector:", response.provider.identifier) except ScalekitException as err: # Handle validation errors, conflicts (duplicate name), auth failures, etc. print("Failed to create connector:", err) raise ``` A successful request returns the created connector. Next, create a connection in the Scalekit Dashboard, then continue with the standard connector flow to authorize users and call tools. ## List connectors List existing connectors before you create one, to confirm whether a connector for the upstream already exists. You also need the list to find a connector's `identifier` for update and delete requests. **cURL** ```bash title="Terminal" # $env_access_token is a secret - keep it server-side and out of source control. curl --fail-with-body --location "$SCALEKIT_ENVIRONMENT_URL/api/v1/providers?filter.provider_type=CUSTOM&page_size=1000" \ --header "Authorization: Bearer $env_access_token" ``` **Python** The Python SDK lists MCP connectors. For REST connectors, call the API directly: ```bash title="Terminal" curl --fail-with-body --location "$SCALEKIT_ENVIRONMENT_URL/api/v1/providers?filter.provider_type=CUSTOM&page_size=1000" \ --header "Authorization: Bearer $env_access_token" ``` For MCP connectors: ```python title="list_connectors.py" import os from scalekit import ScalekitClient from dotenv import load_dotenv from scalekit.actions.types import ListProvidersRequest from scalekit.v1.providers.providers_pb2 import ProviderType from scalekit.common.exceptions import ScalekitException load_dotenv() # Keep SCALEKIT_CLIENT_SECRET server-side - never commit it or expose it client-side. scalekit_client = ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) try: response = scalekit_client.actions.providers.list_providers( ListProvidersRequest(provider_type=ProviderType.CUSTOM, page_size=1000) ) for provider in response.providers: print(provider.identifier, provider.display_name) except ScalekitException as err: print("Failed to list connectors:", err) raise ``` ## Update a connector Use the [List connectors](#list-connectors) API to get the connector `identifier`, then send the updated payload. Include `display_name`, `proxy_url`, and `auth_patterns` on every update, and echo back any other fields you want to keep - omitted fields are not preserved, so read the current connector first and change only what you need. **cURL** ```bash title="Terminal" # $env_access_token and $SCALEKIT_CLIENT_SECRET are secrets - keep them server-side and out of source control. curl --fail-with-body --location --request PUT "$SCALEKIT_ENVIRONMENT_URL/api/v1/custom-providers/$PROVIDER_IDENTIFIER" \ --header "Authorization: Bearer $env_access_token" \ --header "Content-Type: application/json" \ --data '{...}' ``` **Python** The Python SDK updates MCP connectors. For REST connectors, call the API directly: ```bash title="Terminal" curl --fail-with-body --location --request PUT "$SCALEKIT_ENVIRONMENT_URL/api/v1/custom-providers/$PROVIDER_IDENTIFIER" \ --header "Authorization: Bearer $env_access_token" \ --header "Content-Type: application/json" \ --data '{...}' ``` For MCP connectors: ```python title="update_connector.py" import os from scalekit import ScalekitClient from dotenv import load_dotenv from scalekit.actions.types import ListProvidersRequest, UpdateCustomProviderRequest from scalekit.common.exceptions import ScalekitException load_dotenv() # Keep SCALEKIT_CLIENT_SECRET server-side - never commit it or expose it client-side. scalekit_client = ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) provider_identifier = "prov_..." # from list_providers try: # Read the current state, then echo back every field you want to keep. current = scalekit_client.actions.providers.list_providers( ListProvidersRequest(identifier=provider_identifier) ).providers[0] response = scalekit_client.actions.providers.update_custom_provider( UpdateCustomProviderRequest( identifier=current.identifier, display_name=current.display_name, proxy_url=current.proxy_url, description="Updated description", auth_patterns=current.auth_patterns, icon_src=current.icon_src, metadata=dict(current.metadata), ) ) print("Updated connector:", response.provider.identifier) except ScalekitException as err: print("Failed to update connector:", err) raise ``` ## Delete a connector Use the [List connectors](#list-connectors) API to get the connector `identifier`. If the connector is still in use, remove the related connections or connected accounts first. **cURL** ```bash title="Terminal" # $env_access_token is a secret - keep it server-side and out of source control. curl --fail-with-body --location --request DELETE "$SCALEKIT_ENVIRONMENT_URL/api/v1/custom-providers/$PROVIDER_IDENTIFIER" \ --header "Authorization: Bearer $env_access_token" ``` **Python** ```python title="delete_connector.py" import os from scalekit import ScalekitClient from dotenv import load_dotenv from scalekit.actions.types import DeleteCustomProviderRequest from scalekit.common.exceptions import ScalekitException load_dotenv() # Keep SCALEKIT_CLIENT_SECRET server-side - never commit it or expose it client-side. scalekit_client = ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) try: scalekit_client.actions.providers.delete_custom_provider( DeleteCustomProviderRequest(identifier="prov_...") ) print("Connector deleted.") except ScalekitException as err: # e.g. not found, or forbidden if the connector is still in use. print("Failed to delete connector:", err) raise ``` ## Next - [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/): Create a connected account for the connector and send the user an authorization link. - [Call your connector](https://docs.scalekit.com/agentkit/bring-your-own-connector/making-tool-calls/): Make API or MCP tool calls through your connector. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Create a custom connector](https://docs.scalekit.com/agentkit/reference/custom-connectors/create-a-custom-connector.md) - `PUT` [Update a custom connector](https://docs.scalekit.com/agentkit/reference/custom-connectors/update-a-custom-connector.md) - `DELETE` [Delete a custom connector](https://docs.scalekit.com/agentkit/reference/custom-connectors/delete-a-custom-connector.md) --- Source: https://docs.scalekit.com/agentkit/bring-your-own-connector/making-tool-calls.md # Call your connector Call a custom AgentKit connector as a user: send REST requests through the API proxy with actions.request, or list and run the tools of a custom MCP connector. Make tool calls to your custom connector once the connector, its connection and the user's connected account are set up. The call method depends on the connector type: - **REST API connectors**: call the API through the proxy with `actions.request()`. - **MCP connectors**: list the connector's tools, then call them with `execute_tool`. Both types use the same connection, connected account, and user authorization model. ## Prerequisites Make sure: - The connector exists and is configured with the right [auth pattern](https://docs.scalekit.com/agentkit/bring-your-own-connector/create-connector) - A [connection](https://docs.scalekit.com/agentkit/connections) is configured for the connector - The [connected account](https://docs.scalekit.com/agentkit/connected-accounts) exists - The user has completed [authorization](https://docs.scalekit.com/agentkit/tools/authorize) Create a connection for your connector in the Scalekit Dashboard: > Image: Connections page showing a custom connector connection alongside built-in connectors After the user completes authorization, the connected account appears in the Connected Accounts tab: > Image: Connected Accounts tab showing an authenticated account for a custom connector ## REST API proxy calls Call a REST API connector through the API proxy, the same way you call any catalog connector. [Call any API](https://docs.scalekit.com/agentkit/tools/custom-tools/) has the full steps in Python, Node.js and cURL, including the account check and error handling. Two things are specific to a custom connector: - `path` is relative to the connector's `proxy_url`, not the app's public base URL. - The connector definition controls how Scalekit authenticates the call, so the request looks the same whether the connector uses OAuth, an API key or basic auth. **Python** ```python response = actions.request( connection_name="your-provider-connection", # from AgentKit > Connections identifier="user_123", method="GET", path="/v1/customers", # relative to the connector's proxy_url ) print(response.status_code, response.json()) ``` **Node.js** ```typescript const response = await scalekit.actions.request({ connectionName: 'your-provider-connection', // from AgentKit > Connections identifier: 'user_123', method: 'GET', path: '/v1/customers', // relative to the connector's proxy_url }); console.log(response.status, response.data); ``` ## MCP tool calling An MCP connector's tools come from the upstream MCP server, and you call them exactly like built-in tools: list the scoped tools for the connection to get their names and input schemas, then call `execute_tool`. [Use built-in tools](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/) covers both steps in Python, Node.js and cURL, and how to read the result. **Python** ```python from google.protobuf.json_format import MessageToDict page, _ = actions.tools.list_scoped_tools( identifier="user_123", filter={"connection_names": ["your-mcp-connection"]}, page_size=100, ) print([MessageToDict(t.tool)["definition"]["name"] for t in page.tools]) result = actions.execute_tool( tool_name="tool_name_from_the_list", identifier="user_123", connection_name="your-mcp-connection", tool_input={"key": "value"}, # match the tool's input_schema ) print(result.data) ``` **Node.js** ```typescript const page = await scalekit.tools.listScopedTools('user_123', { filter: { connectionNames: ['your-mcp-connection'] }, pageSize: 100, }); console.log(page.tools.map((t) => t.tool?.definition?.name)); const result = await scalekit.actions.executeTool({ toolName: 'tool_name_from_the_list', identifier: 'user_123', connector: 'your-mcp-connection', toolInput: { key: 'value' }, // match the tool's input_schema }); console.log(result.data); ``` ## Next - [Environments and regions](https://docs.scalekit.com/agentkit/environments/): Create a Production environment and choose a region. - [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to finish before real users connect. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Execute a tool](https://docs.scalekit.com/agentkit/reference/tools/execute-a-tool.md) - `GET` [List a user's scoped tools](https://docs.scalekit.com/agentkit/reference/tools/list-scoped-tools.md) --- Source: https://docs.scalekit.com/agentkit/environments.md # Environments and regions Use Development and Production environments in AgentKit: create Production, choose the US or EU region, find each environment URL and switch between them. An environment is an isolated copy of AgentKit: its own connections, connected accounts, API credentials, settings and logs. You build in a Development environment, then create a Production environment for real users. This page covers both, how regions work, and how to move between environments. ## What you get at signup Signing up creates a workspace with one **Development** environment. Development is free and meant for building and testing. Every new environment, Development or Production, starts with: - A GitHub connection named `github-connect`, which uses Scalekit's OAuth app, so you can make a tool call before you configure anything. The [Quickstart](https://docs.scalekit.com/agentkit/quickstart/) uses it. - A client ID. Generate a client secret yourself, as shown in [API credentials](https://docs.scalekit.com/agentkit/api-credentials/). Environments don't share anything. A connection, a connected account or a client secret in Development doesn't exist in Production, so you set up Production's connections again and each user connects again there. ## Development and Production | | Development | Production | | --- | --- | --- | | Environment URL | `https://.scalekit.dev` | `https://.scalekit.com` | | Billing | Always free | Billed on its own plan. See [Billing](https://docs.scalekit.com/agentkit/billing/) | | Client IDs | Start with `skc_` | Start with `prd_` | | User verification | All modes, including **Scalekit users only** for testing | **Custom user verifier** or **None** | | Custom domain | Not available | Available on paid plans. See [Custom domain](https://docs.scalekit.com/agentkit/advanced/custom-domain/) | | Quickstart in the dashboard | Shown | Hidden | In the EU region, environment URLs end in `.eu.scalekit.dev` and `.eu.scalekit.com`. ## Choose a region Scalekit runs separate US and EU regions with no shared data. Choose the region on the signup page, with **Change Region**, before you create your account. Your workspace and all its environments stay in that region, and you can't move a workspace between regions later. To use both, create a workspace in each. EU data residency is an add-on to your plan. When you create an environment in an EU workspace, the dashboard asks you to confirm it. See [Where data is hosted](https://docs.scalekit.com/agentkit/security/#where-data-is-hosted). ## Create a Production environment 1. ### Enable Production Open the environment switcher at the top left of the dashboard, next to your workspace name, and select **Enable Production**. Enter a card in the **Enable Production** form. When it's saved, the dashboard shows **Production enabled** and opens **Manage Environments**. Only Admins can add a payment method. See [Team members and roles](https://docs.scalekit.com/agentkit/team-members/). 2. ### Create the environment Select **Create Environment**, enter a **Name**, choose **Production** as the environment type, and select **Create**. You can't change an environment's type after you create it. 3. ### Set it up for real users In the new environment: - Generate a client secret and load the new environment URL, client ID and secret into your production configuration. See [API credentials](https://docs.scalekit.com/agentkit/api-credentials/). - Create your connections. For your own branding on consent screens, use [your own OAuth app](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/). - Set **AgentKit** > **Settings** > **User Verification** to **Custom user verifier**. See [Verify users](https://docs.scalekit.com/agentkit/user-verification/). A new Production environment starts on the Free plan. Change it in **Billing**. See [Billing](https://docs.scalekit.com/agentkit/billing/). ## Switch environments The environment switcher at the top left shows the current environment. Select it to pick another environment. Everything in the dashboard, including the **AgentKit** pages, then shows that environment. To see every environment or rename one, select **Manage Environments** in the switcher, or open **Workspace** > **Environments**. To delete an environment, contact [support](mailto:support@scalekit.com). ## Check it worked Your code talks to the environment whose URL and credentials it uses. Call the API with the Production values, for example with the [Quickstart](https://docs.scalekit.com/agentkit/quickstart/), and check that the connected account appears in **AgentKit** > **Connected Accounts** with Production selected in the switcher. ## Common problems ### Production is locked with "Requires a payment method" The workspace has no payment method yet. Select **Enable Production** in the environment switcher and add a card, or ask an Admin to. ### A connected account exists in Development but not in Production Environments share nothing. Create the connection in Production and have the user authorize again there. ### My code reaches the wrong environment The environment URL, client ID and client secret must all come from the same environment. A secret from Development fails against a Production URL. ## Next - [API credentials](https://docs.scalekit.com/agentkit/api-credentials/): Get the environment URL, client ID and a client secret for your code. - [Launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to check before real users connect. --- Source: https://docs.scalekit.com/agentkit/api-credentials.md # API credentials Find your AgentKit environment URL, client ID and client secret in the Scalekit dashboard, load them as environment variables, and rotate a secret safely. Your code authenticates to Scalekit with three values: the environment URL, a client ID and a client secret. This page shows where to find them, how to load them, and how to rotate a secret without downtime. ## Before you start - A Scalekit account. Signing up creates a Development environment. See [Environments and regions](https://docs.scalekit.com/agentkit/environments/). - The **Admin** or **Developer** role. Members can't see API credentials. See [Team members and roles](https://docs.scalekit.com/agentkit/team-members/). ## Get your credentials Each environment has its own credentials. Check that the environment switcher at the top left shows the environment you want, then: 1. Open **Developers** > **Settings** > **API Credentials**. 2. Copy the **Environment URL** and the **Client ID** from **Environment details**. 3. Under **Client secrets**, select **Generate new secret**, then **Copy to clipboard**. The secret is shown once. Scalekit stores only a hash of it, so if you lose it, generate a new one. New environments start with no secret. ## Load them in your code Your code reads these environment variables. Keep them in a `.env` file that you don't commit, or in your platform's secret manager. To load a `.env` file, install `python-dotenv` (Python) or `dotenv` (Node.js): ```bash title=".env" SCALEKIT_ENVIRONMENT_URL=https://.scalekit.dev SCALEKIT_CLIENT_ID= SCALEKIT_CLIENT_SECRET= ``` **Python** ```python import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() # reads .env into the environment scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions ``` **Node.js** ```ts import 'dotenv/config'; // reads .env into process.env import { ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); ``` **cURL** ```bash set -a; source .env; set +a # exports the .env values to this shell TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \ -d grant_type=client_credentials \ -d client_id="$SCALEKIT_CLIENT_ID" \ -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token) ``` Use these credentials only on your server. Never put the client secret in browser or mobile code, where anyone can read it. ## Rotate a client secret An environment can have two secrets at a time, so you can switch to a new one before the old one stops working: 1. In **Developers** > **Settings** > **API Credentials**, select **Generate new secret** and copy it. 2. Deploy the new secret to every service that uses the old one. 3. Check that the old secret's **Last used** time stops updating. 4. Select **Delete** on the old secret and confirm. It stops working immediately. Rotate right away if a secret may have leaked, for example after it was committed to a repository or shown in logs. ## Check it worked Run the cURL example: it prints an access token. With an SDK, any call such as `actions.get_connected_account` succeeds instead of returning `401`. The secret's **Last used** time updates in the dashboard. ## Common problems ### `401` or `invalid_client` The client ID, secret and environment URL don't all come from the same environment, or the secret was deleted. Copy all three again from the environment you're calling. ### **Generate new secret** is disabled The environment already has two secrets. Delete the one you no longer use, then generate a new one. ### **Delete** is disabled It's the environment's only secret. Generate a new one first, so your code always has a working secret. ### I can't see **API Credentials** Your role doesn't include API credentials. Ask an Admin to give you the **Developer** role. ## Next - [Quickstart](https://docs.scalekit.com/agentkit/quickstart/): Make your first tool call with these credentials. - [Security and compliance](https://docs.scalekit.com/agentkit/security/): How Scalekit stores credentials, and what your app is responsible for. --- Source: https://docs.scalekit.com/agentkit/team-members.md # Team members and roles Invite teammates to your Scalekit workspace, give them the Admin, Developer, Member or No Access role, override a role per environment, or create custom roles. Invite your team to the Scalekit dashboard and give each person a role that matches their work. A role applies to every environment in the workspace unless you override it for one environment. ## Before you start - The **Admin** role. Other roles can see the team but can't change it. ## Roles | Role | For | | --- | --- | | **Admin** | Full access to everything, including members, roles and billing. | | **Developer** | Build and configure AgentKit. Can't manage members, roles or billing. | | **Member** | Read-only access to settings and logs. | | **No Access** | Can't open the dashboard. Use it to suspend someone without removing them. | What each role can do in AgentKit: | Area | Admin | Developer | Member | | --- | --- | --- | --- | | Connections: view | Yes | Yes | Yes | | Connections: create and edit | Yes | Yes | No | | Run tools in the **Playground** | Yes | No | No | | Connected accounts: view and create | Yes | Yes | No | | Connected accounts: view credentials and revoke | Yes | No | No | | API credentials: view, generate and delete secrets | Yes | Yes | No | | Tool call logs | Yes | Yes | Yes | | Webhooks: view | Yes | Yes | Yes | | Webhooks: create and edit | Yes | Yes | No | | Environments: create | Yes | No | No | | Environments: rename | Yes | Yes | No | | Billing | Yes | No | No | | Team, roles and environment access: view | Yes | Yes | Yes | | Team, roles and environment access: change | Yes | No | No | **No Access** can do none of these. ## Invite a teammate 1. Open **Workspace** > **Team Members** and select **Invite**. 2. Enter their **Email**, choose a **Role**, and select **Invite**. The person appears in **Team Members** with the role you chose. ## Change a role In **Team Members**, open the member's menu and select **Edit role**. Choose a new **Workspace role** and select **Save**. ### Give a different role in one environment Under **Environment access** in the same drawer, select **Set override** next to an environment and choose the role for that environment only. For example, make someone an Admin in Development and a Member in Production. **Reset to Default** removes the override. ## Remove a teammate In **Team Members**, open the member's menu, select **Remove Member**, and confirm with **Delete**. To block access but keep the person's record, give them **No Access** instead. ## Create a custom role When the four roles don't fit, open **Workspace** > **Roles** and select **Create role**. Give it a **Role name** and **Description**, then choose its **Permissions**, or start from a preset such as **Support** or **Billing Owner**. You can only grant permissions your own role has. Custom roles appear in the **Role** list when you invite or edit a member. ## Check it worked Ask the teammate to sign in. They see only the pages their role allows: for example, a Member doesn't see **API Credentials**, and a Developer doesn't see **Billing**. ## Common problems ### I don't see **Invite** or **Edit role** Your role can't change the team. Ask an Admin. ### A Developer can't run a tool in the Playground Running tools uses a user's live credentials, so only Admins can. Give the person the Admin role in a Development environment with an environment override, or create a custom role that includes it. ## Next - [Environments and regions](https://docs.scalekit.com/agentkit/environments/): Development and Production, and how to create Production. - [Security and compliance](https://docs.scalekit.com/agentkit/security/): What Scalekit protects, and what your app is responsible for. --- Source: https://docs.scalekit.com/agentkit/billing.md # Billing Manage your AgentKit subscription in the Scalekit dashboard: add a payment method, change a Production environment's plan, track tool calls and view invoices. Manage your subscription from the Scalekit dashboard: add a payment method, choose a plan for each Production environment, track usage and view invoices. For plan prices and what each includes, see [pricing](https://www.scalekit.com/pricing). ## How billing works - **Development is free.** Development environments are always on the Free plan and never billed. - **Each Production environment has its own plan.** Two Production environments can be on different plans. - **One payment method and billing address for the workspace.** They apply to every environment. - **AgentKit usage is measured in tool calls.** Each tool call counts once, whether it succeeds or the app returns an error. Calls rejected because the tool or connected account doesn't exist, and calls that hit a Scalekit internal error, don't count. Connected accounts aren't limited. - **Going over the Free plan upgrades you.** When a Production environment on the Free plan uses up its tool calls, it moves to the next plan automatically, as long as the workspace has a payment method. Without one, tool calls stop until you add a card. ## Before you start - The **Admin** role. Other roles can't see billing. See [Team members and roles](https://docs.scalekit.com/agentkit/team-members/). - A Production environment selected in the environment switcher. **Billing** appears in the sidebar only in Production. ## Add a payment method Open the environment switcher at the top left and select **Enable Production**. Enter a card in the form. The same card pays for every Production environment in the workspace. See [Create a Production environment](https://docs.scalekit.com/agentkit/environments/#create-a-production-environment). ## Change your plan 1. With a Production environment selected, open **Developers** > **Billing**. The **Overview** tab shows **Your plan**. 2. Select **Manage plan**, choose a plan, then select **Confirm change**. The dashboard shows **Plan updated successfully.** The change applies to this environment only. To move to a lower plan, or to cancel, contact [support](mailto:support@scalekit.com): lower plans show **Contact support to downgrade**. For Enterprise, select **Talk to sales**. ## Track usage The **Usage** tab shows the current **Billing period** and, for each metered item such as tool calls, how much you've used against your plan's allowance. ## Update billing details On the **Overview** tab, **Billing Information** shows the name, email address, address and billing period for the workspace. Select **Update billing info** or **Update payment details** to change them in the Stripe billing portal. ## View invoices The **Invoices** tab lists this environment's invoices. You can also select **View past invoices** under **Billing Information**. ## Common problems ### I don't see **Billing** in the sidebar Either a Development environment is selected, or your role isn't Admin. Switch to a Production environment, or ask an Admin. ### "You have exceeded the free usage quota" A Production environment on the Free plan used up its tool calls, and the workspace has no payment method. Tool calls fail until you add one: select **Enable Production** in the environment switcher and enter a card. ### "Your invoice is overdue" A payment failed. Select **Pay Bill** to pay it, and update your card with **Update payment details** so the next one goes through. ### "Add a payment method to see invoices." The workspace has no payment method yet. Add one as shown in [Add a payment method](#add-a-payment-method). ## Next - [Environments and regions](https://docs.scalekit.com/agentkit/environments/): Create and switch between Development and Production. - [Pricing](https://www.scalekit.com/pricing): Plan prices and what each plan includes. --- Source: https://docs.scalekit.com/agentkit/enterprise-features.md # Enterprise features Workspace controls for security and compliance reviews: SSO and SCIM for the Scalekit dashboard, custom roles, log streaming to your SIEM and your own encryption key. These features help your security and IT teams control who manages AgentKit, see what your agents do, and hold the keys to your data. Which ones you can use depends on your plan; see [pricing](https://www.scalekit.com/pricing), or [contact us](https://docs.scalekit.com/support/contact-us/) to turn one on. Encryption doesn't depend on your plan: Scalekit always encrypts your data at rest with a key specific to each environment. Bring your own key when you want to hold that key yourself. | Feature | What it gives you | |---|---| | **SSO for the Scalekit dashboard** | Your team signs in to the Scalekit dashboard through your identity provider. [Contact us](https://docs.scalekit.com/support/contact-us/) to set it up | | **SCIM for the Scalekit dashboard** | Your identity provider adds and removes dashboard members as people join or leave. [Contact us](https://docs.scalekit.com/support/contact-us/) to set it up | | **[Custom roles](https://docs.scalekit.com/agentkit/team-members/#create-a-custom-role)** | Give each teammate only the dashboard permissions they need | | **[Log streaming](https://docs.scalekit.com/agentkit/log-streaming/)** | Send tool calls and connected account changes to Datadog, CrowdStrike or Google SecOps | | **[Bring your own key](https://docs.scalekit.com/agentkit/encryption-keys/#set-up-byok-with-gcp-kms)** | Encrypt your environment's data with a key you hold in your own Google Cloud KMS, in place of the Scalekit-managed key, and revoke Scalekit's access to it | ## Next - [Stream logs to your SIEM](https://docs.scalekit.com/agentkit/log-streaming/): Send tool calls and account changes to Datadog, CrowdStrike or Google SecOps. - [Security and compliance](https://docs.scalekit.com/agentkit/security/): What Scalekit protects, and what your app is responsible for. --- Source: https://docs.scalekit.com/agentkit/log-streaming.md # Stream logs to your SIEM Stream AgentKit tool calls, connected account changes and other events to Datadog, CrowdStrike or Google SecOps, and check what was delivered. Send a record of your agents' tool calls, and of changes to your users' connected accounts, to your security team's SIEM. Scalekit pushes each event to the destination as it happens, so you can alert on it and keep it with the rest of your audit logs. ## Before you start - Log streaming depends on your plan. If it isn't on yours, the **Log Streaming** page says so. [Contact us](https://docs.scalekit.com/support/contact-us/) to turn it on. - An Admin role in the Scalekit dashboard, or a [custom role](https://docs.scalekit.com/agentkit/team-members/#create-a-custom-role) that can manage log streaming. - An account with one of these destinations: | Destination | What you need | |---|---| | Datadog | Your Datadog **Site**, such as `datadoghq.com`, and an **API key** | | CrowdStrike | An **Ingest URL** and an **Ingest token** from a CrowdStrike data connector | | Google SecOps | Set up with Scalekit: you grant a Scalekit service account access to your instance instead of sharing a credential. [Contact us](https://docs.scalekit.com/support/contact-us/) | Using a different log destination? [Contact us](https://docs.scalekit.com/support/contact-us/) and tell us which one. Each environment streams separately. Set up a destination in Production to capture your users' activity. ## Create a destination 1. In the Scalekit dashboard, switch the product to **Auth for MCP & SaaS** and go to **Developers** > **Log Streaming**. Log streaming covers your AgentKit events too. 2. Select **Create destination** and choose the **Provider**. 3. Enter a **Display name**, such as `Production SIEM`, and the destination's details from the table above. 4. Under **Events to stream**, keep every category checked, or uncheck the ones you don't want. For AgentKit, keep **Agent Tool Calls** and **Connected Accounts**. 5. Make sure **Stream events to this destination** is on, then select **Create destination**. To stop streaming without losing the setup, edit the destination and switch **Stream events to this destination** off. ## What AgentKit streams | Category | Events | |---|---| | **Agent Tool Calls** | `connected_account.tool_executed`, one for each tool call made on a user's connected account. A call that fails before Scalekit finds the connected account sends no event | | **Connected Accounts** | `connected_account.created`, `connected_account.updated`, `connected_account.deleted`, `connected_account.status_updated`, `connected_account.magic_link_generated`, `connected_account.oauth_succeeded`, `connected_account.oauth_tokens_fetched`, `connected_account.token_refresh_succeeded`, `connected_account.token_refresh_failed`, `connected_account.tool_discovery_failed` | | **Admin & Configuration** | Changes to the environment's configuration | Each event has the same envelope as an [account event webhook](https://docs.scalekit.com/agentkit/account-events/): `spec_version`, `id`, `type`, `occurred_at`, `environment_id`, `object` and `data`. A tool call event's `data` says who called which tool, and how it went: - **The tool:** `tool_name`, `tool_provider`, `tool_execution_id` and `tool_execution_duration_ms`. - **The user:** `connected_account_id`, `connected_account_identifier`, `connection_id` and `connection_name`. - **The outcome:** `status` and `is_error`, plus `error_code` and `error_message` when the call failed. - **The caller,** when known: `source`, `agent_id` and `agent_run_id`. The tool's input and output are not included. > note > > An event can be delivered more than once. Use the event's `id` to drop duplicates in your SIEM. ## Check it worked 1. In the destination's menu, select **Test connection**. Scalekit sends a test event, and the dashboard shows **Connection succeeded.** when the destination accepts it. 2. Search your SIEM for `type:scalekit.logstream.connectivity_test` to confirm it arrived. 3. Make a tool call, then open **View deliveries** on the destination. The `connected_account.tool_executed` event shows as **Delivered**. If a delivery fails, Scalekit retries it. **View deliveries** shows each attempt with its HTTP status and when the next retry is due. ## Next - [Enterprise features](https://docs.scalekit.com/agentkit/enterprise-features/): SSO, SCIM, custom roles and your own encryption key. - [Security and compliance](https://docs.scalekit.com/agentkit/security/): What Scalekit protects, and what your app is responsible for. --- Source: https://docs.scalekit.com/agentkit/encryption-keys.md # Manage encryption keys Protect AgentKit data at rest with Scalekit-managed encryption keys, or bring your own key from Google Cloud Key Management Service (KMS) and rotate it. Scalekit always encrypts data at rest, with a key specific to each environment. By default Scalekit manages that key for you; you can also bring your own from a cloud Key Management Service (KMS) provider if your compliance policy requires you to own and control the root key. This guide shows you how to view and manage keys in the dashboard and set up Bring Your Own Key (BYOK) with GCP Cloud KMS. ## Key types | Type | Description | |------|-------------| | **Scalekit managed Data Encryption Key (DEK)** | Scalekit generates and manages the key. No setup required. | | **Bring your own key (BYOK)** | You provide a key from your own KMS. You own the key lifecycle, including rotation and revocation. | ## Key states | State | Description | |-------|-------------| | | Created, not yet in use. No data is encrypted with it. | | | The active key. All new encryption operations use it. | | | Replaced by a newer key. Existing records may still reference it until re-encryption completes. | ## Manage keys Go to **Settings** and open the **Encryption keys** tab. 1. **Create a key** Click **Create New Key** and select a provider: - **Scalekit Managed DEK**: Scalekit generates and manages the key. Click **Create**. - **BYOK - GCP Cloud KMS**: Complete [Set up BYOK with GCP KMS](#set-up-byok-with-gcp-kms) first to create the GCP key and grant access, then enter the key resource name here and click **Create**. The key is created in **Staged** state. 2. **Activate the key** Click **Activate** on a Staged key. The key becomes Primary and Scalekit uses it for all new encryption operations. > note: One primary key at a time > > Creating a new key does not replace the current Primary key. The new key stays Staged until you explicitly activate it. 3. **Re-encrypt existing data** To apply the new key to existing records, click **Re-encrypt Data**. Scalekit decrypts each existing record with the previous key and re-encrypts it with the new key. ## Set up BYOK with GCP KMS Use BYOK to register an encryption key from Google Cloud KMS that your team owns and controls. Scalekit uses the KMS API for all encrypt and decrypt operations and never stores the key material. Whether you can use BYOK depends on your plan; see [pricing](https://www.scalekit.com/pricing). Encryption at rest with a Scalekit-managed key doesn't. > caution: You own the key lifecycle > > BYOK gives you direct control over your encryption key. Scalekit cannot rotate or manage the GCP key on your behalf. If the key is disabled, destroyed, or Identity and Access Management (IAM) access is revoked, Scalekit cannot encrypt new data or decrypt existing records until you restore access. ### Prerequisites - A **Google Cloud project** with the [Cloud KMS API enabled](https://cloud.google.com/kms/docs/create-encryption-keys) - **IAM permissions** to create key rings, keys, and set key-level IAM policies - The **Scalekit service account email**, shown in the Scalekit dashboard when you select BYOK - `gcloud` CLI installed and authenticated, or access to the [GCP Console](https://console.cloud.google.com/security/kms) 1. **Create a key ring** A [key ring](https://cloud.google.com/kms/docs/resource-hierarchy#key_rings) groups encryption keys by location. Key rings cannot be deleted or renamed once created. Choose the name and location carefully. **gcloud CLI** ```bash gcloud kms keyrings create "scalekit-kms-keyring" \ --location "global" \ --project "YOUR-GCP-PROJECT" ``` Replace `YOUR-GCP-PROJECT` with your Google Cloud project ID. **GCP Console** 1. Open [**Security > Key management**](https://console.cloud.google.com/security/kms), click **Key rings**. 2. Click **Create key ring**, enter `scalekit-kms-keyring` as the name. 3. Set **Location** to **Global** and click **Create**. > tip: Choose the right location > > Use `global` if your Scalekit environment has no regional data-residency requirement. For region-specific compliance, choose a location such as `us-east1` or `europe-west1`. See [Cloud KMS locations](https://cloud.google.com/kms/docs/locations). 2. **Create an encryption key** Create a symmetric AES-256-GCM key inside the key ring. **gcloud CLI** ```bash gcloud kms keys create "scalekit-kms-key" \ --location "global" \ --keyring "scalekit-kms-keyring" \ --purpose "encryption" \ --protection-level "hsm" \ --project "YOUR-GCP-PROJECT" ``` **GCP Console** 1. Click the key ring you created, then click **Create key**. 2. Enter `scalekit-kms-key` as the name. 3. Set **Protection level** to **HSM** (recommended) and click **Continue**. 4. Set **Key material** to **HSM-generated** and click **Continue**. 5. Set **Purpose** to **Symmetric encrypt/decrypt** and click **Continue**. 6. Set **Rotation period** per your policy and click **Create**. 3. **Grant Scalekit access to the key** Grant both roles at the key level to limit Scalekit's IAM access to this specific key. | Role | Purpose | |------|---------| | `roles/cloudkms.cryptoKeyEncrypterDecrypter` | Encrypt and decrypt the DEK | | `roles/cloudkms.viewer` | Read key metadata for health checks and listing | **gcloud CLI** ```bash gcloud kms keys add-iam-policy-binding "scalekit-kms-key" \ --keyring "scalekit-kms-keyring" \ --location "global" \ --project "YOUR-GCP-PROJECT" \ --member "serviceAccount:SCALEKIT-SERVICE-ACCOUNT" \ --role "roles/cloudkms.cryptoKeyEncrypterDecrypter" gcloud kms keys add-iam-policy-binding "scalekit-kms-key" \ --keyring "scalekit-kms-keyring" \ --location "global" \ --project "YOUR-GCP-PROJECT" \ --member "serviceAccount:SCALEKIT-SERVICE-ACCOUNT" \ --role "roles/cloudkms.viewer" ``` **GCP Console** 1. In the GCP Console, open **Security > Key management**, click **Key rings**, then click the key ring name. 2. Click the key name, then open the **Permissions** tab. 3. Click **Grant access**. A side panel opens. 4. In the **New principals** field, enter the Scalekit service account email. Copy it from **Settings > Encryption keys > Create New Key > BYOK - GCP Cloud KMS** in the Scalekit dashboard. 5. In the **Assign Roles** section, select **Cloud KMS CryptoKey Encrypter/Decrypter** from the first **Role** dropdown. 6. Click **+ Add another role** and select **Cloud KMS Viewer**. 7. Click **Save**. The policy update takes effect within a few minutes. > note: Key-level vs project-level IAM > > Grant these roles at the **key level** to limit Scalekit's access to only this specific key. 4. **Register the key in Scalekit** - In the Scalekit dashboard, go to **Settings** and open the **Encryption keys** tab. - Click **Create New Key** and select **BYOK - GCP Cloud KMS**. - Copy the Scalekit service account email shown in the modal. You need it for step 3 (grant Scalekit access to the key) if you have not granted IAM access yet. - Enter the fully-qualified GCP key resource name: ``` projects/YOUR-GCP-PROJECT/locations/global/keyRings/scalekit-kms-keyring/cryptoKeys/scalekit-kms-key ``` To retrieve the exact name from the CLI: ```bash gcloud kms keys describe "scalekit-kms-key" \ --keyring "scalekit-kms-keyring" \ --location "global" \ --project "YOUR-GCP-PROJECT" \ --format="value(name)" ``` - Click **Create**. The key is created in **Staged** state. **Key resource name format** The key reference must follow this format: ``` projects/{PROJECT_ID}/locations/{LOCATION}/keyRings/{KEYRING_NAME}/cryptoKeys/{KEY_NAME} ``` To retrieve the key ring path: ```bash gcloud kms keyrings describe "scalekit-kms-keyring" \ --location "global" \ --project "YOUR-GCP-PROJECT" \ --format="value(name)" ``` Append `/cryptoKeys/scalekit-kms-key` to get the full key reference. 5. **Activate the key** Click **Activate** on the staged key. The key becomes Primary and Scalekit uses it for all new encryption operations. > caution: Activation is permanent > > Once activated, you cannot deactivate the key without creating and activating a new key. Confirm your IAM grants are in place before activating. 6. **Re-encrypt existing data** Activation covers new writes automatically. Click **Re-encrypt Data** to migrate existing records. Scalekit decrypts each record with the previous key and re-encrypts it with the new key. ## Monitor with audit logs Cloud KMS logs every cryptographic operation to [Cloud Audit Logs](https://cloud.google.com/kms/docs/audit-logging). Use this filter in **Cloud Logging** to see all encrypt, decrypt, and key events for your key: ``` resource.type="cloudkms_cryptokey" ``` ## Fix common errors **Permission denied when activating or using the key** Check that both IAM bindings are applied to the correct key: ```bash gcloud kms keys get-iam-policy "scalekit-kms-key" \ --keyring "scalekit-kms-keyring" \ --location "global" \ --project "YOUR-GCP-PROJECT" ``` The output should list the Scalekit service account with both `roles/cloudkms.cryptoKeyEncrypterDecrypter` and `roles/cloudkms.viewer`. If the bindings are missing, repeat step 3. **Re-encryption reports unrecoverable rows** Unrecoverable rows are records that could not be re-encrypted, typically because the previous key version was disabled or destroyed before re-encryption completed. Contact [support@scalekit.com](mailto:support@scalekit.com) if you need help recovering affected records. **Key resource name is not accepted** Verify the format is exactly: ``` projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING_NAME/cryptoKeys/KEY_NAME ``` Run `gcloud kms keys describe` with `--format="value(name)"` to retrieve the exact string. Do not construct it manually. ## Next - [Security and compliance](https://docs.scalekit.com/agentkit/security/): What Scalekit protects, and what your app is responsible for. - [Troubleshoot connection and OAuth errors](https://docs.scalekit.com/agentkit/troubleshooting/): Find the error your user saw and how to fix it. --- Source: https://docs.scalekit.com/agentkit/advanced/launch-checklist.md # AgentKit launch checklist Check your AgentKit integration before real users connect: Production credentials and connections, your own OAuth apps, user verification, events and security. Work through this list in your Production environment before real users connect their accounts. Each item says how to confirm it's done and links to the steps. Development and Production share nothing, so most of the work is setting up again in Production what you built in Development. ## Set up Production - [ ] **Create a Production environment.** The environment switcher at the top left lists it. See [Create a Production environment](https://docs.scalekit.com/agentkit/environments/#create-a-production-environment). - [ ] **Deploy Production credentials.** Your production servers use the Production `SCALEKIT_ENVIRONMENT_URL`, `SCALEKIT_CLIENT_ID` and `SCALEKIT_CLIENT_SECRET`, and no Development values remain. The secret's **Last used** time updates when your server calls Scalekit. See [API credentials](https://docs.scalekit.com/agentkit/api-credentials/). - [ ] **Keep the client secret on your server.** It isn't in browser or mobile code, logs or your repository. ## Recreate your connections - [ ] **Create every connection your agent uses in Production.** Each one appears in **AgentKit** > **Connections** in Production. See [Set up a connection](https://docs.scalekit.com/agentkit/connections/). - [ ] **Match connection names in your code.** The `connection_name` values your code sends match the Production connection names exactly. - [ ] **Create your custom connectors and Virtual MCP servers again, if you use them.** They belong to one environment, so create them with your Production credentials. See [Create a connector](https://docs.scalekit.com/agentkit/bring-your-own-connector/create-connector/) and [Create a Virtual MCP server](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/). ## Publish your own OAuth apps - [ ] **Use your own OAuth app for each connection.** Connections can go live on Scalekit's credentials, but switch to your own OAuth apps before real users connect. Your users then see your app's name on the consent screen instead of Scalekit's, rate limits and quotas belong to your app, and a provider restricting the shared app doesn't affect you. See [Use your own OAuth app](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/). The rest of this section applies to connections that use your own OAuth app. A provider creates a new OAuth app in a development or testing mode that only authorizes the account that created it. The connection works while you test with your own account, then fails for your customers. For example, Airtable returns "This OAuth application cannot be used outside of development". - [ ] **Add the Production redirect URI to each app.** Copy it from the connection in Production. It differs from Development's, and it must match exactly. See [Use your own OAuth app](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/). - [ ] **Publish each app** out of development or testing mode, and complete the profile the provider requires first, such as a logo, terms of service and privacy policy. - [ ] **Recheck the credentials after publishing.** Publishing can change the client ID. The client ID, client secret and scopes in Scalekit match the provider's. - [ ] **Authorize with an account outside your own organization.** It connects without an "unverified app" or "needs admin approval" screen. See [Troubleshoot connection and OAuth errors](https://docs.scalekit.com/agentkit/troubleshooting/). ## Connect users safely - [ ] **Use a stable, internal ID as each user's `identifier`.** It comes from your authenticated session, never from the request body, and isn't an email address or shared across users. See [Choose an identifier](https://docs.scalekit.com/agentkit/concepts/#choose-an-identifier). - [ ] **Turn on user verification.** In Production, **AgentKit** > **Settings** > **User Verification** is set to **Custom user verifier**, your authorization links pass `user_verify_url`, and your verify route confirms the user. See [Verify users](https://docs.scalekit.com/agentkit/user-verification/). - [ ] **Send authorization links from your server.** A test user can open one, authorize, and reach `ACTIVE`. See [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/). ## Handle accounts that stop working - [ ] **Check status before tool calls, and send a new link when it isn't `ACTIVE`.** Revoke a test user's access at the provider, and your app asks them to reconnect. See [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/). - [ ] **Listen for account events.** A webhook endpoint in Production receives `connected_account.status_updated`, and your handler verifies the signature. See [Listen for account events](https://docs.scalekit.com/agentkit/account-events/). - [ ] **Delete connected accounts when your users delete their accounts.** Your account-deletion flow deletes each of the user's connected accounts, so Scalekit no longer holds their tokens. See [Delete a connected account](https://docs.scalekit.com/agentkit/connected-accounts/#delete-a-connected-account). ## Run agents safely - [ ] **If you use Virtual MCP, mint a session token for each run, on your server.** Tokens aren't stored between runs or sent to a browser. See [Mint session tokens](https://docs.scalekit.com/agentkit/mcp/session-tokens/). - [ ] **Check `mcp_server_url` before you hand a Virtual MCP server to a client.** The value you store for each server isn't empty. See [Check it worked](https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/#check-it-worked). - [ ] **Handle tool call errors and rate limits.** Your agent reads the error a failed call returns, and retries with backoff when it gets a `429`. See [Errors and rate limits](https://docs.scalekit.com/agentkit/reference/errors/). - [ ] **Monitor tool calls in AgentKit > Logs.** Someone on your team checks **AgentKit** > **Logs** for failed calls after launch. See [What Scalekit stores](https://docs.scalekit.com/agentkit/security/#what-scalekit-stores). - [ ] **Leave Store connector error details off** in **AgentKit** > **Logs**, unless you're debugging a connector. With it on, logs keep error responses that can include data from the app. See [What Scalekit stores](https://docs.scalekit.com/agentkit/security/#what-scalekit-stores). - [ ] **Review what your app is responsible for.** See [Security and compliance](https://docs.scalekit.com/agentkit/security/). ## Prepare your workspace - [ ] **Give each teammate the least access they need.** Only people who manage members and billing are **Admin**. See [Team members and roles](https://docs.scalekit.com/agentkit/team-members/). - [ ] **Choose a plan for Production.** **Developers** > **Billing** shows its plan and your tool call usage. See [Billing](https://docs.scalekit.com/agentkit/billing/). ## Optional - [ ] **Brand the Hosted Connect Widget** in Production with your name, logo and colors. See [Brand the Hosted Connect Widget](https://docs.scalekit.com/agentkit/advanced/branding/). - [ ] **Serve Production from your own domain,** such as `auth.yourapp.com`, so users see it on authorization pages. See [Set up a custom domain](https://docs.scalekit.com/agentkit/advanced/custom-domain/). - [ ] **Bring your own encryption key** if your security review requires it. See [Manage encryption keys](https://docs.scalekit.com/agentkit/encryption-keys/). ## Next - [Security and compliance](https://docs.scalekit.com/agentkit/security/): What Scalekit protects, and what your app is responsible for. - [Troubleshoot connection and OAuth errors](https://docs.scalekit.com/agentkit/troubleshooting/): Find the error your user saw and how to fix it. --- Source: https://docs.scalekit.com/agentkit/security.md # Security and compliance How AgentKit protects user credentials: per-environment encryption keys in Google Cloud KMS, what Scalekit stores and for how long, regions and certifications. AgentKit holds your users' OAuth tokens and API keys, and makes API calls with them on your agent's behalf. This page explains how Scalekit protects those credentials and the data that passes through a tool call, and what your app is responsible for. Use it for a security review, or as a checklist before you onboard real users. ## Certifications - **SOC 2 Type II** and **ISO 27001** certified. - **GDPR** and **CCPA** compliant. The [Data Processing Agreement](https://www.scalekit.com/legal/data-processing-agreement) includes Standard Contractual Clauses and lists sub-processors. - **[HIPAA](https://www.scalekit.com/legal/hipaa) eligible**, with a Business Associate Agreement on Enterprise plans. Audit reports and penetration test summaries are available under NDA from the [Trust Center](https://www.scalekit.com/trust-center). ## How credentials are stored Every credential a user connects is encrypted before it's written to the database, whether it's an OAuth token or a value entered in the [Hosted Connect Widget](https://docs.scalekit.com/agentkit/hosted-connect-widget/), such as an API key or a username and password: - **A key per environment.** Scalekit encrypts credentials with AES-256-GCM under a data encryption key that belongs to one environment. No two environments share a key. - **Keys wrapped by Google Cloud KMS.** Each environment's key is itself encrypted by a master key in Google Cloud KMS, which is held apart from the database and the application. A copy of the database alone yields nothing usable. - **Scoped by environment.** Every read and write of stored credentials is filtered by environment, so one environment's context can't reach another's credentials. - **Rotation without downtime.** When a key rotates, new values are encrypted with the new key and Scalekit re-encrypts existing values in the background, so existing credentials keep working. To re-encrypt existing data right away, use **Re-encrypt Data** on [Encryption keys](https://docs.scalekit.com/agentkit/encryption-keys/). - **Your own key, optionally.** Hold the master key in your own Google Cloud KMS. Revoking Scalekit's access to it makes the stored credentials unusable to Scalekit. See [Encryption keys](https://docs.scalekit.com/agentkit/encryption-keys/). By default, Scalekit doesn't return a user's raw tokens in API responses. Your agent calls tools with `execute_tool` or the [API proxy](https://docs.scalekit.com/agentkit/tools/custom-tools/), and Scalekit adds the token to the outgoing request. If your app needs to call an app with the tokens itself, Scalekit support can turn on credential access for an environment. See [Get a connected account's credentials](https://docs.scalekit.com/agentkit/reference/connected-accounts/get-connected-account-credentials/). ## What Scalekit stores - **Users' OAuth tokens and API keys.** Stored: Yes, encrypted as above. Kept for: Until the connected account is deleted. - **Connection settings, including your OAuth app's client secret.** Stored: Yes. Kept for: Until the connection is deleted. - **Tool call responses.** Stored: No. Returned to your agent and not kept. Kept for: Not stored. - **Tool call inputs.** Stored: No. Sent to the app and not kept. Kept for: Not stored. - **Tool call logs.** Stored: Tool name, connection, connected account, identifier, status, error code, duration and time. Also the app's error response, when **Store connector error details** is on. Kept for: 90 days. - **Your API client secrets.** Stored: A one-way hash. The secret is shown once, when you create it. Kept for: Until you delete it. Tool call logs show in **AgentKit** > **Logs**. They never contain a tool's successful response. When a call fails, they keep Scalekit's own error message, and keep the error the app returned only when **Store connector error details** is on in **AgentKit** > **Logs**. That error can include data from the app, so leave the setting off unless you're debugging a connector. Tool call logs also contain the user's identifier, so use an internal user ID rather than an email address. See [Choose an identifier](https://docs.scalekit.com/agentkit/concepts/#choose-an-identifier). ## Where data is hosted Scalekit runs two independent regions on Google Cloud, with no shared application state: | Region | Location | | --- | --- | | US | `us-west2` (Los Angeles) | | EU | `europe-west3` (Frankfurt) | You choose the region when you sign up, and your workspace's data stays in it. See [Environments and regions](https://docs.scalekit.com/agentkit/environments/). To keep credentials, tool calls and logs inside your own infrastructure, [self-host AgentKit](https://docs.scalekit.com/agentkit/self-hosted/) with an enterprise license. ## In transit All Scalekit endpoints require HTTPS with TLS 1.3. If an app only accepts traffic from known addresses, allow Scalekit's [outbound IP addresses](https://docs.scalekit.com/reference/outbound-ip-addresses/) for your region. ## What your app is responsible for Scalekit protects credentials once a user connects. Your app decides who connects and who can act as whom: - **Verify users in production.** Set **AgentKit** > **Settings** > **User Verification** to **Custom user verifier**, so the person who approves access is the user your app meant. With **None**, anyone who opens an authorization link activates the account. See [Verify users](https://docs.scalekit.com/agentkit/user-verification/). - **Use a stable, internal identifier.** Pass your app's user ID, never an email address or anything a user can choose. Take it from your server-side session, not from the request body, so one user can't act as another. - **Keep one user's credentials with that user.** When you hand a credential to an agent runtime, such as a [Virtual MCP session token](https://docs.scalekit.com/agentkit/mcp/session-tokens/), store it per user and never share it across users. - **Keep API credentials on the server.** Load your client ID and secret from environment variables or a secret manager, never from browser or mobile code. Rotate a secret by creating a new one before you delete the old one. See [API credentials](https://docs.scalekit.com/agentkit/api-credentials/). - **Request only the scopes you need.** A narrower scope limits what a leaked token can do. See [Configure scopes](https://docs.scalekit.com/agentkit/connections/#configure-scopes). - **Give agents only the tools they need.** Start with read-only tools, and ask the user before a destructive call. Tool definitions carry `read_only_hint` and `destructive_hint` for this. See [Use built-in tools](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/). - **Verify webhook signatures.** Scalekit signs every webhook. Check the signature before you trust a payload. See [Verify each request](https://docs.scalekit.com/agentkit/account-events/#verify-each-request-and-handle-the-event). - **Limit who can use the dashboard.** Give teammates the least access their work needs. See [Team members and roles](https://docs.scalekit.com/agentkit/team-members/). ## Next - [Launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to check before real users connect. - [Verify users](https://docs.scalekit.com/agentkit/user-verification/): Confirm the person who connects is the user your app intended. --- Source: https://docs.scalekit.com/agentkit/advanced/branding.md # Brand the Hosted Connect Widget Make the Hosted Connect Widget your users see when they connect an account look like your product: name, logo, colors, font, text, domain and consent screen. When a user opens an [authorization link](https://docs.scalekit.com/agentkit/tools/authorize/), they land on the [Hosted Connect Widget](https://docs.scalekit.com/agentkit/hosted-connect-widget/), then go to the app's consent screen to grant access. This page covers everything you can change so that flow looks like your product, and where to change it. For what users see and do in the widget, see [Hosted Connect Widget](https://docs.scalekit.com/agentkit/hosted-connect-widget/). ## What your users see The widget shows: - Your logo next to the app's icon, and a headline that names your application: "Acme wants to connect to your Gmail". - A short overview and a description of the app the user is connecting. - A **Connect** button, and links to your terms of service and privacy policy. - A "Powered by Scalekit" badge, which some plans let you remove. After the user clicks **Connect**, the app's own consent screen shows the name of the OAuth app that asks for access. The page's address is your environment's domain, or your custom domain if you set one up. | What you change | Where | Applies to | |---|---|---| | Name, logo, favicon, font, colors, corners, background and legal links | **Customize** > **Branding** | Every connection in the environment | | App name, overview and description in the widget | **Connections** > your connection > **Widgets** | One connection | | "Powered by Scalekit" badge | **Customize** > **Branding** | Every connection in the environment | | The domain in authorization links | **Customize** > **Custom Domain** | The environment | | The app name on the consent screen | Your own OAuth app, set on the connection | One connection | ## Brand the widget In the AgentKit dashboard, go to **Customize** > **Branding**. The preview on the right updates as you change a setting. Save to apply your changes to the widget. | Setting | What it changes | Accepts | |---|---|---| | **Application Name** | Your name in the headline | Text. Without a name, the headline reads "Connect to your Gmail" | | **Logo** | The logo next to the app's icon | Upload a PNG or JPG, or enter a URL for any format, including SVG. At least 36 px high | | **Favicon** | The browser tab icon | Upload a PNG or JPG, or enter a URL for any format. Square images work best | | **Font** | The font on the page | Upload a WOFF2, WOFF or TTF file of up to 2 MB, or enter an `https://` URL that points directly at one. If the font can't load, the default font is used | | **Primary Button Background** and **Primary Button Text Color** | The **Connect** button | Colors | | **Border radius** | How rounded buttons and boxes are | **None**, **Small**, **Medium** or **Large** | | **Border color** | The border of the connect card | A color | | **Background Color** | The page background | A color | | **Background Style** | The page background, with an image or gradient | A CSS `background` shorthand value | | **Terms of Service URL** and **Privacy Policy URL** | The legal links under the button | URLs. The page shows only the links you set | Branding belongs to the environment, so set it in Development and in Production. If you also use SaaSKit in the same environment, its **Branding** page edits the same settings: the name, logo, colors and font you set here also apply to your login pages. ## Describe each connection The widget describes the app with the connector's default name and description. To write your own, for example to say what your agent does with the user's account: 1. Go to **AgentKit** > **Connections** and open the connection. 2. Open the **Widgets** tab. 3. Set the **App name** (shown in the headline in place of the connector's name), a **Short Overview** and a **Description**, then save. ## Remove "Powered by Scalekit" Whether you can remove the badge depends on your plan; see [pricing](https://www.scalekit.com/pricing). If your plan includes it, turn on **Remove "Powered by Scalekit"** in **Customize** > **Branding**. You can remove it in Production environments only. ## Use your own domain Authorization links use your environment's domain, such as `yourapp.scalekit.com`. To use your own, such as `connect.yourapp.com`, [set up a custom domain](https://docs.scalekit.com/agentkit/advanced/custom-domain/). Custom domains work in Production environments, and availability depends on your plan. Authorization links you create after the domain is verified use it. ## Show your app on the consent screen By default, OAuth connectors that offer Scalekit's credentials ask for consent in Scalekit's name. To show your app's name, logo and terms on the consent screen instead, [use your own OAuth app](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/) for the connection. > note: Provider consent screen > > The consent screen belongs to the app the user connects, such as Google or Slack. You change what it shows in that app's developer console, not in Scalekit. ## Check it worked Create an authorization link for a test user and open it in a private browser window. The widget shows your logo, name and colors, and the address bar shows your custom domain if you set one up. ## Next - [Set up a custom domain](https://docs.scalekit.com/agentkit/advanced/custom-domain/): Serve authorization links from your own domain. - [Use your own OAuth app](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/): Show your app's name on the provider's consent screen. --- Source: https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth.md # Use your own OAuth app Use your own OAuth app for an AgentKit connection, so users see your app's name on the consent screen and your app gets its own quota at the provider. Many OAuth connectors come with Scalekit's own OAuth app credentials, so you can create a connection and test it without registering an app with the provider. The connection form offers **Use Scalekit credentials** when a connector supports it. Connectors without Scalekit credentials need your own OAuth app from the start, as described in [Configure connections](https://docs.scalekit.com/agentkit/connections/#set-up-an-oauth-connection). You can go live on Scalekit's credentials, but switch to your own OAuth app before real users connect. On Scalekit's credentials, your users see Scalekit's name and branding on the consent screen, your requests share Scalekit's rate limits and quotas, and a provider restricting the shared app affects your connection too. **Bring your own credentials** lets you replace Scalekit's shared OAuth credentials with your own. Once configured, users see your app name, logo, and terms on every OAuth consent screen. ## What changes when you use your own credentials - **Consent screens** display your application's name and branding - **Rate limits and quotas** are tied to your OAuth app, not Scalekit's shared pool - **Provider relationship** is direct, and your OAuth app appears in provider dashboards and audit logs - **Compliance**: useful if your organization requires a direct relationship with each OAuth provider Nothing changes in your code or the Scalekit SDK. The switch is purely a dashboard configuration on the connection. ## Configure your credentials 1. ### Copy the redirect URI from Scalekit Go to **AgentKit** > **Connections** and click **Edit** on the connection you want to update. Select **Use your own credentials**. The form expands and displays a **Redirect URI**. Copy it. 2. ### Register your OAuth app with the provider In the provider's developer console, create a new OAuth app (or use an existing one). Add the Redirect URI you copied in the previous step to the list of authorized redirect URIs. > caution: Redirect URI must match exactly > > The URI must match character-for-character. A mismatch will cause OAuth flows to fail with a redirect_uri_mismatch error. The provider gives you a **Client ID** and **Client Secret** after registration. Many provider consoles create the app in an internal, development or testing mode by default. In that mode only some accounts can authorize it, so it is not enough for customer consent. For example, a Google app in the **Testing** publishing status works only for the accounts listed as test users on its OAuth consent screen. Add each account you test with, and allow a few minutes for a new test user to take effect. 3. ### Enter your credentials and save Back in Scalekit Dashboard, enter the **Client ID** and **Client Secret** from your OAuth app and click **Save**. All new OAuth flows for this connection will now use your credentials. Saving credentials only wires your app into Scalekit. Before customers can connect, promote the app to allow external accounts and validate it end-to-end — see the [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/). ## Existing connected accounts > caution: Existing connected accounts are not affected immediately > > Switching credentials does not re-authorize users who are already active. They continue using the previous credentials until they re-authorize. If you need all users to see your branding immediately, generate new authorization links and prompt them to re-authorize. ## Next - [Set up a custom domain](https://docs.scalekit.com/agentkit/advanced/custom-domain/): Serve authorization pages from your own domain. - [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to finish before real users connect. --- Source: https://docs.scalekit.com/agentkit/advanced/custom-domain.md # Set up a custom domain Serve your AgentKit Production environment from your own domain, such as auth.yourapp.com: add a CNAME record, verify it, and Scalekit provisions SSL. Custom domains enable you to offer a fully branded experience. By default, Scalekit assigns a unique endpoint URL, but you can replace it via CNAME configuration. The custom domain also applies to the authorization server URL shown on the OAuth consent screen during MCP authentication; users will see your branded domain instead of the auto-generated `yourapp.scalekit.com`. | Before | After | |--------|-------| | `https://yourapp.scalekit.com` | `https://auth.yourapp.com` | - **Environment:** CNAME configuration is available only for production environments. Availability also depends on your plan; see [pricing](https://www.scalekit.com/pricing). - **SSL:** After successful CNAME configuration, an SSL certificate for your custom domain is automatically provisioned ## Set up your custom domain > Image: Scalekit Settings Custom Domain tab showing the subdomain URL field and a CNAME record in the DNS configuration table To set up your custom domain: 1. Go to your domain's DNS registrar 2. Add a new record to your DNS settings and select **CNAME** as the record type 3. Switch to production environment in the Scalekit dashboard 4. Copy the **Name** (your desired subdomain) from **Customize** > **Custom Domain** in the Scalekit dashboard and paste it into the **Name/Label/Host** field in your DNS registrar 5. Copy the **Value** from **Customize** > **Custom Domain** and paste it into the **Destination/Target/Value** field in your DNS registrar 6. Save the record in your DNS registrar 7. In the Scalekit dashboard, click **Verify** CNAME record changes can take up to 72 hours to propagate, although they typically happen much sooner. ## Troubleshoot CNAME verification If there are any issues during the CNAME verification step: - Double-check your DNS configuration to ensure all values are correctly entered - Once the CNAME changes take effect, Scalekit will automatically provision an SSL certificate for your custom domain. This process can take up to 24 hours You can click on the **Check** button in the Scalekit dashboard to verify SSL certification status. If SSL provisioning takes longer than 24 hours, please contact us at [support@scalekit.com](mailto:support@scalekit.com) ## DNS registrar guides For detailed instructions on adding a CNAME record in specific registrars: - [GoDaddy: Add a CNAME record](https://www.godaddy.com/help/add-a-cname-record-19236) - [Namecheap: How to create a CNAME record](https://www.namecheap.com/support/knowledgebase/article.aspx/9646/2237/how-to-create-a-cname-record-for-your-domain) ## Next - [Brand the connect experience](https://docs.scalekit.com/agentkit/advanced/branding/): Add your name, logo and colors to the page users connect from. - [AgentKit launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): Everything to finish before real users connect. --- Source: https://docs.scalekit.com/agentkit/self-hosted.md # Self-host AgentKit Run AgentKit on Kubernetes or Docker in your own infrastructure, with no connection to Scalekit. What changes for connectors, OAuth apps, network and SDKs. AgentKit runs in your own infrastructure with an enterprise license, on Kubernetes or Docker. Connections, connected accounts, the token vault, tool execution and Virtual MCP servers all run there. Once it's installed, your instance has no connection to Scalekit's infrastructure: credentials, tool calls and logs stay in your network. Use it when credentials and tool calls must stay in your network for data residency, compliance or network isolation. To get access, [talk to an engineer](https://scalekit.com/demo). Installation, configuration and upgrade guides come with your license. ## What you provide | Dependency | Requirement | | --- | --- | | Kubernetes (for a Kubernetes deployment) | 1.27 or later, managed or self-managed, with Helm 3.12 or later | | Ingress (for a Kubernetes deployment) | Kubernetes Gateway API or the nginx ingress controller | | PostgreSQL | 15 or later (CockroachDB is also supported) | | Redis | 6.2 or later | | SMTP | Any provider, for team invitations and sign-in email | | Domain | A domain and TLS certificate for your instance | ## Network access No traffic goes to Scalekit, but tools still call the apps they connect to. Your instance calls each connector's API and OAuth token endpoint directly, so allow outbound HTTPS from your instance to the providers you use, such as Google, Slack or Salesforce. Your users' browsers must reach the provider's consent screen when they authorize a connected account. A connector works only if your instance can reach its provider. Connectors for apps that run inside your network, including [your own connectors](https://docs.scalekit.com/agentkit/bring-your-own-connector/overview/), need no internet access. ## OAuth apps Scalekit's own OAuth credentials, offered as **Use Scalekit credentials** when you create a connection, belong to Scalekit's cloud and aren't available on a self-hosted instance. Create an OAuth app with each provider and use it for the connection, as in [Use your own OAuth app](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/). Register the redirect URI your instance shows in the connection form, which is on your own domain. ## Point your app at your instance The SDKs and the REST API work the same way. Set `SCALEKIT_ENVIRONMENT_URL` to your instance's environment URL, and use a client ID and secret from **API credentials** in your instance's dashboard at `https://app.`. See [API credentials](https://docs.scalekit.com/agentkit/api-credentials/). ## Next - [Security and compliance](https://docs.scalekit.com/agentkit/security/): How credentials are stored, and what your app is responsible for. - [Launch checklist](https://docs.scalekit.com/agentkit/advanced/launch-checklist/): What to check before real users connect accounts. --- Source: https://docs.scalekit.com/agentkit/troubleshooting.md # Troubleshoot connection and OAuth errors Fix AgentKit connection errors: account statuses, failed_to_exchange_token, session_not_found, redirect_uri_mismatch, and Google, Microsoft or Slack consent. When a user can't connect an app, or a connected account stops working, the cause is almost always one of three things: the account's status, an error from Scalekit's connection error page, or a rule the provider enforces on your OAuth app. Check the status first, then find the error text below. Each error is its own heading, so you can link straight to it. For errors from tool calls, the API proxy or a Virtual MCP server, see the **Common problems** on [Use built-in tools](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/#common-problems), [Call any API](https://docs.scalekit.com/agentkit/tools/custom-tools/#common-problems) and [Virtual MCP servers](https://docs.scalekit.com/agentkit/mcp/overview/). ## Start with diagnostics The connected account's status tells you whether the user never finished authorizing, still needs identity verification, has an expired token, or disconnected. **Python** ```python import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions account = actions.get_connected_account_details( identifier="user_123", connection_name="github-connect", ).connected_account print(account.status) # ACTIVE, EXPIRED, PENDING_AUTH, PENDING_VERIFICATION or DISCONNECTED ``` **Node.js** ```typescript import 'dotenv/config'; import { ConnectorStatus, ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const { connectedAccount } = await scalekit.actions.getConnectedAccount({ identifier: 'user_123', connectionName: 'github-connect', }); // The status name, such as ACTIVE or PENDING_AUTH console.log(connectedAccount && ConnectorStatus[connectedAccount.status]); ``` **cURL** ```bash curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/api/v1/connected_accounts/details" \ -H "Authorization: Bearer $TOKEN" \ --data-urlencode "connector=github-connect" \ --data-urlencode "identifier=user_123" | jq '.connected_account.status' ``` If the status is `ACTIVE` but a tool call fails, run a read-only tool such as `github_user_get_authenticated`. If that works, the connection is fine and the problem is in the call: see [Use built-in tools](https://docs.scalekit.com/agentkit/tools/scalekit-optimized-tools/#common-problems). To learn about status changes without polling, subscribe to the `connected_account.status_updated` and `connected_account.token_refresh_failed` webhooks. See [Listen for account events](https://docs.scalekit.com/agentkit/account-events/). ## Connected account status | Status | Meaning | Fix | |--------|---------|-----| | `ACTIVE` | Credentials are valid | Nothing to fix. Tool calls work. | | `PENDING_AUTH` | The user hasn't finished authorizing | [Send an authorization link](#pending_auth) | | `PENDING_VERIFICATION` | Authorization succeeded, identity verification hasn't | [Finish user verification](#pending_verification) | | `EXPIRED` | The token expired or was revoked and Scalekit couldn't refresh it, or the app rejected a stored API key or other credential | [Send a new authorization link](#expired) | | `DISCONNECTED` | The account was disconnected in Scalekit | [Send a new authorization link](#disconnected) | ### `PENDING_AUTH` The user hasn't finished authorizing, or you started re-authorization and they haven't completed it. Create an authorization link and send it through your app, for example as an in-app prompt or on a settings page. **Python** ```python if account.status == "PENDING_AUTH": link = actions.get_authorization_link( connection_name="github-connect", identifier="user_123", ) print(link.link) ``` **Node.js** ```typescript import { ConnectorStatus } from '@scalekit-sdk/node'; if (connectedAccount?.status === ConnectorStatus.PENDING_AUTH) { const { link } = await scalekit.actions.getAuthorizationLink({ connectionName: 'github-connect', identifier: 'user_123', }); console.log(link); } ``` The status changes to `ACTIVE` when the user finishes, or to `PENDING_VERIFICATION` if your environment verifies users. ### `PENDING_VERIFICATION` The user authorized the app, but Scalekit is waiting for your app to confirm the user's identity before it activates the account. Complete the verification step described in [Verify user identity](https://docs.scalekit.com/agentkit/user-verification/). After verification succeeds, the status is `ACTIVE`. ### `EXPIRED` The access token expired and Scalekit couldn't refresh it, for example because the user removed your app's access at the provider, such as in their Google Account's third-party connections. Scalekit refreshes tokens on its own, so there's no refresh call to make: send the user a new authorization link, as for [`PENDING_AUTH`](#pending_auth). If the account expires again soon after every re-authorization, the connection never received a refresh token. Providers issue one only when the connection asks for offline access. Add the provider's offline-access scope, such as `offline_access`, in [Configure scopes](https://docs.scalekit.com/agentkit/connections/#configure-scopes), then have the user connect again. [Common causes](https://docs.scalekit.com/agentkit/connected-accounts/#common-causes) lists the other reasons accounts expire. ### `DISCONNECTED` The account was disconnected in Scalekit, from the dashboard or by a disconnect call from your app. Its credentials are cleared, so only re-authorization fixes it. Send a new authorization link, and until the user reconnects, show the state in your UI and stop scheduling tool calls for that account. ### 401 or `REAUTHENTICATION_NEEDED`: the user must authorize again A tool call returns HTTP 401 with `error_code` `TOOL_ERROR` and `tool_error_code` `REAUTHENTICATION_NEEDED`, or `UNAUTHENTICATED` while the connected account is `EXPIRED`. Retrying with a new Scalekit access token doesn't help. What it means depends on how the connection authenticates: - **OAuth.** When the app rejects the user's access token, Scalekit refreshes the token and retries the call once. You see this error when the refresh failed, usually because the user revoked your app's access or the refresh token expired. The account is now `EXPIRED`. - **API key, bearer token or basic auth.** There's no token to refresh, so a single 401 from the app marks the account `EXPIRED`. The app rejected the stored credential, for example because it was deleted, rotated or lacks a permission the tool needs: some apps answer a missing permission with 401 rather than 403. Check the credential at the provider before the user enters it again. In both cases, send the user a new authorization link, as for [`PENDING_AUTH`](#pending_auth): for API key and basic auth connections, the link collects the new credential. Pause tool calls for that account until its status is `ACTIVE` again. [Authorize a user](https://docs.scalekit.com/agentkit/tools/authorize/) shows the full flow, and [Errors and rate limits](https://docs.scalekit.com/agentkit/reference/errors/) lists every error code. If the refresh succeeds but the app still rejects the call, the token is valid and lacks a permission: see [`PERMISSION_DENIED`](#authorization-succeeds-but-tools-fail-on-a-missing-scope). ## Errors on Scalekit's connection page When authorization fails, Scalekit sends the user to its connection error page. The page shows a title, and its **Debug info** panel shows the `error` code, the `error_description` and an **Auth Request ID**. Find the title or the code below. > tip: Keep the Auth Request ID > > The Auth Request ID lets Scalekit support find the failed attempt in the logs. **Copy error details** on the page copies it with the error and description. ### Authorization failed (`failed_to_exchange_token`) The user approved access, but Scalekit couldn't finish the connection. Read the `error_description` to find which step failed: - **The provider rejected the token exchange.** The description holds the provider's error, such as `invalid_client` or `invalid_grant`. Check the client ID and secret on the connection: see [Invalid client or client authentication failed](#invalid-client-or-client-authentication-failed). An authorization code is valid once and briefly, so a user who waited a long time on the consent screen should start again. The page also suggests organization-level causes. The user's admin may have blocked your app with an access policy, the app may lack permission in the organization's settings, or the user's account may not have the rights to authorize it. The [provider errors](#provider-errors) below cover how Google, Microsoft and Slack report these. ### Session expired or not available (`session_not_found`) Your environment's user verification mode is **Scalekit users only**, and the person authorizing isn't signed in to the Scalekit dashboard. Scalekit uses the dashboard session to confirm who the user is, so without one it can't activate the account. - If the user is on your team, have them sign in to the Scalekit dashboard in the same browser and start the flow again. - If the user is a customer, they don't have a Scalekit login. Switch to **Custom user verifier** in **AgentKit** > **Settings** > **User Verification**, and verify users in your own app. See [Verify user identity](https://docs.scalekit.com/agentkit/user-verification/). This isn't a timeout: retrying without signing in fails the same way. ### Account mismatch (`access_denied`) The flow was refused. Two things cause it: - **The user declined consent** at the provider, or the provider denied the request. The provider sends `access_denied` back and Scalekit shows it with this title. Offer the user a way to start again. - **Scalekit's identity check failed** in **Scalekit users only** mode. The user is signed in to a different Scalekit workspace, or, for a flow started from the dashboard's playground, as a different user from the one who started it. Sign in with the expected account and start again. ### Invalid request (`invalid_request`) The request reached Scalekit without a required parameter, or with one it couldn't read, such as a missing `auth_request_id` or `state`. This happens when a link is truncated, edited or reused. Create a new authorization link and use it unchanged. If the provider returned `invalid_request`, the authorization request didn't meet its rules: check the connection's scopes and settings. ### Something went wrong (`server_error`) An unexpected error, in Scalekit or at the provider. Start the flow again after a few minutes, and check the [Scalekit status page](https://status.scalekit.com) and the provider's status page. If it persists, contact support with the Auth Request ID. Two `error_description` values point to a specific step: - **`user_verify_url not configured for verification redirect`.** Your environment uses **Custom user verifier**, but the authorization link was created without a `user_verify_url`. Pass one when you create the link, or switch modes. See [Verify user identity](https://docs.scalekit.com/agentkit/user-verification/#common-scenarios). - **`Error executing post auth hooks`.** Scalekit stored the tokens but a step after authorization failed. Start the flow again. If it fails again, contact support with the Auth Request ID. ### Other codes from the provider Scalekit passes the provider's own OAuth error through to the page, where it shows as **Unable to complete connection**. The codes are defined in [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1): | Code | Meaning | What to do | |------|---------|------------| | `unauthorized_client` | The provider doesn't allow this OAuth app to use this flow | Check the app's settings in the provider's console | | `invalid_scope` | A requested scope is unknown or not allowed for this app | Fix the scopes on the connection, then retry | | `unsupported_response_type` | The provider doesn't support the authorization code flow for this app | Check the app type in the provider's console | | `temporarily_unavailable` | The provider is overloaded or down for maintenance | Retry after a few minutes | ## OAuth app configuration errors These come from the OAuth app a connection uses, whether it's Scalekit's or [one you registered](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/). ### `redirect_uri_mismatch` The redirect URI registered in the provider's OAuth app doesn't match the one Scalekit sends. Providers require an exact match. 1. In the Scalekit dashboard, open **AgentKit** > **Connections** and select the connection. 2. Copy the **Redirect URI**. 3. In the provider's developer console, paste it into the OAuth app's allowed redirect URIs. 4. Save, and start the connection flow again. Most mismatches are a trailing slash (`/callback/` and `/callback`), `http` instead of `https`, or a missing port number in local development. ### Invalid client or client authentication failed The client ID or secret on the connection doesn't match the provider's OAuth app. Providers report it as `invalid_client`, inside a `failed_to_exchange_token` error. 1. Open **AgentKit** > **Connections** and select the connection. 2. Compare the **Client ID** and **Client Secret** with the OAuth app in the provider's console. 3. If the secret was rotated or expired, create a new one in the provider's console and paste it into the connection. 4. Start the connection flow again. ### `invalid_state` Scalekit checks the OAuth `state` parameter to protect against cross-site request forgery, and fails the flow with `invalid_state` when it's missing or doesn't match. The connection error page shows it as **Unable to complete connection**. Finish the flow in the same browser it started in, allow cookies, and start again from a fresh authorization link rather than a bookmarked or back-button page. ### Authorization succeeds but tools fail on a missing scope The user's token doesn't include a scope a tool needs. The app returns an error such as Slack's `missing_scope`, or a `403`. Some apps answer a missing scope with a 401 instead. For an OAuth account, Scalekit refreshes the token and retries, and when the retry is rejected too, the call returns HTTP 403 with `tool_error_code` `PERMISSION_DENIED` and the message "The connected account does not have permission to perform this action." The account stays `ACTIVE`, because its token works. Add the scope in [Configure scopes](https://docs.scalekit.com/agentkit/connections/#configure-scopes), then have the user authorize again: existing tokens don't gain new scopes. ### The connection works but access tokens come back empty A connected account has an ID, such as `ca_...`, but its access and refresh token fields are empty. That's expected: API responses don't include provider tokens by default, which keeps them out of your app. Call tools with `execute_tool` or the [API proxy](https://docs.scalekit.com/agentkit/tools/custom-tools/), which add the user's token for you. To read the tokens themselves, contact [support](mailto:support@scalekit.com) to enable it. ## Provider errors Each provider sets its own rules for OAuth apps. These are the common ones, with links to the provider's documentation. ### It works for your account but fails for your customers A provider OAuth app often starts in a development or testing state that only its creator, or a short list of test users, can authorize. The connection works while you test it with your own account, and fails when a customer connects. Listing tools also works for you, because your own account already authorized the app. This applies to OAuth apps you registered and to connections that use Scalekit's shared credentials. To fix it, finish the provider's requirements for external use, publish the app, and connect again from an account outside your organization. [Publish your own OAuth apps](https://docs.scalekit.com/agentkit/advanced/launch-checklist/#publish-your-own-oauth-apps) in the launch checklist lists the steps. For example: - **Google** limits apps in the **Testing** publishing status to 100 test users that you list on the OAuth consent screen. Publish the app to production, and complete verification if it requests sensitive or restricted scopes. See [Google's publishing status guide](https://support.google.com/cloud/answer/15549945). - **Airtable** returns "This OAuth application cannot be used outside of development" until the integration has a privacy policy URL, a terms of service URL and a support email. See [Airtable's OAuth integration guide](https://airtable.com/developers/web/guides/oauth-integrations). - **ZoomInfo** offers partner apps for serving customers across organizations. Partner apps go through ZoomInfo's review before customers can install them. See [ZoomInfo partner apps](https://docs.gtm.ai/docs/partner-app). ### Google: "Access blocked" or "This app is blocked" Google stops the user on its sign-in screen before they can grant access: - **"Access blocked: … has not completed the Google verification process"**, with `Error 403: access_denied`. The OAuth app is in **Testing** status and the user isn't on its list of test users. Add them as a test user on the OAuth consent screen, or publish the app. - **"This app is blocked"**. Google blocked the app from requesting the sensitive scopes the connection asks for, usually because the app isn't verified for them. A shared OAuth app can be blocked too. Use [your own OAuth app](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/), and complete [Google's app verification](https://support.google.com/cloud/answer/7454865) for the scopes you request. ### Google: unverified app screen or `admin_policy_enforced` Google shows an unverified app screen when an app that hasn't completed verification requests sensitive or restricted scopes, and caps such an app at 100 new users. Complete [Google's app verification](https://support.google.com/cloud/answer/7454865), or request fewer sensitive scopes. `admin_policy_enforced` means the user's Google Workspace administrator doesn't allow the app to access one or more requested scopes. The administrator can [mark the app as trusted](https://knowledge.workspace.google.com/admin/apps/control-which-apps-access-google-workspace-data) by its OAuth client ID in the Admin console's API controls. See [Google's OAuth error reference](https://developers.google.com/identity/protocols/oauth2/web-server). > note: Testing apps get short-lived refresh tokens > > Google issues refresh tokens that expire in 7 days to apps with an external user type in **Testing** status, unless they request only name, email and profile scopes. Accounts on such an app expire weekly until you publish it. See [Google's OAuth 2.0 guide](https://developers.google.com/identity/protocols/oauth2). ### Microsoft: `AADSTS65001` or "Need admin approval" `AADSTS65001` means the user or an administrator hasn't consented to the permissions the app requests. When the tenant doesn't allow users to consent, the user sees **Need admin approval**, and the error can be `AADSTS90094` (administrator consent is required). 1. Open the app registration in the Microsoft Entra admin center. 2. Check that its API permissions match the connection's scopes. 3. Have a tenant administrator select **Grant admin consent**. 4. Start the connection flow again. See Microsoft's [consent troubleshooting guide](https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/app-integration/troubleshoot-consent-issues) and [error code reference](https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes). ### Microsoft: `AADSTS50020` The account the user signed in with doesn't exist in the tenant the app expects, for example a personal account or an account from another organization signing in to a single-tenant app. Have the user sign in with their work or school account in that tenant, have an administrator add them as an external user, or make the app registration multi-tenant. See Microsoft's [error code reference](https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes). ### Slack: the workspace requires app approval When a Slack workspace only allows pre-approved apps, a member who tries to install your app submits a request instead, and the connection can't finish until an administrator approves it. Ask a workspace administrator to approve the app, or test in a workspace where you manage app settings. See [Slack's app approval guide](https://slack.com/help/articles/222386767-Manage-app-approval-for-your-workspace). ### The provider rate limits your requests The provider rejected calls because of its rate limit or quota, usually with a `429`. Retry with exponential backoff, and cache read results where you can. With [your own OAuth credentials](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/), your app gets its own quota at providers that set quotas per OAuth app. ## Custom and MCP connector errors These come from [custom connectors](https://docs.scalekit.com/agentkit/bring-your-own-connector/create-connector/), including MCP servers you add yourself. ### MCP accounts need re-authorizing within hours Users connect without problems, but within hours their connected accounts need re-authorizing, with `REAUTHENTICATION_NEEDED` on the next tool call. Scalekit couldn't refresh the token. The usual cause is that the server's token endpoint expects the client credentials in a different place from where Scalekit sends them. 1. Find the methods the server accepts in `token_endpoint_auth_methods_supported` in its OAuth metadata, usually at `/.well-known/oauth-authorization-server`. 2. Open **AgentKit** > **Connections**, select the connection, and set **Token Endpoint Auth Method** to match, for example **Request Body (Post params)** for `client_secret_post`. See [Match the server's token endpoint auth method](https://docs.scalekit.com/agentkit/bring-your-own-connector/create-connector/#match-the-servers-token-endpoint-auth-method). 3. Have each affected user authorize again. If the method already matches, check whether the server issues refresh tokens at all. Some MCP servers issue only short-lived access tokens, so their users must authorize again when each token expires. ### "No tools found for this account. Try reauthorizing the account." The dashboard shows this on a connection's **Tools** tab when Scalekit has no tools for the selected connected account: it couldn't list the MCP server's tools for that account. Scalekit lists them when the user connects, and the server rejected the request or returned no tools. Common causes: - **The credentials don't work at the server.** The user entered the wrong API key or token, or the connector's auth type doesn't match what the server expects, such as **Bearer token** for a server that takes an API key. - **The OAuth client was rejected.** The server doesn't support Dynamic Client Registration, so the connection needs the client ID and secret of [your own OAuth app](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/), with the connection's redirect URI registered in that app. Don't enter a client ID copied from the provider's own MCP client. - **The server blocked Scalekit.** A self-hosted or restricted server may need to allow Scalekit's [outbound IP addresses](https://docs.scalekit.com/reference/outbound-ip-addresses/) and the connection's redirect URI. Fix the cause, then have the user authorize again. With [log streaming](https://docs.scalekit.com/agentkit/log-streaming/), each failure also arrives as a `connected_account.tool_discovery_failed` event, with the server's error. ## Get help Open **AgentKit** > **Connected Accounts** in the Scalekit dashboard to see the account's status and tool call logs. When you contact [support@scalekit.com](mailto:support@scalekit.com), include: - The Auth Request ID from the connection error page, if there was one - The connected account ID, or the user's identifier and the connection name - The full error text and when it happened - The steps that reproduce it ## Next - [Manage connected accounts](https://docs.scalekit.com/agentkit/connected-accounts/): Check an account's status and ask users to reconnect. - [Listen for account events](https://docs.scalekit.com/agentkit/account-events/): Get a webhook when an account expires or is disconnected. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `GET` [Get a connected account](https://docs.scalekit.com/agentkit/reference/connected-accounts/get-a-connected-account.md) - `GET` [Get a connected account's credentials](https://docs.scalekit.com/agentkit/reference/connected-accounts/get-connected-account-credentials.md) - `POST` [Get an authorization link](https://docs.scalekit.com/agentkit/reference/authorization/get-an-authorization-link.md) --- Source: https://docs.scalekit.com/agentkit/advanced/migrate-from-composio.md # Migrate from Composio to Scalekit Move an agent from Composio to Scalekit AgentKit: map concepts, recreate connections, have users re-authorize, then move tool calls, MCP and custom tools. This guide maps Composio concepts to their Scalekit AgentKit equivalents and walks through each migration step: SDK setup, authentication, tool execution, and MCP. Use it as a reference while porting your agent code. > note: Users must re-authorize > > OAuth refresh tokens are bound to the OAuth client that issued them. After migration, each user must complete the OAuth consent flow once through Scalekit to create a new connected account. Existing Composio tokens cannot be transferred. ## Concept mapping | Composio | Scalekit | Notes | |---|---|---| | Toolkit (e.g. `GITHUB`) | **Connector** (e.g. `github`) | Scalekit uses lowercase slugs | | Tool (e.g. `GITHUB_CREATE_ISSUE`) | **Tool** (e.g. `github_issue_create`) | Same concept, lowercase naming | | Auth config | **Connection** | OAuth app credentials, scopes, redirect URIs | | Connected account | **Connected account** | Per-user credential record | | `user_id` / entity ID | **`identifier`** | Your app's unique user ID, passed per API call | | Connect Link | **Authorization link** | OAuth redirect URL for user consent | | Session (`composio.create()`) | **`identifier`** on each call | No session to create: each call names the user | | Provider package (`composio_openai`) | `@scalekit-sdk/node` or `scalekit-sdk-python` | One SDK works with every framework | | `session.tools()` | `listScopedTools()` | Get tools a user is authorized to call | | `session.tools.execute()` | `executeTool()` | Execute a tool on behalf of a user | | `session.mcp.url` | **Virtual MCP server URL + session token** | Static server URL with a short-lived bearer token per agent run | | Custom tool (in-memory) | **Custom tool** (API Proxy) | Defined in your app code using `actions.request()` | | `executeToolRequest` (proxy) | `actions.request()` | Proxied REST API call | | Trigger | **Trigger** (beta) | Subscribe in the dashboard; events arrive at your endpoint as signed webhooks. See [How triggers work](https://docs.scalekit.com/agentkit/triggers/overview/) | | `COMPOSIO_SEARCH_TOOLS` | `searchTools()` | Rank tools by relevance to a task, or filter `listScopedTools()` by connection name | | `COMPOSIO_REMOTE_WORKBENCH` | Your own runtime | Run code where your agent runs | ## 1. Set up Scalekit 1. **Create a Scalekit account** Sign up at [app.scalekit.com](https://app.scalekit.com) and copy your API credentials from **Dashboard > Developers > Settings > API Credentials**. 2. **Set environment variables** Add them to a `.env` file that you don't commit. The code in step 4 loads it. ```bash title=".env" SCALEKIT_CLIENT_ID=your_client_id SCALEKIT_CLIENT_SECRET=your_client_secret SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com ``` 3. **Install the SDK** **Python** ```bash pip install scalekit-sdk-python python-dotenv ``` **Node.js** ```bash npm install @scalekit-sdk/node dotenv ``` 4. **Initialize the client** Scalekit uses a single client instance. There is no session object — you pass `identifier` on each API call. **Python** ```python import os from scalekit import ScalekitClient from dotenv import load_dotenv load_dotenv() # reads .env into the environment scalekit_client = ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) actions = scalekit_client.actions ``` **Node.js** ```typescript import 'dotenv/config'; // reads .env into process.env import { ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET! ); ``` ## 2. Configure connections In Composio, auth configs are created programmatically or via the dashboard. In Scalekit, you configure **connections** in the dashboard. For each Composio toolkit your agent uses (Gmail, Slack, GitHub, etc.), create a corresponding connection in **AgentKit** > **Connections** > **Add connection**. See [Configure a connection](https://docs.scalekit.com/agentkit/connections/) for the full walkthrough. | Composio auth type | Scalekit equivalent | |---|---| | OAuth 2.0 (Composio managed) | OAuth 2.0 (use Scalekit credentials to start, then bring your own) | | OAuth 2.0 (custom) | OAuth 2.0 (bring your own credentials) | | API key | API key (user provides during connected account creation) | | Bearer token | Bearer token | | Basic auth | Basic auth | > tip: Scalekit credentials for quick testing > > Many OAuth connectors offer a **Use Scalekit credentials** option that lets you skip OAuth app registration, and it works in production too. Before real users connect, switch to your own credentials, so the consent screen shows your app's name and your app gets its own rate limits. See [Bring your own OAuth](https://docs.scalekit.com/agentkit/advanced/bring-your-own-oauth/). ## 3. Migrate authentication Both platforms create per-user records (connected accounts) and generate OAuth links. The SDK methods differ. #### Create a connected account and authorize **Python** **Before (Composio):** ```python # Composio handles auth in-chat or via connect link session = composio.create(user_id="user_123") # Auth is triggered automatically when a tool requires it ``` **After (Scalekit):** ```python # Create or retrieve the connected account response = actions.get_or_create_connected_account( connection_name="gmail", identifier="user_123", ) connected_account = response.connected_account # Generate an authorization link if the account is not yet active if connected_account.status != "ACTIVE": link_response = actions.get_authorization_link( connection_name="gmail", identifier="user_123", ) auth_url = link_response.link # Redirect or send auth_url to the user ``` **Node.js** **Before (Composio):** ```typescript // Composio handles auth in-chat or via connect link const session = await composio.create("user_123"); // Auth is triggered automatically when a tool requires it ``` **After (Scalekit):** ```typescript import { ConnectorStatus } from '@scalekit-sdk/node'; // Create or retrieve the connected account const response = await scalekit.actions.getOrCreateConnectedAccount({ connectionName: 'gmail', identifier: 'user_123', }); const connectedAccount = response.connectedAccount; // Generate an authorization link if the account is not yet active if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { const linkResponse = await scalekit.actions.getAuthorizationLink({ connectionName: 'gmail', identifier: 'user_123', }); const authUrl = linkResponse.link; // Redirect or send authUrl to the user } ``` **Key difference:** Composio can trigger auth in-chat automatically. With Scalekit, your app explicitly creates the connected account and sends the authorization link to the user. Once the user completes the OAuth flow, the connected account becomes `ACTIVE` and your agent can execute tools. #### Check connected account status Composio tracks two statuses (`ACTIVE` and `INACTIVE`). Scalekit uses more granular states: | Scalekit status | Meaning | |---|---| | `PENDING_AUTH` | User hasn't completed authentication | | `PENDING_VERIFICATION` | Authentication complete; user verification still required | | `ACTIVE` | Credentials valid, ready for tool calls | | `EXPIRED` | Credentials expired or were revoked; re-authentication required | | `DISCONNECTED` | Account was disconnected | Check status before executing tools. If the account is not `ACTIVE`, generate a new authorization link. ## 4. Migrate tool calls #### List available tools **Python** **Before (Composio):** ```python session = composio.create(user_id="user_123") tools = session.tools() # all tools the user is authorized for ``` **After (Scalekit):** ```python tools_response, _ = scalekit_client.actions.tools.list_scoped_tools( identifier="user_123", # Required: list every connection whose tools the agent should see filter={"connection_names": ["gmail"]}, page_size=100, ) ``` **Node.js** **Before (Composio):** ```typescript const session = await composio.create("user_123"); const tools = await session.tools(); // all tools the user is authorized for ``` **After (Scalekit):** ```typescript const { tools } = await scalekit.tools.listScopedTools('user_123', { // Required: list every connection whose tools the agent should see filter: { connectionNames: ['gmail'] }, pageSize: 100, }); ``` #### Execute a tool **Python** **Before (Composio):** ```python session = composio.create(user_id="user_123") tools = session.tools() # Framework handles execution via the agent loop, or: # composio.tools.execute(tool_name="GMAIL_FETCH_MAILS", params={...}) ``` **After (Scalekit):** ```python result = actions.execute_tool( tool_name="gmail_fetch_mails", identifier="user_123", connection_name="gmail", tool_input={"query": "is:unread", "max_results": 5}, ) print(result.data) ``` **Node.js** **Before (Composio):** ```typescript const session = await composio.create("user_123"); const tools = await session.tools(); // Framework handles execution via the agent loop ``` **After (Scalekit):** ```typescript const result = await scalekit.actions.executeTool({ toolName: 'gmail_fetch_mails', identifier: 'user_123', connector: 'gmail', toolInput: { query: 'is:unread', max_results: 5 }, }); console.log(result.data); ``` **Key differences:** - Composio tool names are uppercase (`GMAIL_FETCH_MAILS`); Scalekit uses lowercase (`gmail_fetch_mails`) - Composio's session model means you don't pass `user_id` on each call. With Scalekit, pass `identifier` and the connection name on every `executeTool` call: `connection_name` in Python, `connector` in Node.js - Both return structured, LLM-ready output ### Map tool names Composio and Scalekit may name tools differently for the same connector. Browse the connector's tool list in the [Scalekit connector catalog](https://docs.scalekit.com/agentkit/connectors/) to find the exact tool names. Common patterns: | Composio tool name | Scalekit tool name | |---|---| | `GMAIL_FETCH_MAILS` | `gmail_fetch_mails` | | `SLACK_SEND_MESSAGE` | `slack_send_message` | | `GITHUB_CREATE_ISSUE` | `github_issue_create` | | `NOTION_CREATE_PAGE` | `notion_page_create` | Tool input schemas may also differ. Check each tool's parameters in the connector catalog and update your agent's tool input accordingly. ## 5. Migrate MCP Both platforms support MCP (Model Context Protocol) for framework-agnostic tool discovery and execution. **Before (Composio):** ```json { "mcpServers": { "composio": { "url": "https://backend.composio.dev/v3/mcp/{SERVER_ID}?user_id={USER_ID}", "headers": { "x-api-key": "" } } } } ``` **After (Scalekit):** Scalekit MCP uses Virtual MCP servers: 1. **Create a Virtual MCP server** — define which connections and tools the server exposes (one-time). This gives you a static `mcp_server_url`. 2. **Mint a session token** — before each agent run, call `create_session_token` for the user. Pass it as a bearer auth header. See [Virtual MCP servers](https://docs.scalekit.com/agentkit/mcp/overview/) for the full setup. ```json { "mcpServers": { "scalekit": { "url": "", "headers": { "Authorization": "Bearer " } } } } ``` **Key difference:** Composio embeds the user ID in the URL. Scalekit uses a static server URL shared across all users, with a short-lived session token per agent run for authentication. ## 6. Migrate custom tools In Scalekit, custom tools use **API Proxy mode** (`actions.request`). The proxy works out of the box for most connectors, with no extra configuration. If a call returns `proxy not enabled for provider`, see [Call any API](https://docs.scalekit.com/agentkit/tools/custom-tools/#proxy-not-enabled-for-provider). You define the tool contract in your application code and call the provider's REST endpoint through Scalekit, which injects the user's credentials automatically. | Composio approach | Scalekit approach | |---|---| | `@composio.tools.custom_tool` decorator | Define the tool in your app code | | In-memory, lost on restart | Lives in your codebase | | `executeToolRequest` for authenticated API calls | `actions.request()` — works out of the box for most connectors | **Python** ```python response = actions.request( connection_name="gmail", identifier="user_123", method="GET", path="/gmail/v1/users/me/messages", ) ``` **Node.js** ```typescript const response = await scalekit.actions.request({ connectionName: 'gmail', identifier: 'user_123', method: 'GET', path: '/gmail/v1/users/me/messages', }); ``` ## 7. Add custom connectors If your agent connects to an API or MCP server that isn't in Scalekit's built-in catalog, you can add your own connector. Custom connectors support OAuth 2.0, API keys, bearer tokens, and other auth types. Once created, they work exactly like built-in connectors — same connected account flow, same `actions.request()` proxy, same MCP tool calling. See [Add your own connector](https://docs.scalekit.com/agentkit/bring-your-own-connector/overview/) for the full walkthrough. ## Checklist - [ ] Scalekit account created, API credentials saved as environment variables - [ ] Scalekit SDK installed - [ ] Connections created in the Scalekit Dashboard for each connector - [ ] Connected account creation and authorization link flow ported - [ ] Tool names updated from uppercase to lowercase - [ ] `executeTool` calls updated with `identifier` and the connection name (`connection_name` in Python, `connector` in Node.js) - [ ] Tool input schemas verified against the Scalekit connector catalog - [ ] Virtual MCP server created and session token flow implemented (if using MCP) - [ ] Custom tools ported to `actions.request()` (if applicable) - [ ] Agent tested end-to-end with a test user - [ ] Users re-authorized through Scalekit's OAuth flow ## Next - [Quickstart](https://docs.scalekit.com/agentkit/quickstart/): Make your first tool call as a user. - [Choose a framework](https://docs.scalekit.com/agentkit/examples/): Build the same agent in another framework. ## API reference The endpoints this page's code calls, with every field and the SDK method for each: - `POST` [Create a connected account](https://docs.scalekit.com/agentkit/reference/connected-accounts/create-a-connected-account.md) - `POST` [Get an authorization link](https://docs.scalekit.com/agentkit/reference/authorization/get-an-authorization-link.md) - `POST` [Execute a tool](https://docs.scalekit.com/agentkit/reference/tools/execute-a-tool.md) - `GET` [List a user's scoped tools](https://docs.scalekit.com/agentkit/reference/tools/list-scoped-tools.md)