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

# MCP Connectors

> Let a PAL delegate multi-step work to your MCP servers during a conversation, and follow it from your client.

A **connector** is an MCP server you register once with [Create Connector](/api-reference/connectors/create-connector) - Linear, GitHub, Notion, your own - and attach to a PAL. A PAL with connectors attached can hand work off in the background: it acknowledges the request, keeps talking to the user, and reports the outcome when it lands.

This is different from [LLM tools](/sections/conversational-video-interface/pal/llm-tool), where one call returns one result. A delegated task can take several steps across several services - *"find the GitHub PR for that ticket and tell me if it merged"* is one task, not three tool calls you have to orchestrate.

## Registering a connector

Two calls, once per server:

1. [Create Connector](/api-reference/connectors/create-connector) with the server URL and how to authenticate. For an OAuth server the response carries an `authorize_url` - send the customer there and Tavus stores the tokens when they come back.
2. Attach it to a PAL, below.

There is nothing to import: the PAL connects to the server and discovers its tools itself, so a connector is usable as soon as it is created and (for OAuth servers) linked. [Preview Connector Tools](/api-reference/connectors/preview-connector-tools) shows what a server exposes if you want to display that in your UI.

Check `oauth_status` on the connector to see where an OAuth server stands: `linked` is ready, `needs_reauth` means the customer must go through [Reconnect OAuth](/api-reference/connectors/reconnect-connector-oauth) again.

## Attaching connectors to a PAL

Set `layers.mcp.connectors` to the connector ids you want reachable - either on [Create PAL](/api-reference/pals/create-pal), or on an existing PAL with [Patch PAL](/api-reference/pals/patch-pal). There is no separate attach/detach call.

```json Create PAL with two connectors attached theme={null}
{
  "pal_name": "Engineering assistant",
  "layers": {
    "mcp": {
      "connectors": ["c8-58ea0f6420b2", "c0-8982a37dc5e0"]
    }
  }
}
```

Patch PAL takes [JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) operations, not a merge body, so attaching to an existing PAL is an `add` (or `replace`, once `layers.mcp` is already set) against `/layers/mcp/connectors`:

```json Patch PAL to attach a connector theme={null}
[
  { "op": "add", "path": "/layers/mcp/connectors", "value": ["c8-58ea0f6420b2"] }
]
```

| Field        | Type             | Description                                                                                    |
| ------------ | ---------------- | ---------------------------------------------------------------------------------------------- |
| `connectors` | array of strings | Connector ids. Up to 10 per PAL. Each must be a connector you own, or the request is rejected. |

<Note>
  Connectors are attached per PAL, not per conversation. Detaching a connector is the off switch.
</Note>

## Choosing which tools a PAL may call

A connector's tools are all included by default. `layers.mcp.connector_tools` scopes that down: a map from connector id to the tool names the PAL may call. One connection can be used across PALs with different scopes. For example, a Customer Support PAL can scope the Slack connection to write access, while a Sales PAL scopes the same connection to read-only access. It is the same connector and the same OAuth grant, permissioned per PAL.

### 1. List the server's tools

[Preview Connector Tools](/api-reference/connectors/preview-connector-tools) returns the server's own `tools/list` response, untouched. The `name` of each entry is what you scope with:

```json GET /v2/connectors/c8-58ea0f6420b2/preview theme={null}
{
  "tools": [
    {
      "name": "search_messages",
      "description": "Search messages across channels.",
      "inputSchema": { "type": "object", "properties": { "query": { "type": "string" } } }
    },
    { "name": "list_channels", "description": "List channels in the workspace." },
    { "name": "post_message", "description": "Post a message to a channel." }
  ]
}
```

### 2. Set the scope on the PAL

```json Create PAL with one connector scoped down theme={null}
{
  "pal_name": "Support assistant",
  "layers": {
    "mcp": {
      "connectors": ["c8-58ea0f6420b2", "c0-8982a37dc5e0"],
      "connector_tools": {
        "c8-58ea0f6420b2": ["search_messages", "list_channels"]
      }
    }
  }
}
```

Or on an existing PAL:

```json Patch PAL to scope one connector down theme={null}
[
  {
    "op": "add",
    "path": "/layers/mcp/connector_tools",
    "value": { "c8-58ea0f6420b2": ["search_messages"] }
  }
]
```

| Field             | Type   | Description                                                                                                                                                   |
| ----------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `connector_tools` | object | Connector id to the tool names the PAL may call. Up to 200 names per connector, no duplicates, each 1-64 characters of letters, digits, `_`, `.`, `:` or `-`. |

### 3. Change it later

Patch a longer list to widen a scope. To go back to every tool the server exposes, remove the connector's entry:

```json Patch PAL back to every tool theme={null}
[
  { "op": "remove", "path": "/layers/mcp/connector_tools/c8-58ea0f6420b2" }
]
```

<Note>
  An empty array is rejected - `{"c8-58ea0f6420b2": []}` is not how you deny a connector. To stop a PAL using a connector, detach it.
</Note>

## Following a task from your client

Delegation is asynchronous, so the conversation does not block. Three events describe each task, and **every one is keyed by `task_id`** - up to five tasks can run at once, so the key is what tells you which task an event belongs to.

| Event                                                                            | When                                                                                                                                           |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| [`conversation.agent_start`](/sections/event-schemas/conversation-agent-start)   | The task began. Carries the task in the PAL's own words.                                                                                       |
| [`conversation.agent_update`](/sections/event-schemas/conversation-agent-update) | An update the background agent chose to give the user - a short title and a sentence. Only when `spoken_updates` or `visual_updates` is `all`. |
| [`conversation.agent_stop`](/sections/event-schemas/conversation-agent-stop)     | The outcome, with a status and the answer.                                                                                                     |

```json A task, start to finish theme={null}
{ "event_type": "conversation.agent_start",
  "properties": { "task_id": "call_abc123",
                  "task": "find the GitHub PR for PROD-4874 and say if it merged" } }

{ "event_type": "conversation.agent_update",
  "properties": { "task_id": "call_abc123", "type": "update",
                  "title": "PR found",
                  "text": "Found the PROD-4874 pull request, checking whether it merged." } }

{ "event_type": "conversation.agent_stop",
  "properties": { "task_id": "call_abc123", "status": "done",
                  "summary": "The PR is open, not merged yet." } }
```

Four things worth building around:

* **Key the lifecycle on `agent_start` / `agent_stop`, not on `conversation.tool_call`.** The delegating tool call is also broadcast, but for a task that fails to dispatch the stop is emitted *before* it. A stop for a `task_id` you have not seen is terminal - close it out rather than waiting for a start that is not coming.
* **Every `agent_start` gets exactly one `agent_stop`**, including tasks that never ran and tasks cancelled when the call ends. There is no state where a task is silently still running.
* **`agent_update` is what the agent chose to tell the user, never the answer.** The background agent decides when something is worth an update and writes it as a card - a title and a sentence - a few times per task at most. It is the same update the PAL speaks and shows on Magic Canvas, so a client that renders it matches the call. Tool calls are not reported. The result itself is only ever in the stop event - do not assemble it from updates. No updates are emitted unless `spoken_updates` or `visual_updates` is `all`.
* **You do not have to relay the result.** The PAL speaks it when the task finishes. Use `summary` for your UI, not for driving speech.

## What the user hears

The PAL acknowledges the request when it delegates ("let me look into that") and keeps the conversation going. When the task finishes, it speaks the outcome - it does not wait for the user to say something first, and if the user has moved on it answers them first and mentions the result after. Interim steps are not spoken unless the user asks what is happening, or the PAL is set to [narrate its tasks](#choosing-what-the-pal-says-about-a-task).

A task that fails or times out is spoken too, so the user is never left waiting on something the PAL promised and dropped. A PAL set to `none` says nothing in any of these cases. A task still running when the call ends is not - there is no one left to hear it.

## What the user sees

A PAL with [Magic Canvas](/sections/conversational-video-interface/magic-canvas/overview) on a video call also shows the task as a text card next to the video. `layers.mcp.visual_updates` decides how much, with the same three values as `spoken_updates` and independently of it. It defaults to `none`: a card takes over the canvas, so showing one is opt-in. Under `outcome` one card appears with the result when the task finishes; under `all` each update the agent gives replaces the card as the task runs, and the result replaces it at the end. The result card clears itself after 45 seconds. No card is shown in audio-only or chat conversations, in external meetings, or for a PAL without the Magic Canvas skill - the setting is inert there.

## Choosing what the PAL says about a task

Two settings pick which moments of a background task reach the user, with the same three values and independent of each other: `layers.mcp.spoken_updates` for what the PAL says, `layers.mcp.visual_updates` for what it shows on Magic Canvas. Speech defaults to `outcome`; the card defaults to `none`, so it is opt-in.

| Value     | Spoken (`spoken_updates`) / shown (`visual_updates`)                                                                                   |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `none`    | Nothing is volunteered. The outcome still reaches the PAL's context, so it can answer if the user asks how the task went.              |
| `outcome` | The outcome, once, when the task finishes. Nothing before it.                                                                          |
| `all`     | The updates the background agent chooses to give while it works - a short title and a sentence, a few per task - and then the outcome. |

```json Create PAL that narrates its tasks theme={null}
{
  "pal_name": "Engineering assistant",
  "layers": {
    "mcp": {
      "connectors": ["c8-58ea0f6420b2"],
      "spoken_updates": "all",
      "visual_updates": "all"
    }
  }
}
```

```json Show every update, say only the result theme={null}
[
  { "op": "add", "path": "/layers/mcp/spoken_updates", "value": "outcome" },
  { "op": "add", "path": "/layers/mcp/visual_updates", "value": "all" }
]
```

```json Patch PAL to keep tasks quiet theme={null}
[
  { "op": "add", "path": "/layers/mcp/spoken_updates", "value": "none" }
]
```

With either setting on `all`, the background agent decides what is worth saying: it is told to give an update when it learns something the user would want to hear before the answer, or when the task is taking a while, and never to narrate its steps. Problems and retries are not updates; they belong in the outcome. A card shows each update at once; speech takes them one at a time, only when the PAL has room to, so a task that moves quickly voices fewer updates than it shows - [`conversation.agent_update`](/sections/event-schemas/conversation-agent-update) still carries all of them. A common pairing is `visual_updates: all` with `spoken_updates: outcome`: the user can watch the task without the PAL talking over the conversation. Keep `outcome` for short tasks, where an update is longer than the wait; use `all` for long ones, where silence reads as a stall. `none` suits a PAL whose tasks are side effects the user does not need read back, such as logging a note after a call.

This field used to be a boolean; writing one is now rejected.

## Limits

|                                   |             |
| --------------------------------- | ----------- |
| Connectors per PAL                | 10          |
| Tools selectable per connector    | 200         |
| Concurrent tasks per conversation | 5           |
| Task timeout                      | 120 seconds |

A task requested while five are already running is rejected rather than queued, and the PAL tells the user to ask again in a moment.
