> **Building with AI coding agents?** Install the authstack plugin with one command. This equips your agent with accurate Scalekit implementation patterns.
>
> **Recommended**:
> ```bash
> npx @scalekit-inc/cli setup
> ```
>
> Global:
> ```bash
> npm install -g @scalekit-inc/cli
> scalekit setup
> ```
>
> Supports Claude Code, Cursor, GitHub Copilot, Codex + skills for other Agent Skills-compatible agents.
> Skills: integrate-agentkit, implement-saaskit, add-mcp-oauth, implement-sso, implement-scim.
> [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# 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.

## Before you start

- A [Virtual MCP server](/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](/agentkit/concepts/#choose-an-identifier) explains why.
- A client set up as in the [Quickstart](/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.

   **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.

   **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 <session token>` 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 ClientSession
   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 streamable_http_client(mcp_server_url, http_client=http_client) as (read, write):
               async with ClientSession(read, write) as mcp_session:
                   await mcp_session.initialize()
                   result = await mcp_session.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](/agentkit/mcp/connect-any-client/) has the full HTTP contract, including `tools/call` and the response format. [Choose a framework](/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"`.

### 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. If the server lists `connect_account`, the agent can get the link itself.

## Next

  - [Connect any MCP client](/agentkit/mcp/connect-any-client/): The HTTP contract for clients that don't use an MCP SDK.
  - [Choose a framework](/agentkit/examples/): Run an agent against the server in your framework.
  - [Verify users](/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)


---

## More Scalekit documentation

| Resource | What it contains | When to use it |
|----------|-----------------|----------------|
| [/llms.txt](/llms.txt) | Structured index with routing hints per product area | Start here — find which documentation set covers your topic before loading full content |
| [/llms-full.txt](/llms-full.txt) | Complete documentation for all Scalekit products in one file | Use when you need exhaustive context across multiple products or when the topic spans several areas |
| [sitemap-0.xml](https://docs.scalekit.com/sitemap-0.xml) | Full URL list of every documentation page | Use to discover specific page URLs you can fetch for targeted, page-level answers |
