Skip to content
Scalekit Docs
Talk to an Engineer Dashboard

LangGraph

Build a LangGraph agent that reads Gmail as the user with AgentKit: wire Scalekit tools into a ToolNode graph, or connect to a Virtual MCP server.

Build a LangGraph agent that reads a user’s Gmail inbox. The graph has two nodes: the model, and a ToolNode that runs the Scalekit tools the model asks for. In Python, Scalekit returns tools as LangChain tool objects. In Node.js, you wrap each tool with LangChain’s tool() helper.

Using LangChain’s agent loop without a graph? See LangChain.

  • 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 langgraph langchain-openai

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.

build_graph (buildGraph in Node.js) takes a list of tools and returns a compiled graph. The model node calls the LLM. When the LLM asks for tools, tools_condition routes to the ToolNode, which runs them and returns their results to the model. When the LLM answers without tool calls, the graph ends.

Fetch the tools scoped to this user and run the graph. Use built-in tools explains the scoped tool list, paging past 100 tools, and how to read data when the app returns an error.

from langchain_openai import ChatOpenAI
from langgraph.graph import START, MessagesState, StateGraph
from langgraph.prebuilt import ToolNode, tools_condition
def build_graph(tools):
llm = ChatOpenAI(model="gpt-4o").bind_tools(tools)
def call_model(state: MessagesState):
return {"messages": [llm.invoke(state["messages"])]}
builder = StateGraph(MessagesState)
builder.add_node("agent", call_model)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "agent")
# Run the tools the model asked for, or end when it answers without tool calls
builder.add_conditional_edges("agent", tools_condition)
builder.add_edge("tools", "agent")
return builder.compile()
tools = actions.langchain.get_tools(
identifier=identifier,
connection_names=[connection_name],
page_size=100, # up to 100 tools per page; page with page_token for more
)
graph = build_graph(tools)
result = graph.invoke(
{"messages": [("user", "Fetch my last 5 unread emails and summarize them")]}
)
print(result["messages"][-1].content)

When the app itself rejects a request, the tool doesn’t raise: the app’s error response comes back as the tool’s result, and the model reads it like any other result. If a tool raises an error instead, such as a network failure, the two languages differ:

  • Node.js: ToolNode returns the error to the model.
  • Python: ToolNode returns only invalid-argument errors to the model, and raises the rest. To return every error to the model, create the node with ToolNode(tools, handle_tool_errors=True).

The script prints a summary of the user’s unread emails. result["messages"] (result.messages in Node.js) holds the whole run: the user message, the model’s tool calls, a tool message with each Gmail response, and the final answer.

To give the graph the tools of a Virtual MCP server, load them with LangChain’s MCP adapters and pass them to the same build_graph. Pass the server URL and a session token for this user.

Terminal window
pip install "langchain-mcp-adapters>=0.3,<1"
import asyncio
import os
from datetime import timedelta
from langchain_mcp_adapters.client import MultiServerMCPClient
# 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():
client = MultiServerMCPClient(
{
"scalekit": {
"transport": "streamable_http",
"url": mcp_url,
"headers": {"Authorization": f"Bearer {mcp_token}"},
}
}
)
graph = build_graph(await client.get_tools())
result = await graph.ainvoke(
{"messages": [("user", "Fetch my last 5 unread emails and summarize them")]}
)
print(result["messages"][-1].content)
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: