Skip to content
Scalekit Docs
Talk to an Engineer Dashboard

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.

  • 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 .env file from the 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.
Terminal window
pip install scalekit-sdk-python python-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.

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

Later blocks reuse this setup.

# 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 for production auth handling.

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 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)

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.

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

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 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())

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: