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
Section titled “Before you start”-
A Virtual MCP server and its ID. For cURL, keep the ID in
CONFIG_IDand the URL inMCP_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 explains why. -
A client set up as in the Quickstart:
actionsin Python,scalekitin Node.js. For cURL, get an access token and keep it inTOKEN:Terminal window 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)
-
Check the user’s connections
Section titled “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.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}")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}`);}Terminal window 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_AUTHand returns its link. -
Mint the token
Section titled “Mint the token”Set
expirya little above how long the run takes. It must be between 60 seconds and 24 hours, and defaults to one hour.from datetime import timedeltasession = actions.mcp.create_session_token(mcp_config_id=config_id,identifier="user_123",expiry=timedelta(minutes=30),)session_token = session.tokenprint(session.expires_at)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));Terminal window 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) -
Connect the MCP client
Section titled “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:import asyncioimport httpx2from mcp import ClientSessionfrom mcp.client.streamable_http import streamable_http_clientasync 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())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();Terminal window 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
mcp2.x (pip install "mcp>=2"), and the Node.js example uses@modelcontextprotocol/sdk. For clients without an MCP SDK, Connect any MCP client has the full HTTP contract, includingtools/calland the response format. Choose a framework shows the setup for CrewAI, Mastra and Claude Managed Agents.
Choose when to mint
Section titled “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
Section titled “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
Section titled “Common problems”401 with invalid or expired bearer token
Section titled “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
Section titled “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
Section titled “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.
API reference
The endpoints this page's code calls, with every field and the SDK method for each: