> **Building with AI coding agents?** Install the authstack plugin with one command. This equips your agent with accurate Scalekit implementation patterns.
>
> **Recommended** (the `-y` flags skip prompts, so the command runs without an interactive terminal):
> ```bash
> npx -y @scalekit-inc/cli setup -y
> ```
>
> Global:
> ```bash
> npm install -g @scalekit-inc/cli
> scalekit setup -y
> ```
>
> Supports Claude Code, Cursor, GitHub Copilot, Codex + skills for other Agent Skills-compatible agents in `~/.agents/skills`.
> Skills: integrate-agentkit, implement-saaskit, add-mcp-oauth, implement-sso, implement-scim.
> [Full setup guide](https://docs.scalekit.com/dev-kit/build-with-ai/)

---

# 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](/agentkit/examples/langchain/).

## 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](/agentkit/connections/).
- Have the `.env` file from the [Quickstart](/agentkit/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.

## Install

  **Python**

  ```sh
  pip install scalekit-sdk-python python-dotenv langgraph langchain-openai
  ```

  **Node.js**

  ```sh
  npm install @scalekit-sdk/node dotenv @langchain/langgraph @langchain/core @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`.

## Initialize

  **Python**

  ```python
  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
  ```

  **Node.js**

  ```typescript
  import '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

  **Python**

  ```python
  # 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."
      )
  ```

  **Node.js**

  ```typescript
  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 tools
  const 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](/agentkit/tools/authorize/) for production auth handling.

## Build and run the graph

`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](/agentkit/tools/scalekit-optimized-tools/) explains the scoped tool list, paging past 100 tools, and how to read `data` when the app returns an error.

  **Python**

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

  **Node.js**

  ```typescript
  import { ChatOpenAI } from '@langchain/openai';
  import { tool, type StructuredToolInterface } from '@langchain/core/tools';
  import { MessagesAnnotation, START, StateGraph } from '@langchain/langgraph';
  import { ToolNode, toolsCondition } from '@langchain/langgraph/prebuilt';

  function buildGraph(tools: StructuredToolInterface[]) {
    const llm = new ChatOpenAI({ model: 'gpt-4o' }).bindTools(tools);
    return new StateGraph(MessagesAnnotation)
      .addNode('agent', async (state) => ({ messages: [await llm.invoke(state.messages)] }))
      .addNode('tools', new ToolNode(tools))
      .addEdge(START, 'agent')
      // Run the tools the model asked for, or end when it answers without tool calls
      .addConditionalEdges('agent', toolsCondition)
      .addEdge('tools', 'agent')
      .compile();
  }

  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 explicitly
  const definitions = scopedTools.flatMap((t) => (t.tool?.definition ? [t.tool.definition] : []));
  const tools = definitions.map((definition) =>
    tool(
      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);
      },
      {
        name: String(definition.name),
        description: String(definition.description ?? ''),
        schema: definition.input_schema as Record<string, unknown>,
      },
    ),
  );

  const graph = buildGraph(tools);
  const result = await graph.invoke({
    messages: [{ role: 'user', content: 'Fetch my last 5 unread emails and summarize them' }],
  });
  console.log(result.messages.at(-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)`.

## Check it worked

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.

## Use MCP instead

To give the graph the tools of a [Virtual MCP server](/agentkit/mcp/overview/), 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.

  **Python**

  ```sh
  pip install "langchain-mcp-adapters>=0.3,<1"
  ```

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

  **Node.js**

  ```sh
  npm install @langchain/mcp-adapters
  ```

  ```typescript
  import { MultiServerMCPClient } from '@langchain/mcp-adapters';

  // Returned by createConfig when you created the Virtual MCP server
  const 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 run
  const session = await scalekit.actions.mcp.createSessionToken({
    mcpConfigId: configId,
    identifier,
    expirySeconds: 60 * 60,
  });

  const mcpClient = new MultiServerMCPClient({
    servers: {
      scalekit: {
        transport: 'http',
        url: mcpUrl,
        headers: { Authorization: `Bearer ${session.token}` },
      },
    },
  });
  try {
    const mcpGraph = buildGraph(await mcpClient.getTools());
    const mcpResult = await mcpGraph.invoke({
      messages: [{ role: 'user', content: 'Fetch my last 5 unread emails and summarize them' }],
    });
    console.log(mcpResult.messages.at(-1)?.content);
  } finally {
    await mcpClient.close();
  }
  ```

See [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/) and [Mint session tokens](/agentkit/mcp/session-tokens/) to create the server, check the user's connections and mint a token.

## Next

  - [Manage connected accounts](/agentkit/connected-accounts/): Check an account's status and ask users to reconnect.
  - [AgentKit launch checklist](/agentkit/advanced/launch-checklist/): Everything to finish before real users connect.

## API reference

The endpoints this page's code calls, with every field and the SDK method for each:

- `POST` [Execute a tool](https://docs.scalekit.com/agentkit/reference/tools/execute-a-tool.md)
- `GET` [List a user's scoped tools](https://docs.scalekit.com/agentkit/reference/tools/list-scoped-tools.md)
- `POST` [Mint a session token](https://docs.scalekit.com/agentkit/reference/virtual-mcp-servers/mint-a-session-token.md)


---

## More Scalekit documentation

| Resource | What it contains | When to use it |
|----------|-----------------|----------------|
| [/llms.txt](/llms.txt) | Structured index with routing hints per product area | Start here — find which documentation set covers your topic before loading full content |
| [/llms-full.txt](/llms-full.txt) | Complete documentation for all Scalekit products in one file | Use when you need exhaustive context across multiple products or when the topic spans several areas |
| [sitemap-0.xml](https://docs.scalekit.com/sitemap-0.xml) | Full URL list of every documentation page | Use to discover specific page URLs you can fetch for targeted, page-level answers |
