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
Section titled “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. - Have the
.envfile from the Quickstart, and add your Anthropic API key asANTHROPIC_API_KEYto it. - The samples load
.envwithload_dotenv()frompython-dotenvin Python, andimport 'dotenv/config'in Node.js.
Install
Section titled “Install”pip install scalekit-sdk-python python-dotenv anthropicnpm install @scalekit-sdk/node @anthropic-ai/sdk dotenvFor 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
Section titled “Initialize”import osimport scalekit.clientfrom dotenv import load_dotenvimport anthropicfrom 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.actionsclient = anthropic.Anthropic()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
Section titled “Connect the user to Gmail”# Connect the user's Gmail account, and wait until it's ACTIVE before calling toolsconnection_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." )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 toolsconst 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 for production auth handling.
Run the agent
Section titled “Run the agent”Fetch tools scoped to this user, then run the full Claude tool-use loop. Use built-in tools explains the scoped tool list, paging past 100 tools, and how to read data when the app returns an error.
# Fetch tools scoped to this userscoped_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 loopmessages = [{"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"))// Fetch tools scoped to this userconst { 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 explicitlyconst 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 loopconst 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<string, unknown>, }); 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
Section titled “Use MCP instead”The Messages API can connect to a Scalekit Virtual MCP server through Anthropic’s 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.
from datetime import timedelta
# Returned by create_config when you created the Virtual MCP serverconfig_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 runmcp_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"))// 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 serverconst 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 and Mint session tokens to create the server, check the user’s connections and mint a token.
API reference
The endpoints this page's code calls, with every field and the SDK method for each: