Skip to main content
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 or a Python handler instead. On this page: The file that does it holds one block and nothing else:
tools/web_search.yaml
Then the file name goes in an agent’s list, the same way any tool file does:
agent.yaml
This is a worked example rather than a quote, with auth: added to show the field. The real one ships in 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

string
The server’s registered platform name. Omit to use this tool file’s name.
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.
string
Accepts sse or streamable_http. If omitted, use the runtime’s URL-based transport selection; a URL ending in /mcp selects streamable HTTP.
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.
list of strings
Unique, non-empty server tool names. Required on SLNG. If omitted on a code target, expose every tool the server offers.
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:
string
required
Accepts bearer or api_key. Required when auth is present; no scheme is inferred.
string
required
An UPPER_SNAKE environment variable name holding the token, never the token itself. There is no default.
string
An HTTP header name, legal only with type: api_key. Omitted means X-API-Key. Bearer authentication uses Authorization: Bearer.

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:
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: 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

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

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:
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:
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:
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:
And the tool source factory shared by startup validation and the agent that listed it:
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:
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:
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:
Fix: move the source to the agent’s tools: list, as in 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:
Fix: name the tools you want under mcp.tools. Read what the server offers with voiceai mcp list.

Where to go next

Prebuilt tools

The last way a tool can run: the ones the runtime already has.

Secrets

Where url_env and token_env values come from.