> ## Documentation Index
> Fetch the complete documentation index at: https://nango.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent sessions

> Optimized interface for AI agents to use integrations

Agent sessions are an optimized interface for AI agents to use integrations.

Each session has three parts:

* **Tenant** — on whose behalf the agent can call integrations
* **Toolset** — which integrations and tools it can use
* **Discovery** — how it finds and calls those tools

Your backend creates the session and exposes it to the agent as an MCP server. The agent never sees raw credentials, and cannot change the scope of its own session.

The available toolset can include Nango catalog tools, tools you manage yourself, and the tools of [MCP servers](#mcp-server-tools) your tenant has connected. Catalog tools are available out of the box and update automatically.

Example sessions:

* A sales agent with a user's personal Gmail & Google Calendar, and a read-only org-wide Salesforce connection
* A finance agent with a few read-only tools on an org's accounting system, and read & write access to a team's Slack
* A developer agent with a team's Linear access and a user's personal GitHub access

> [!NOTE]
>
> We are quickly expanding agent sessions, follow our [changelog](https://nango.dev/docs/updates/changelog.md) for announcements and improvements!

## Quickstart

**1. Create a session from your backend**

By default, a session covers every tool on the integrations the tenant has a connection for.

To define the tenant, select the connections the agent may use with tag selectors:

```bash theme={null}
curl --request POST \
  --url 'https://api.nango.dev/sessions' \
  --header 'Authorization: Bearer <NANGO-API-KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "tenant": {
      "connections": {
        "any": [{ "tags": { "organization_id": "acme", "workspace": "marketing" } }]
      }
    }
  }'
```

Requires an API key with the `environment:agent_sessions:write` scope. See [API key scopes](https://nango.dev/docs/reference/backend/http-api/api-keys.md#scopes).

This returns a ready-to-use session:

```json theme={null}
{
  "data": {
    "session_id": "0f4a...",
    "session_token": "nango_agent_session_...",
    "mcp_url": "https://api.nango.dev/session/0f4a.../mcp",
    "expires_at": "2026-09-15T10:00:00.000Z",
    "toolset": {
      "notion": { "connected": true, "tools_pinned": 0, "tools_searchable": 12 },
      "slack": { "connected": true, "tools_pinned": 0, "tools_searchable": 7 }
    },
    "meta_tools": { "nango_tool_search": true, "nango_execute": true, "nango_proxy": false }
  }
}
```

**2. Pass the MCP server to your agent**

Pass the `mcp_url` to your agent, with the `session_token` as bearer auth:

> [tab] OpenAI
>
> ```typescript theme={null}
> import OpenAI from "openai";
>
> const client = new OpenAI();
>
> const response = await client.responses.create({
>   model: "gpt-5.6",
>   input: "Summarize the last 5 emails in the user's inbox",
>   tools: [
>     {
>       type: "mcp",
>       server_label: "nango", // Or a name suitable for your session
>       server_url: "<mcp_url>",
>       authorization: "<session_token>",
>       require_approval: "never"
>     }
>   ]
> });
>
> console.log(response.output_text);
> ```
>
> The Responses API never stores the `authorization` value, so send it on every request.

> [tab] Anthropic
>
> ```typescript theme={null}
> import Anthropic from "@anthropic-ai/sdk";
>
> const client = new Anthropic();
>
> const message = await client.beta.messages.create({
>   model: "claude-opus-5",
>   max_tokens: 1024,
>   messages: [{ role: "user", content: "Summarize the last 5 emails in the user's inbox" }],
>   mcp_servers: [
>     {
>       type: "url",
>       name: "nango",
>       url: "<mcp_url>",
>       authorization_token: "<session_token>"
>     }
>   ],
>   tools: [{ type: "mcp_toolset", mcp_server_name: "nango" }],
>   betas: ["mcp-client-2025-11-20"]
> });
>
> console.log(message.content);
> ```
>
> The MCP connector is behind the `mcp-client-2025-11-20` beta.

> [tab] Vercel AI SDK
>
> ```typescript theme={null}
> import { createMCPClient } from "@ai-sdk/mcp";
> import { openai } from "@ai-sdk/openai";
> import { generateText, stepCountIs } from "ai";
>
> const mcpClient = await createMCPClient({
>   transport: {
>     type: "http",
>     url: "<mcp_url>",
>     headers: { Authorization: "Bearer <session_token>" }
>   }
> });
>
> try {
>   const { text } = await generateText({
>     model: openai("gpt-5.6"),
>     prompt: "Summarize the last 5 emails in the user's inbox",
>     tools: await mcpClient.tools(),
>     stopWhen: stepCountIs(10)
>   });
>
>   console.log(text);
> } finally {
>   await mcpClient.close();
> }
> ```

> [tab] LangChain
>
> ```typescript theme={null}
> import { MultiServerMCPClient } from "@langchain/mcp-adapters";
> import { createAgent } from "langchain";
>
> const client = new MultiServerMCPClient({
>   mcpServers: {
>     nango: {
>       transport: "http",
>       url: "<mcp_url>",
>       headers: { Authorization: "Bearer <session_token>" }
>     }
>   }
> });
>
> const agent = createAgent({
>   model: "openai:gpt-5.6",
>   tools: await client.getTools()
> });
>
> const result = await agent.invoke({
>   messages: [{ role: "user", content: "Summarize the last 5 emails in the user's inbox" }]
> });
>
> console.log(result.messages.at(-1)?.content);
> ```

> [tab] Mastra
>
> ```typescript theme={null}
> import { openai } from "@ai-sdk/openai";
> import { Agent } from "@mastra/core/agent";
> import { MCPClient } from "@mastra/mcp";
>
> const mcp = new MCPClient({
>   id: "nango-session",
>   servers: {
>     nango: {
>       url: new URL("<mcp_url>"),
>       requestInit: {
>         headers: { Authorization: "Bearer <session_token>" }
>       }
>     }
>   }
> });
>
> const agent = new Agent({
>   name: "Integrations agent",
>   instructions: "Use the Nango tools to complete the user's request.",
>   model: openai("gpt-5.6"),
>   tools: await mcp.getTools()
> });
>
> const response = await agent.generate("Summarize the last 5 emails in the user's inbox");
>
> console.log(response.text);
> ```
>
> Give each session's client a distinct `id` if you create one per request.

Your agent can now use every tool the session exposes.

## Session basics

### Creating a session

Create a session with the tools and access your agent needs.

```jsonc theme={null}
{
  "tenant": {
    "connections": {  // Either "any" or "pinned" is required
      "any": [
        { "tags": { "endUserId": 74, "organizationId": "7283" }},
        { "tags": { "endUser": 74, "workspaceSlug": "slack-marketing-workspace" }}
      ],
      "pinned": [
        { "integration_id": "notion", "connection_id": "882386f9-efae-48aa-bea1-58a4f6fbc2ff" }
      ]
    }
  },

  "toolset": {       // Optional, defaults to all integrations
    "notion": { "allow": { "tools": ["read_doc", "upsert_doc"] } },
    "slack": "*"
  },
 
  "pinned_tools": {  // Optional, defaults to no pinned tools
    "notion": ["read_doc"]
  },

  "meta_tools": {    // Optional. nango_proxy is off unless you turn it on
    "nango_tool_search": true,
    "nango_execute": true,
    "nango_proxy": false
  },

  "expires_in": "1h" // Optional. Minimum "60s", maximum and default "15d".
}
```

Creation fails if your tenant selectors are [not unique](#resolving-ambiguity), or if a toolset or tool doesn't exist in your Nango environment.

Each session gets a unique `session_id` and `session_token`. The token is a scoped access token for that session — treat it as a secret, and keep session durations as short as your use case allows.

### Session MCP server

Every session exposes its own MCP server at its `mcp_url`. It speaks Streamable HTTP and authenticates with the `session_token` as a bearer token, so any MCP-capable agent or client can connect to it.

`tools/list` returns the enabled meta tools plus the pinned tools, paginated 50 at a time through `nextCursor`. Searchable tools are callable but not listed, and the agent finds them with the `nango_tool_search` meta tool.

A tool called directly by its name takes its input under `input`, next to the task's `goal` and the call's `intent`, the same way [`nango_execute`](#nango_execute) does. A tool that takes no input takes only `goal` and `intent`.

```json theme={null}
{
    "input": { "timeMin": "2026-11-05T00:00:00Z", "timeMax": "2026-11-06T00:00:00Z" },
    "goal": "move my Thursday 1:1 with my manager to Friday and let them know",
    "intent": "find the 1:1 to move"
}
```

### Terminating a session

Sessions are immutable and cannot be extended. They expire on their own once `expires_in` is up, and you can end one early with `DELETE /sessions/{session_id}`:

```bash theme={null}
curl --request DELETE \
  --url 'https://api.nango.dev/sessions/0f4a...' \
  --header 'Authorization: Bearer <NANGO-API-KEY>'
```

Requires an API key with the `environment:agent_sessions:write` scope, the same one session creation uses.

```json theme={null}
{
  "data": {
    "session_id": "0f4a...",
    "ended_at": "2026-09-15T10:00:00.000Z",
    "reason": "terminated"
  }
}
```

Terminating revokes the `session_token` together with the session, so the session's MCP server answers `401` from that point on. A tool call already in flight is not cancelled.

Terminating is idempotent. A session that has already ended keeps its original `ended_at` and `reason`, so terminating it again returns the same response. An unknown session, or one from another environment, returns a `404`.

Recreate the session with the same parameters if you need it again.

## Tenant

The tenant defines on whose behalf the agent may act. For example: use a specific user's Gmail credentials, but the org-wide credentials for Salesforce.

You express this with [Connection tag](https://nango.dev/docs/guides/auth/connection-tags-configuration-metadata.md#connection-tags) selectors under `any`, which the session resolves to the right Connections in Nango. Tags within one entry are ANDed, entries are ORed, up to 10 entries.

Here, `organization_id`, `workspace`, and `user_id` are Connection tags we set up for our application.

```json theme={null}
{
  "tenant": {
    "connections": {
      "any": [
        { "tags": { "organization_id": "acme", "workspace": "marketing" } },
        { "tags": { "user_id": "27382" } }
      ]
    }
  }
}
```

Selectors must resolve to **exactly one connection per integration** at creation time. The resolved connection is stored on the session, and every tool call the agent makes afterwards uses it.

### Resolving ambiguity

If the selectors match two or more connections on the same integration, there is no way to tell which one the session should use. Creation fails with `ambiguous_connections`, and no session is created:

```json theme={null}
{
  "error": {
    "code": "ambiguous_connections",
    "message": "1 integration matched more than one connection. Narrow the connection tags or pin a connection id.",
    "payload": {
      "integrations": {
        "notion": {
          "match_count": 2,
          "candidates": [{ "connection_id": "3fd0cc49-..." }, { "connection_id": "f91890c5-..." }]
        }
      }
    }
  }
}
```

There are two ways to resolve this:

1. Add tags to the selector until it matches exactly one connection per integration.
2. Use `pinned` to name the connection to use for that integration.

```json theme={null}
{
  "tenant": {
    "connections": {
      "any": [{ "tags": { "organization_id": "acme" } }],
      "pinned": [{ "integration_id": "notion", "connection_id": "f91890c5-..." }]
    }
  }
}
```

You can only use one pin per integration. A pin must be one of the connections the selectors matched, or creation fails with `pinned_connection_not_matched`. Pinning a connection that does not exist fails with `unknown_pinned_connection`.

You can also use `pinned` without any `any` selectors, if you already know the exact connections you want to use.

An integration that matches no connection does not fail session creation. It appears in the toolset as `connected: false`, and its tool calls fail with an error saying it is not connected.

What that error tells the agent to do next depends on whether [`nango_create_connection`](#nango_create_connection) is enabled. With it off, the agent is told it cannot fix this itself, to pass it to the user and carry on with the tools it has. With it on, the error names the tool so the agent can fetch a connect link and hand that over instead.

## Toolsets & tools

`toolset` defines which tools are available to the agent.

The toolset is a subset of the Integrations that are enabled in your Nango environment.

```json theme={null}
{
  "toolset": {
    "notion": { "allow": { "tools": ["read_doc", "upsert_doc"] } },
    "slack": "*"
  }
}
```

You can use the following selectors:

* `"*"` on an integration means every tool on it, and every tool of its MCP server
* `allow` makes the integration an allowlist, where `"*"` means every tool
* `deny` always subtracts from whatever `allow` gave
* To deny an integration entirely, leave it out of the toolset
* `toolset: "*"` at the top level means every integration in the environment
* Leaving `toolset` out entirely means every integration the tenant resolved a connection for, which is narrower than `"*"`

> [!WARNING]
>
> `allow` and `deny` limit which tools the agent can call, not which API endpoints it can reach. With [`nango_proxy`](#nango_proxy) on, the agent can call any endpoint of a connected integration in the toolset, including ones your deny list was meant to keep out. For an MCP server, that includes sending `tools/call` to the server directly, for any of its tools.

Sessions can expose the tools in your integration and [MCP server tools](#mcp-server-tools). Naming a sync fails with `unsupported_function_type`, an unknown integration with `unknown_integration`, and an unknown tool with `unknown_tool`. Any error fails creation, so a session is either fully valid or not created.

### Pinned tools

By default, everything in the toolset is searchable with the `nango_tool_search` meta tool, but none of it is listed in the MCP server's tool list. This keeps a large toolset from filling the agent's context with hundreds of definitions it will never call.

`pinned_tools` exposes specific tools in that list, so they are in context as soon as the agent loads the server:

```json theme={null}
{
  "toolset": { "notion": { "allow": { "tools": ["read_doc", "upsert_doc"] } } },
  "pinned_tools": { "notion": ["read_doc"] }
}
```

A pinned tool must be in the toolset, otherwise creation fails with `tool_not_in_toolset`.

### MCP server tools

Integrations for MCP servers, such as [Notion (MCP)](https://nango.dev/docs/api-integrations/notion-mcp.md) or [Linear (MCP)](https://nango.dev/docs/api-integrations/linear-mcp.md), bring the server's own tools into the session. There is nothing to deploy and nothing to turn on. Include the integration in the toolset like any other and give the tenant a connection to it.

When the session is created, Nango lists the tools of each connected MCP server in the toolset and adds them alongside its Nango tools. From then on they work like any other tool:

* `nango_tool_search` finds them, with the input schema the server gave
* Pinned, they are listed with that input schema and the server's annotations
* Calling one by its listed name or through `nango_execute` runs it on the server, on the session's connection, and returns the server's result as is

```json theme={null}
{
  "toolset": {
    "notion-mcp": { "allow": { "tools": ["notion-search", "notion-fetch"] } },
    "linear-mcp": { "deny": { "tools": ["delete_comment"] } }
  },
  "pinned_tools": { "notion-mcp": ["notion-search"] }
}
```

`allow`, `deny` and `pinned_tools` name a tool exactly as the server lists it. In the session, the tool gets a name like a Nango tool's, such as `notion-mcp__notion-search`. Agents call it by the name the tool list or `nango_tool_search` gives.

The list is taken once, at creation. Tools the server adds or removes afterwards don't change the session, so create a new one to pick them up.

#### When a server can't be listed

A server that fails or does not answer `tools/list` within 10 seconds does not fail session creation. The session is created without that server's tools, and the toolset summary says so with `mcp_server`:

```json theme={null}
{
  "toolset": {
    "notion-mcp": { "connected": true, "tools_pinned": 0, "tools_searchable": 0, "mcp_server": "unavailable" },
    "linear-mcp": { "connected": true, "tools_pinned": 1, "tools_searchable": 23, "mcp_server": "available" }
  }
}
```

`mcp_server` is only present for integrations with a connected MCP server.

#### Running MCP server tools

Every call goes through the [Nango proxy](https://nango.dev/docs/guides/platform/proxy-requests.md), so Nango applies and refreshes the connection's credentials, and the call shows up in Nango logs as a proxy operation.

A result the server marks with `isError` reaches the agent unchanged. When the call itself fails, the agent gets an error that says what to do next:

* **Timeout:** a tool has 60 seconds to answer. The agent is told it may still have run, so it checks before calling again.
* **Too large:** results are capped at 5 MB. The agent is told to narrow its input.
* **Refused:** a `403` on one tool, for example a missing OAuth scope, fails that tool only. A `401`, or a `403` before any tool runs, means the connection needs reconnecting, see [reconnecting a connection](#reconnecting-a-connection).

## Meta tools

Meta tools are Nango's own tools. They are always pinned to the MCP server's tool list.

| Tool | What it does |
| - | - |
| `nango_tool_search` | Finds tools in the session's toolset from a few keywords per tool and the task's goal |
| `nango_execute` | Runs one of the session's tools by name |
| `nango_proxy` | Makes an authenticated request to a provider API, for when no tool fits. Bypasses `allow` and `deny`, [see the warning](#toolsets-%26-tools) |
| `nango_create_connection` | Returns a connect link for one of the session's integrations that has no connection, or a reconnect link for one that has |

`nango_tool_search` and `nango_execute` default to `true`, `nango_proxy` and `nango_create_connection` to `false`. Set them on session creation with `meta_tools`:

```json theme={null}
{ "meta_tools": { "nango_execute": false, "nango_proxy": true } }
```

### nango\_tool\_search

Takes up to five `queries`, each a few keywords naming one action, and a `goal` with the task the user asked for, in their words. An agent that needs several tools searches for all of them in one call.

```json theme={null}
{
    "queries": ["find emails by subject", "forward an email"],
    "goal": "find the invoice email from last week and forward it to accounting"
}
```

The result holds one entry in `results` per query, in the order sent. Each entry carries its `query`, `guidance` for the agent, `matches` for the best hits, and `related` tools for weaker matches to narrow down. Each match names the `tool` to pass to `nango_execute`, its `integration`, `action`, and `provider`, whether it is `listed`, and its `connection` status.

Only a best match includes `input`, its argument schema — a weak match is a lead, not something to call yet.

### nango\_execute

Runs one of the session's tools by name, on the connection the session resolved for that integration. Along with the tool and its input, it takes the task's `goal` and an `intent`, why this call is made.

```json theme={null}
{
    "tool": "google-calendar__list-events",
    "input": { "timeMin": "2026-11-05T00:00:00Z", "timeMax": "2026-11-06T00:00:00Z" },
    "goal": "move my Thursday 1:1 with my manager to Friday and let them know",
    "intent": "find the 1:1 to move"
}
```

The goal stays the same on every call made for one task. The intent changes with each call. A later call to the same tool for Friday keeps the goal and sends the intent `check Friday has a free slot`.

Execution is synchronous and appears in Nango logs. See [Limits](https://nango.dev/docs/guides/platform/limits.md#function-execution-limits) for execution limits.

### nango\_proxy

The escape hatch for when no tool in the session covers what the agent needs. It makes an authenticated HTTP request to a provider API on the connection the session resolved for that integration, so the agent names an integration and never a connection.

```json theme={null}
{
    "integration": "notion",
    "method": "POST",
    "path": "/v1/search",
    "body": { "query": "roadmap" },
    "goal": "find the roadmap doc and summarize what ships this quarter",
    "intent": "no tool searches pages by title, a search pages tool would"
}
```

Like the other tools, it takes the task's `goal`. Its `intent` says why no tool fit and which tool would have done it.

It also takes `query_params` and `headers`. Nango authenticates the request from the session's connection, so credential headers are rejected rather than forwarded, including the provider's own (`x-api-key` and the like) alongside `authorization`.

The integration has to be one in the session's toolset with a resolved connection, but the path is not restricted to what the toolset exposes. That is why `nango_proxy` is off by default: turning it on lets the agent reach any endpoint of a connected integration.

The result is the provider's `status`, `headers` and `body`, for JSON or UTF-8 text responses up to 5 MB. Binary and larger responses are not supported, and neither is file upload. Use the [HTTP proxy](https://nango.dev/docs/reference/backend/http-api/proxy) for those.

### nango\_create\_connection

Lets the agent get the user to connect an integration that has none, instead of dead-ending on it. It takes one of the session's integrations and returns a link for the user to open.

```json theme={null}
{ "integration": "notion" }
```

```json theme={null}
{
  "guidance": "Give this link to the user and wait...",
  "integration": "notion",
  "provider": "notion",
  "connect_url": "https://connect.nango.dev/?session_token=...",
  "expires_at": "2026-09-21T17:06:41.145Z"
}
```

The agent passes the link to the user, who authorizes it the same way as any [connect link](https://nango.dev/docs/guides/auth/share-connect-link.md), and the tools for that integration work for the rest of the session once the connection is created.

Nango tags the new connection with `nango/agent_session` so the session can find it. You can configure the tool to add your own tags, allowing the next session to use tag selectors to include the connection:

```json theme={null}
{ "meta_tools": { "nango_create_connection": { "enabled": true, "tags": { "organization_id": "acme" } } } }
```

#### Reconnecting a connection

A connection can stop working mid-session: its credentials expired and can no longer be refreshed, or a tool needs a scope the connection was not granted. With `nango_create_connection` enabled, the agent can get the user to reconnect it without leaving the session. It needs no extra configuration.

The user gets a [reconnect link](https://nango.dev/docs/guides/auth/auth-guide.md#build-reauthorization) and authorizes the session's existing connection again. A reconnect requests the integration's current scopes, so it fixes a missing scope the user declined the first time, or one you have since added to the integration. The session's tools then run on the new credentials.

* **Same connection:** it keeps its connection ID and its tags. The tags configured on the tool are only added to new connections.
* **Works for either kind of connection:** one the session resolved from the tenant when it was created, or one the agent created during the session.
* **Nothing to reconnect:** if the integration has no connection yet, the user gets a link to create one instead.
* **Deleted connection:** a deleted connection cannot be reconnected, so a new session is needed.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.