Voice Agent MCP Tools: Timeouts That Fit a Turn - Zian AI

Voice Agent MCP Tools: Timeouts That Fit a Turn

Quick answer: Attach one MCP server on streamable HTTP, set transport_type explicitly rather than trusting the URL, then lower the timeouts. LiveKit Agents ships timeout: float = 5, sse_read_timeout: float = 60 * 5 and client_session_timeout_seconds: float = 5. No 5-second ceiling fits inside one phone turn. Allow-list the tools, then add a health check.

Scope, so you know what this page is and is not. This is about your voice agent as an MCP client: the transport it speaks, the timeout values it ships with, and the two failure modes that have been filed against it. It applies whether you wire the connection yourself in LiveKit Agents or point a platform such as Vapi at a Zapier, Make or Composio server, and the worked failures below involve Google’s own hosted Gmail MCP server and the Gemini Realtime plugin. When a tool is allowed to fire relative to an unfinished caller turn is a separate layer, and we have already answered it in our guide to gating AI agent tool calls on an unfinished turn. Why a tool turn produces caller-audible silence, the three-clock test and the perception thresholds behind the numbers below are all in our page on why an AI voice agent goes silent on tool calls, and none of that is restated here. What follows starts where those two stop: the protocol underneath the tool.

Why MCP’s defaults are wrong for a phone call

Model Context Protocol was designed for a chat client. In a chat window a five-second wait is invisible: the spinner turns, the user looks at another tab, the answer lands. Every default in the stack is calibrated to that. On a phone call the same five seconds is a hang-up.

Here is the whole page in one sentence. The 1.5-second dead-line rule: past roughly 1.5 seconds of unexplained silence, a caller stops waiting and starts checking whether the line is dead — they say “hello?”, they talk over the reply when it finally lands, or they hang up. That is our operating rule rather than a vendor benchmark; the peer-reviewed turn-taking work it is anchored to is set out on the dead-air page linked above.

Now read the shipping defaults against it. From livekit-agents/livekit/agents/llm/mcp.py on the main branch, read 22 September 2026 (repository HEAD 844e1a37; that file’s most recent commit is a527e385, 20 August 2026), the constructor is:

def __init__(
    self,
    url: str,
    transport_type: Literal["sse", "streamable_http"] | None = None,
    allowed_tools: list[str] | None = None,
    headers: dict[str, Any] | None = None,
    timeout: float = 5,
    sse_read_timeout: float = 60 * 5,
    client_session_timeout_seconds: float = 5,
    *,
    tool_result_resolver: MCPToolResultResolver | None = None,
) -> None:

Three numbers, and only one of them is the one people think it is. timeout is the HTTP connection timeout. sse_read_timeout is 300 seconds. client_session_timeout_seconds is the one that governs how long a single tool call may take: the base class assigns it to self._read_timeout and passes it into the MCP ClientSession as read_timeout_seconds=timedelta(seconds=self._read_timeout). That is your in-turn deadline, and it defaults to 5 — roughly 3.3 times the point at which a caller has already decided the line is dead.

The two-bucket rule: set the timeout against the turn, not the default

Call this the two-bucket rule. Every MCP tool your agent holds is either in-turn or off-turn, and the bucket decides the timeout:

  • In-turn tools must return inside the caller’s patience. Their timeout is a backstop set just above the budget, so a hung server is caught within one filler sentence rather than within one abandoned call.
  • Off-turn tools have been deliberately taken off the critical path. The agent says so out loud, the tool reports progress, and the timeout is generous. LiveKit’s own MCPToolset docstring example uses MCPServerHTTP(url="...", client_session_timeout_seconds=120).

The quotable part: the 5-second default is both too long and too short. It is too long for anything inside a turn and too short for anything you meant to run in the background. It is the middle bucket, and there is no middle bucket. A tool left on the default has not been assigned to either.

Working out the in-turn budget: the arithmetic end to end

Substitute your own measured numbers; these inputs are illustrative placeholders, not measurements of any platform.

  • Dead-line budget: 1,500 ms (our rule, above).
  • Minus the part of the turn that runs before MCP is reached — end-of-turn detection, ASR finalisation, LLM time-to-first-token and writing the tool arguments: 700 ms.
  • Minus the part that runs after the result lands — TTS time-to-first-byte and one-way telephony transport: 250 ms.
  • Left for the entire MCP round trip: 550 ms.

Then set the timeout at roughly twice that, not at the budget itself: the budget governs when the agent must speak, the timeout governs when it gives up. At 550 ms of headroom, a client_session_timeout_seconds of 1 to 2 is a deadline. Five is a formality.

One term most teams never add: on Vapi the connection is not amortised across the call. Vapi’s MCP documentation states that “Each time the model invokes a specific MCP tool, Vapi creates a new connection to the MCP server and sends the request with the X-Call-Id / X-Chat-Id header to identify the call or chat”, and that “multiple MCP sessions are created per call or chat”. Connection setup therefore sits inside that 550 ms on every single invocation, not once at the start.

The threshold table: what to set, by tool class

Tool classes and settings are our decision rule. The only vendor-published figures in this table are the LiveKit defaults and the 120 from LiveKit’s own docstring example, both cited in the source table at the end. The “what the agent does while it waits” column carries over the mitigations from the dead-air page’s band table rather than re-deriving them; the reasoning behind each one is there, not here.

Tool class Budget inside one turn What the agent does while it waits What to set
Read-only lookup on a system you control (order status, account balance, opening hours) Under 550 ms p95 — it should disappear into an ordinary turn gap Nothing. Do not add filler in front of a 300 ms tool; the filler is longer than the silence client_session_timeout_seconds=1, timeout=2. In-turn bucket
Write on a system you control (book, amend, cancel, log) Under 1.2 s p95 One short pre-synthesised sentence, issued before the request goes out, interruptible client_session_timeout_seconds=2, timeout=2. In-turn bucket, and it belongs on the commit list in the turn-gating guide above
Third-party API behind the MCP server (CRM, calendar, payment gateway, Zapier or Make scenario) Treat as unbounded until you have a p95 across a week of real calls Preamble, then surface the server’s own progress: LiveKit forwards MCP progress notifications as ctx.update() when MCPToolOptions(report_progress=True) is set on that tool client_session_timeout_seconds=3 if it stays in-turn. If it cannot make 3, move it to the off-turn bucket
Long-running (document generation, multi-step workflow, anything waiting on a human) Does not belong inside a turn at any budget The agent commits to a follow-up and keeps talking. ToolFlag.CANCELLABLE and report_progress=True exist for exactly this shape client_session_timeout_seconds=120, off the critical path. Off-turn bucket
Unmeasured — you have not timed it on a real call Unknown, which in practice means unbounded Nothing yet. You have a measurement problem first Leave it out of the allow-list until it has a p95

Which transport do I get: does my URL end in /mcp or /sse?

The Model Context Protocol specification’s current revision is 2026-07-28, and its Streamable HTTP page states that Streamable HTTP “was introduced in protocol version 2025-03-26 as a replacement for the HTTP+SSE transport from protocol version 2024-11-05”. LiveKit says the same thing in the MCPServerHTTP docstring: “Note: SSE transport is being deprecated in favor of streamable HTTP transport.” Vapi says it a third time, listing the protocol metadata options as "shttp" (default) and "sse" marked deprecated.

So pick streamable HTTP. The trap is how you get it. LiveKit auto-detects from the URL path, and the detection is narrower than the docstring suggests. The docstring lists three cases — URLs ending in sse use SSE, URLs ending in mcp use streamable HTTP, other URLs default to SSE for backward compatibility — but the function that decides is a single test:

def _should_use_streamable_http(self, url: str) -> bool:
    """... Returns True for streamable HTTP if URL ends with 'mcp',
    False for SSE if URL ends with 'sse' or for backward compatibility."""
    parsed_url = urlparse(url)
    path_lower = parsed_url.path.lower().rstrip("/")
    return path_lower.endswith("/mcp")

Read it literally: the only path that gets streamable HTTP is one ending /mcp. A server published at /api/mcp/v1, /mcp-server or a bare host with no path silently gets SSE, the transport everybody is moving off. The fix is one argument: pass transport_type="streamable_http" explicitly and stop depending on a string match against somebody else’s URL scheme.

Two more current-revision details worth knowing before you design around a long-lived connection. Revision 2026-07-28 removed the GET stream endpoint and removed protocol-level sessions. And for the moment the caller hangs up mid-tool-call, the specification is explicit: “Closing the SSE response stream MUST be treated by the server as cancellation of that request.” Check that your hangup path actually closes it, or your MCP server keeps working on a call that no longer exists.

Allow-list the tools, and know what the allow-list does not do

MCPServerHTTP takes allowed_tools: list[str] | None. In the source read on 22 September 2026, the filter runs in list_tools() after super().list_tools() has already fetched and built every tool the server offers, then keeps the ones whose name is in the set. That is the right behaviour for the problem it solves — a smaller tool list means a shorter schema, fewer tokens in front of the model and less time spent writing arguments — but it is a client-side filter on what the model sees, not a restriction on what the server sends back.

Why that distinction is not academic: in the second documented failure below, the tools were already allow-listed down to five, and four of the five took the session down anyway. An allow-list narrows your exposure. It does not validate what arrives.

If you are on a platform rather than a framework, the shape is different again, and Vapi documents it plainly: “The MCP tool itself is not meant to be invoked by the model. It serves as a configuration mechanism for Vapi to fetch and inject the specific tool definitions from the MCP server into the model’s context.” The fetch happens at the start: “When a call or chat starts, Vapi connects to your configured MCP server using Streamable HTTP protocol by default, fetches the list of available tools, and dynamically adds them to your assistant’s available tools”. Vapi’s MCP tool configuration exposes server.url, server.headers, metadata and protocol, and the page states that “The tools available through MCP are determined by your MCP server provider.” The narrowing, in other words, happens on the server you point at.

Vapi also publishes the size problem, which is the other half of allow-listing: “Context Overflow Warning: Some MCP server tool calls (eg: GitHub API queries) may return large amounts of data. This can exceed model context limits, affecting assistant performance and potentially causing failures, especially with models like GPT-4o.” A tool that returns a 40-row result set is not a latency problem, it is a context problem, and it will not show up until a caller asks the one question that triggers it.

My agent hangs when it calls an MCP tool: two documented failures

Both of these are public engineering records on an open tracker, filed by named reporters with runnable repro code. They are readable precisely because the framework is open. A closed platform can carry the same class of defect with nothing you can read, so treat the absence of a tracker as absence of evidence rather than evidence of absence. Every cell below was read from the issue body on 22 September 2026.

Reported failure Mechanism, as reported What the caller hears Status at 22 Sep 2026
The MCP server process dies mid-call and nothing notices
livekit/agents issue 6291, opened 2 July 2026 by GitHub user Jonnyton. Reported on livekit-agents 1.6.4, also reproduced against main at e2b2d09
The connection task _run_client holds the session open and parks on await self._closing_ev.wait(). The report traces that transport stream death is not linked to that wait, so the task stays parked — the repro prints task done=False a full second after the server process exits. Because the task never unwinds, the finally: block that sets self._client = None does not run, so the guarded ToolError that would have said “internal service is unavailable” is never reached, and calls fall through to self._client.call_tool(...) and raise a bare anyio.ClosedResourceError with an empty message. initialized is self._client is not None, so it keeps returning True The agent keeps talking and can no longer do anything. It is not silent, which is what makes it hard to spot: it answers normally and every lookup quietly fails with an error string the model cannot read back to the caller Open. A community pull request, 6298, proposes detecting server death and failing loudly; it was still open and unmerged when we read it
One tool’s JSON Schema takes the whole session down
livekit/agents issue 7349, opened 19 September 2026 by GitHub user caiolea0. Reported on livekit-agents 1.8.2, livekit-plugins-google 1.8.2, google-genai 2.18.1, model gemini-3.1-flash-live-preview
The tools came from Google’s own hosted Gmail MCP server at https://gmailmcp.googleapis.com/mcp/v1, and 4 of the 5 allow-listed tools failed the same way: create_draft on readOnly: true, get_thread, get_message and search_threads on x-google-enum-descriptions. Both keywords are legitimate — readOnly is in the JSON Schema specification and the other is a Google extension — so the report notes any MCP server can emit them. The Gemini Realtime plugin then fails a pydantic validation while building its connect config, and the report identifies a second defect: the config is built outside the retry try, so the exception escapes before any connection attempt Dead air from hello. The worker joins the room, RoomIO links to the participant, the mic publishes, and the agent never speaks or hears. The reporter recorded six start attempts, all dead, with the only clue in the worker log Closed as completed, 21 September 2026, fixed by pull request 7353 (merged). Note the release gap: the newest livekit-agents on PyPI was 1.8.2, published 15 September 2026, so on our read date the fix was on main and not yet in a published release

The transferable lesson is the pair, not either one. The first says a working MCP connection can stop being one without the framework changing its mind about it. The second says a tool you never call can stop the agent before the first word. One is a runtime failure, one is a startup failure, and no amount of timeout tuning addresses either.

The health check the framework does not have

Issue 6291’s reporter puts the diagnosis in one line: initialized is just self._client is not None. We read the same property in main on 22 September 2026 and it is exactly that. It reports whether a ClientSession object is still assigned to a local attribute. It does not send anything to the server, so it cannot tell you the server is there. It is a variable check wearing the name of a health check.

What a real one checks, and you have to write it yourself:

  1. A real round trip on a timer. Call list_tools() against the live server every 10 to 15 seconds while a call is up. It issues an actual MCP tools/list request through the same ClientSession, so it fails the same way a tool call would — which is the entire point. Set invalidate_cache() first, or the cached list answers instead of the server.
  2. A deadline shorter than your in-turn budget. If the probe cannot return inside the same window you gave a read-only tool, the server is not healthy for this call even if it eventually answers.
  3. The tool names, not just a 200. Compare the returned names against your allow-list. A server that restarted with a changed schema is a different failure from a server that died, and the second documented failure above is what a schema change can cost you.
  4. A state the agent can act on. This is the part that is not plumbing. A failed probe should change what the agent is allowed to promise — drop to a script that does not offer lookups, and hand to a human rather than keep offering a capability that is gone. In issue 6291 the agent kept talking; that is the behaviour to design against.
  5. A reconnect path. The primitives are in the file: MCPToolset.setup(reload=True) invalidates the cache and re-fetches, and MCPServer.aclose() then initialize() rebuilds the connection. Nothing in the source re-runs initialize() on its own, so the schedule is yours.

The five steps, and how you know you are finished

Half a day on a stack you already run. No new dependency.

  1. Day one, first hour: attach exactly one server. One MCPServerHTTP, with transport_type="streamable_http" passed explicitly rather than inferred from the URL. Step one is done when the agent lists that server’s tools at startup.
  2. Sort every tool into the two buckets. In-turn or off-turn, no tool in both, none missing. This is a file, not a discussion.
  3. Set the timeouts from the table. Every tool leaves the 5-second default. While you are there, decide whether each write tool should also be gated on turn confirmation — that decision is a different axis and is covered in the turn-gating guide linked at the top.
  4. Allow-list, then validate. Pass allowed_tools with the smallest working set, then print the JSON Schema your server actually returns for each one and check it against what your model provider accepts. Issue 7349 is what skipping this costs.
  5. Add the health check. Probe, deadline, name comparison, agent state, reconnect.

The finish state, and it is a test rather than a feeling: with a call in progress, kill the MCP server process. Within one probe interval the agent should say something true to the caller, the call should stay up, and when the server comes back the tools should work again without ending the call. If the agent carries on as though nothing happened, you have reproduced issue 6291 on your own stack and you are not finished.

Should I use MCP or a plain function tool for my phone agent?

Honest answer: for one or two endpoints you own, a direct function tool is fewer moving parts and one less process that can die mid-call. MCP earns its place when the tools are not yours — a Zapier, Make or Composio server, a vendor’s hosted server, or an internal server maintained by a team that is not you — because then the alternative is hand-writing and re-writing schemas for integrations somebody else keeps changing.

What that convenience costs, stated plainly so you can do the arithmetic yourself: a second process in the call path, a transport choice with a deprecation on it, three timeout values that ship wrong for telephony, a tool list you do not control the schema of, and a health check nobody has written for you. That is a real engineering surface, and it is the same surface whether you are using a framework or a platform — on Vapi the MCP tool is, in its own documentation’s words, a configuration mechanism, so the server you point it at is still yours to keep alive.

Where Zian fits

Zian AI is an autonomous AI sales-agents platform running phone, SMS, email and WhatsApp agents, with API and CRM integrations including HubSpot, Salesforce, HighLevel and Zapier — which means tool turns, and therefore everything on this page, sit on the design surface rather than at the edge of it. The general shape of that wiring, and when a native integration beats a generic connector, is in our guide to AI agent CRM integration patterns. For teams whose lookups hit systems that cannot leave their own network, Zian supports private model deployment on customer infrastructure, which moves the tool round trip inside the same boundary as the data.

Zian does not publish a tool-turn latency figure, and we would rather you ran the kill-the-server test above against any platform on your shortlist than read a number with no method attached. Zian AI has been running outbound acquisition since 2017 and is in partnership-application beta — a limited number of teams are taken on by application. More of these questions are collected in the Zian AI FAQ hub, and if you want to talk about a deployment, Apply For Partnership.

Where every figure on this page comes from

Figure Who published it Link Date read
timeout: float = 5, sse_read_timeout: float = 60 * 5, client_session_timeout_seconds: float = 5 on MCPServerHTTP.__init__; allowed_tools filtered inside list_tools(); initialized returns self._client is not None; _should_use_streamable_http tests only for a path ending /mcp; the client_session_timeout_seconds=120 docstring example; “SSE transport is being deprecated in favor of streamable HTTP transport”; MCPToolOptions(report_progress=True) forwarding progress to ctx.update(), ToolFlag.CANCELLABLE, MCPToolset.setup(reload=True), invalidate_cache() and MCPServer.aclose() LiveKit (livekit/agents, main, livekit-agents/livekit/agents/llm/mcp.py; HEAD 844e1a37, file’s last commit a527e385 of 20 Aug 2026) raw.githubusercontent.com source file 22 Sep 2026
Server-death mechanism, task done=False, anyio.ClosedResourceError, versions 1.6.4 and main e2b2d09, issue status open GitHub user Jonnyton, on the livekit/agents issue tracker (issue 6291, opened 2 Jul 2026) livekit/agents issue 6291 22 Sep 2026
Pull request 6298 open and unmerged GitHub (livekit/agents pull request 6298) livekit/agents pull request 6298 22 Sep 2026
Gmail MCP server URL, 4 of 5 allow-listed tools failing, the named tools and keywords, versions 1.8.2 / 2.18.1 / gemini-3.1-flash-live-preview, six dead start attempts, closed as completed 21 Sep 2026 and fixed by pull request 7353 GitHub user caiolea0, on the livekit/agents issue tracker (issue 7349, opened 19 Sep 2026), with the closing comment by maintainer longcw livekit/agents issue 7349 and pull request 7353 22 Sep 2026
Newest published livekit-agents release 1.8.2, uploaded 15 Sep 2026 Python Package Index (read via its JSON API; the project HTML page renders only a JavaScript notice from a plain fetch) PyPI JSON API, livekit-agents 22 Sep 2026
MCP tool is a configuration mechanism; streamable HTTP by default at call start; a new connection per tool invocation and multiple MCP sessions per call; protocol options shttp (default) and sse (deprecated); tools determined by your MCP server provider; the Context Overflow Warning Vapi (product documentation) docs.vapi.ai MCP tools 22 Sep 2026
Current protocol revision 2026-07-28 Model Context Protocol (versioning page) modelcontextprotocol.io versioning 22 Sep 2026
Streamable HTTP introduced in 2025-03-26 as a replacement for HTTP+SSE from 2024-11-05; revision 2026-07-28 removed the GET stream endpoint and protocol-level sessions; “Closing the SSE response stream MUST be treated by the server as cancellation of that request” Model Context Protocol (specification, revision 2026-07-28) MCP Streamable HTTP specification 22 Sep 2026
The 1.5-second dead-line rule, the two-bucket rule, the 1,500 / 700 / 250 / 550 ms worked budget, and every tool class and setting in the threshold table Zian AI — first-party decision rules and illustrative inputs published on this page, not measurements of any platform This page 22 Sep 2026

FAQ

Why does my voice agent hang when it calls an MCP tool?

Most often because the tool is still inside its timeout while the caller has already given up. LiveKit Agents ships client_session_timeout_seconds at 5, which is the per-request deadline passed into the MCP ClientSession, and five seconds of unexplained silence on a phone call reads as a dropped line. The second cause is the server going away without the client noticing, which is the mechanism reported in livekit/agents issue 6291, opened on 2 July 2026 and still open on 22 September 2026.

What should I set the MCP timeout to for a phone agent?

Sort each tool into one of two buckets first. In-turn tools get a timeout just above the budget you have left after endpointing, the model and speech synthesis have taken their share, which is often 1 to 3 seconds rather than 5. Off-turn tools get a generous timeout and a progress mechanism, and LiveKit’s own docstring example uses 120 seconds for that shape. What you should not do is leave the 5-second default, because it is too long for the first bucket and too short for the second.

Should I use SSE or streamable HTTP for a voice agent MCP server?

Streamable HTTP. The Model Context Protocol specification states that it was introduced in protocol version 2025-03-26 as a replacement for the HTTP+SSE transport from 2024-11-05, LiveKit’s own source comment says SSE is being deprecated in favour of it, and Vapi marks its sse protocol option deprecated while defaulting to shttp. In LiveKit the auto-detection only selects streamable HTTP for a URL path ending in /mcp, so pass transport_type explicitly instead of relying on the URL.

Why does my agent get dead air from the very first second after adding an MCP server?

Check the tool schemas before you check anything else. In livekit/agents issue 7349, a reporter found that tools from Google’s own hosted Gmail MCP server carried the JSON Schema keyword readOnly and the Google extension x-google-enum-descriptions, and 4 of the 5 tools they had allow-listed made the Gemini Realtime plugin fail to build its connect config, so the session never started. That issue was closed as completed on 21 September 2026, fixed by pull request 7353, which had not yet appeared in a published PyPI release when we read it on 22 September 2026.

Is the framework’s initialized flag enough to tell me my MCP server is alive?

No. Reading the source on 22 September 2026, initialized returns self._client is not None, which reports whether a ClientSession object is still assigned to a local attribute. It sends nothing to the server, so it cannot observe a server that has gone away. A usable health check calls list_tools on a timer with a deadline shorter than your in-turn budget, compares the returned names against your allow-list, and changes what the agent is allowed to promise when it fails.

Can an MCP tool return too much data for a voice agent?

Yes, and the vendors that document it treat it as a real failure rather than a performance note. Vapi publishes a Context Overflow Warning on its MCP tools page stating that some MCP server tool calls, giving GitHub API queries as the example, may return large amounts of data that can exceed model context limits, affecting assistant performance and potentially causing failures. On a voice call the practical rule is to make the MCP server return the one field the agent will speak, not the record it came from.

What happens to a running MCP tool call when the caller hangs up?

On streamable HTTP under the current specification revision, closing the response stream is itself the cancellation signal: the specification states that closing the SSE response stream MUST be treated by the server as cancellation of that request, and that the server should stop work as soon as practical. That only helps if your own hangup path actually closes the stream, so test it by ending a call mid-tool-call and watching whether the MCP server keeps working on a conversation that no longer exists.

Related Blogs

Related from Zian AI