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 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.
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 OpenAI API key asOPENAI_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 openai-agentsnpm install @scalekit-sdk/node dotenv @openai/agentsFor 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 osfrom dotenv import load_dotenvfrom 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.actionsimport '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
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 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 explains the scoped tool list, paging past 100 tools, and how to read data when the app returns an error.
import asyncioimport json
from agents import Agent, FunctionTool, Runnerfrom 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)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 explicitlyconst 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<string, unknown>, }); 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
Section titled “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
Section titled “Use MCP instead”The Agents SDK connects to MCP servers natively. Point it at a Virtual MCP server 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.
import asyncioimport osfrom datetime import timedelta
from agents import Agent, Runnerfrom agents.mcp import MCPServerStreamableHttp
# 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=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())import { MCPServerStreamableHttp } from '@openai/agents';
// Returned by createConfig when you created the Virtual MCP serverconst 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 runconst 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 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: