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

> ## Agent Instructions
> Unmute compiles to exactly three targets. Pipecat and LiveKit are code targets: compile writes a Python project you run. SLNG is a hosted target: compile writes a deployment body and SLNG runs the agent, so it has no `unmute dev`. Those three are the only values `provider` accepts in `targets.yaml`. Deepgram and ElevenLabs appear in these docs as model vendors, which is not the same thing as a target, and `slng` is both.
> The Go structs in `internal/spec` and `internal/ir` are the schema truth. Check a field against them, or run `unmute validate`, rather than against what you remember.

# MCP servers

> Point the agent at a remote MCP server and let it offer that server's tools.

An MCP server already describes its own tools. So instead of writing a tool, you
name the server, and the agent offers what the server exposes.

Reach for an MCP source when a whole catalogue of tools already exists behind
one server, maintained by someone else, so attaching them one by one as
separate tool files would just be repetition. For one tool of your own, write
a [webhook](/build/tools/webhook) or a [Python handler](/build/tools/python)
instead.

On this page:

* [The block](#the-block) - five keys, and auth
* [The file is the block, and nothing else](#the-file-is-the-block-and-nothing-else) - what an mcp file refuses
* [Choosing which tools to offer](#choosing-which-tools-to-offer) - a filter, not a contract
* [Where the source can be listed](#where-the-source-can-be-listed) - agent or task
* [What each target needs](#what-each-target-needs) - the SDK, and the dependency
* [Advanced](#advanced) - what the compile writes
* [Troubleshooting](#troubleshooting) - the refusals, and a server that is down

The file that does it holds one block and nothing else:

```yaml tools/web_search.yaml theme={null}
mcp:
  server: firecrawl-mcp-2
  url_env: FIRECRAWL_MCP_URL
  transport: streamable_http
  auth:
    type: bearer
    token_env: FIRECRAWL_API_KEY
  tools:
    - firecrawl_scrape
    - firecrawl_search
```

Then the file name goes in an agent's list, the same way any tool file does:

```yaml agent.yaml theme={null}
agents:
  researcher:
    instructions: instructions.md
    think: reasoning
    speak: voice
    tools:
      - web_search

tools:
  - web_search
```

This is a worked example rather than a quote, with `auth:` added to show the
field. The real one ships in
[`examples/hotel-concierge`](https://github.com/slng-ai/unmute/tree/main/examples/hotel-concierge):
its `tools/web_search.yaml` names the same server and the same two tools, and
carries no `url_env`, `transport` or `auth:`. The platform reads the server by
name from its own registration, so those fields matter only for a code target.
That package deploys to slng only, agent name `hotel-concierge`, alongside four
other tools, so there was nothing for `auth:` to reach.

## The block

<ParamField path="server" type="string">
  The server’s registered platform name. Omit to use this tool file’s name.
</ParamField>

<ParamField path="url_env" type="string">
  An UPPER\_SNAKE environment variable name holding the server URL. Required on LiveKit and
  Pipecat. SLNG uses its registered server and does not read this field.
</ParamField>

<ParamField path="transport" type="string">
  Accepts `sse` or `streamable_http`. If omitted, use the runtime’s URL-based transport
  selection; a URL ending in `/mcp` selects streamable HTTP.
</ParamField>

<ParamField path="auth" type="object">
  `type`, `token_env`, and optional `header`, as described below. Omit for no authored
  authentication. SLNG uses its registered credentials and does not read this block.
</ParamField>

<ParamField path="tools" type="list of strings">
  Unique, non-empty server tool names. Required on SLNG. If omitted on a code target,
  expose every tool the server offers.
</ParamField>

**A package that only targets slng needs none of `url_env`, `transport` or
`auth`.** SLNG already has the server registered, with its own connection
settings and credential; those three fields exist for LiveKit and Pipecat,
which dial the server themselves. Selecting a code target still requires
them, with their existing rules and refusals.

`server` exists because the two names live in different namespaces. A tool file
name is lowercase snake\_case, and a server's name on the platform is whatever
somebody typed in a dashboard: real ones carry dashes and spaces. Without
`server`, a server called `firecrawl-mcp-2` could not be named at all. Read the
names your organisation has with `voiceai mcp list`.

`transport` is optional because both platforms already have a rule for guessing
it: a URL whose path ends in `/mcp` is streamable HTTP, anything else is SSE.
Write it when you want the choice visible instead of inferred.

`auth` is the same shape webhook tools use, so there is nothing new to learn and
no new code in the generated project. Its own three keys:

<ParamField path="type" type="string" required>
  Accepts `bearer` or `api_key`. Required when `auth` is present; no scheme is inferred.
</ParamField>

<ParamField path="token_env" type="string" required>
  An UPPER\_SNAKE environment variable name holding the token, never the token itself.
  There is no default.
</ParamField>

<ParamField path="header" type="string">
  An HTTP header name, legal only with `type: api_key`. Omitted means `X-API-Key`. Bearer
  authentication uses `Authorization: Bearer`.
</ParamField>

## The file is the block, and nothing else

Seven fields that describe one tool to the model are illegal on an `mcp:` file.
Each is refused on its own, with the file, the line, and the reason:

```text theme={null}
tools/web_search.yaml:1: remove `description`: it is not legal on an `mcp:` tool, the
  server describes each of its tools
```

```text theme={null}
tools/web_search.yaml:1: remove `input`: it is not legal on an `mcp:` tool, the server
  owns each tool's parameters
```

The other five say the same thing about `output`, `inject`, `interruption`,
`effect`, and `announce`. In one place, so you can see the whole rule at once:

| Field          | Why not here                                                       |
| -------------- | ------------------------------------------------------------------ |
| `description`  | the server describes each of its tools                             |
| `input`        | the server owns each tool's parameters                             |
| `output`       | the server owns each tool's result                                 |
| `inject`       | an MCP call has the server's own shape, with nothing to merge into |
| `interruption` | MCP tools take the platform's default interruption policy          |
| `effect`       | MCP tools return data; ending the call is a `builtin:` tool        |
| `announce`     | the server owns each tool's speech                                 |

This is the whole point of the shape. One tool file describes one tool the model
can call. An `mcp:` file is not that: it is a **source** of tools, and how many
there are, what they take, and what they return is only known once the server is
running.

## Choosing which tools to offer

```yaml theme={null}
  tools:
    - firecrawl_search
```

A selection filter, not a contract. Firecrawl also exposes scraping and crawling
tools; naming one keeps the rest out of a conversation that has to stay fast.

On LiveKit and Pipecat, the list is **not checked against the server** during
validation, because its tools are fetched at run time. A name the server does
not expose is never offered, and the package still validates on both.

Leave `tools` out and the agent gets everything the server exposes.
An empty list entry or a name repeated twice is still an authoring error and is
refused before the server is contacted.

On SLNG, `unmute deploy` checks every selected name against the registered
server's discovery snapshot. A missing tool blocks deployment. A real deploy
can refresh an unusable snapshot once; a dry run never refreshes it.

`url_env`, `transport`, and `auth` reach no further than that same offline
compile on SLNG. The platform already has the server registered, under
`server`, with whatever credential it needs. Nothing in this block reaches it
at run time except the tool list. Those three fields matter for the code
targets only.

## Where the source can be listed

| Scope       | LiveKit | Pipecat |
| ----------- | ------- | ------- |
| on an agent | yes     | yes     |
| on a task   | yes     | no      |

The reason is the framework's: a Pipecat Flows node builds its advertised tool set
out of its own function schemas, and Pipecat's MCP client offers no per-tool
handler to put in one. So the source goes on the agent, where it is offered
whenever that agent is active.

Two files may name the same `url_env`. They are two independent sources, each with
its own selection and its own assignment, which is how one server can offer a
narrow set to one agent and a wider set to another.

## What each target needs

**LiveKit needs the Python SDK.** Its Node SDK has no MCP support at all, so the
gate is a refusal rather than a warning:

```text theme={null}
livekit: LiveKit MCP tools require sdk_language: python
```

This Unmute release supports exactly `livekit-agents` 1.8.1. A target that
names any other version fails the global version check rather than having its
version quietly changed:

```text theme={null}
livekit: livekit version "1.5.2" is outside the supported range (exactly 1.8.1)
```

**Pipecat needs nothing extra.** It emits one client per source, started with the
bot and closed during normal shutdown and startup rollback.

With Langfuse tracing enabled, Pipecat MCP calls emit finite spans named after the tool, with its arguments and, when completed, the result.
With Coval tracing enabled, the same calls emit `llm_tool_call` spans carrying `function.name`, `tool_call_id`, and `function.arguments`.
Pipecat refuses to start when an agent tool, task function, or MCP source on the same agent exposes the same name.

Either way the dependency the project declares picks up an `mcp` extra for
you, alongside whatever your other models already need. Compiling the worked
example above, with its other roles filled in, writes
`livekit-agents[cartesia,deepgram,mcp,openai]==1.8.1` and
`pipecat-ai[cartesia,deepgram,mcp,openai,runner,silero,webrtc]==1.10.0`.

## Advanced

### What the compile does with it

Every name the block holds becomes part of the generated project. Compiling a
package that names the worked example's tool and also targets livekit adds,
among the names `build/livekit/.env.example` collects from the rest of the
package:

```text theme={null}
FIRECRAWL_API_KEY=
FIRECRAWL_MCP_URL=
```

The same two names land in `build/livekit/agent.py`'s startup check, `REQUIRED_ENV`,
and in `build/livekit/compile-report.json`, which says where each one came from:

```json theme={null}
{
  "name": "FIRECRAWL_MCP_URL",
  "referenced_by": [
    "tools/web_search.yaml mcp.url_env"
  ]
}
```

And the tool source factory shared by startup validation and the agent that
listed it:

```python theme={null}
def _mcp_toolset(source: str) -> mcp.MCPToolset:
    if source == "web_search":
        return mcp.MCPToolset(
            id="web_search",
            mcp_server=mcp.MCPServerHTTP(
                url=os.environ["FIRECRAWL_MCP_URL"],
                transport_type="streamable_http",
                allowed_tools=["firecrawl_scrape", "firecrawl_search"],
                headers=_bearer("FIRECRAWL_API_KEY"),
                timeout=30,
                client_session_timeout_seconds=30,
            ),
        )
```

Before `AgentSession.start`, LiveKit creates temporary clients from this same
factory and checks all distinct sources concurrently. Runtime agents and tasks
then receive fresh clients; preflight clients are never reused.

Leave a field out of the block and the generated call leaves out its argument,
rather than passing a guess: no `transport` means no `transport_type`, no `tools`
means no `allowed_tools`, no `auth` means no `headers`.

## Troubleshooting

### The server is down

Both targets treat a listed MCP source as required:

* **LiveKit** connects and fetches the tools before `AgentSession.start`. A
  failure stops the session before its greeting. Closing every created preflight
  client is always attempted, and a close failure also stops startup. The agent
  or task receives a fresh client from the same factory.
* **Pipecat** raises on startup and the bot exits loudly.

Neither one pretends the tools are there.

**Fix:** bring the server back, or take the source off the agents that list it
so the rest of the agent can still start.

### `url_env` holds a URL instead of a name

`url_env` is a name, never an address. Writing the URL there is refused:

```text theme={null}
livekit: tool "web_search" url_env must be an UPPER_SNAKE environment variable name
```

**Fix:** put the UPPER\_SNAKE variable name here and the address in your
environment.

### `transport` has a value the client does not know

Any other value is refused with both legal ones:

```text theme={null}
livekit: tool "web_search" transport must be sse or streamable_http, not "websocket"
```

**Fix:** write `sse` or `streamable_http`, or leave the key out and let the SDK
infer it from the URL.

### Pipecat refuses an MCP source scoped to a task

The Pipecat refusal names the fix:

```text theme={null}
pipecat: the Pipecat driver cannot scope an MCP tool source to a task: list it on the
  agent instead
```

**Fix:** move the source to the agent's `tools:` list, as in
[Where the source can be listed](#where-the-source-can-be-listed).

### The slng target asks for an explicit tool list

**SLNG has no "whole server" attachment, so `tools:` is required there.**
LiveKit and Pipecat pass the server through and let it decide what it offers.
SLNG writes one reference per tool up front, and an offline compile cannot ask
the server what those are:

```text theme={null}
slng: slng target tool "web_search" exposes every tool on its MCP server, and SLNG
  attaches one reference per tool: list the tools you want under mcp.tools
```

**Fix:** name the tools you want under `mcp.tools`. Read what the server offers
with `voiceai mcp list`.

## Where to go next

<Columns cols={2}>
  <Card title="Prebuilt tools" icon="package" href="/build/tools/prebuilt">
    The last way a tool can run: the ones the runtime already has.
  </Card>

  <Card title="Secrets" icon="key" href="/reference/secrets">
    Where `url_env` and `token_env` values come from.
  </Card>
</Columns>
