Question I'm implementing a LiveKit Agent that dyn...
# ask-ai
s
Question I'm implementing a LiveKit Agent that dynamically activates MCP servers during a conversation. The agent starts with a minimal toolset and includes a tool called
activate_mcp
. When this tool is called, it: Activates an MCP server (e.g.
google_contacts
) Retrieves its tools via
server.list_tools()
Rebuilds the agent with the new
mcp_servers
1. Calls
context.session.update_agent(updated_agent)
This works correctly in terms of activation, but the LLM cannot immediately call the newly available tools in the same turn. Example flow: User request → Agent calls
activate_mcp(service="google_contacts")
→ MCP server activates and exposes tools (e.g.
list_contacts
) → Agent says something like "Give me a moment while I check", but never calls the new tool. It seems like the tool schema snapshot used by the LLM was captured at the beginning of the turn, so newly injected tools are not visible until the next run. My questions: Does
context.session.update_agent()
update the tool registry immediately within the same run, or only for the next turn? Is there a recommended way to trigger a replan / rerun of the agent after dynamically adding tools? Is the recommended pattern instead to expose stable proxy tools that internally activate MCP servers if needed? 1. Are there examples in the LiveKit Agents repo of dynamic tool injection during a conversation? The goal is to allow the agent to chain: activate_mcp → new MCP tool (e.g. list_contacts) within a single user request.
t
Thanks for reaching out, Roman. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — this is a great, specific question. I’ll address each part and give practical patterns you can use to make your agent reliably chain: activate_mcp → use new tool. Short answer up front • Calling
context.session.update_agent(updated_agent)
does update the agent in session state, but the LLM’s view of available tools is captured for the current reasoning cycle and will not see newly injected tools until the next turn. • The reliable patterns are: (A) complete the activation turn and then trigger a new LLM pass where the new tools are visible, or (B) expose stable proxy/wrapper tools that activate MCP servers internally and call the remote tool for the same-turn behavior. What’s happening (why you see the behavior) • The LLM/planner must have a consistent tool schema during a decision cycle. Tool registration is effectively snapshotted at the start of the turn, so mid-turn additions are not available for that same pass. That’s why your agent activates the MCP server successfully but never calls the newly added tool during the same turn. Direct answers to your questions 1) Does
context.session.update_agent()
update the tool registry immediately within the same run, or only for the next turn? • It updates the session’s agent state, but the LLM will not see newly added tools in the current reasoning cycle. New tools are available starting on the next turn. 2) Is there a recommended way to trigger a replan / rerun of the agent after dynamically adding tools? • There isn’t a single magic API that forces the current LLM pass to replan using newly injected tools. Recommended approaches: a. Two-step flow (most straightforward): • The tool
activate_mcp
completes and returns a message indicating activation succeeded. • Immediately start a new LLM pass (for example, call
context.session.generate_reply()
or otherwise append a follow-up system/user instruction) telling the agent to use the newly available tool (e.g., “Now call
list_contacts
to fetch contacts”). The new pass will include the updated tool list. a. Orchestrated follow-up from your controller: • After the activation tool finishes, programmatically enqueue the follow-up prompt/call so the agent continues without waiting for the user to type again. Both ensure the LLM evaluates available tools after the session update. 3) Is the recommended pattern instead to expose stable proxy tools that internally activate MCP servers if needed? • Yes — this is a robust recommended pattern. Expose stable, long-lived function tools (e.g.,
search_google_contacts(query)
or
get_user_contacts()
) that: ◦ Check/activate the MCP server internally if it isn’t already active. ◦ Discover and call the correct MCP tool behind the scenes. ◦ Return results to the agent in the same turn. This hides dynamic loading from the LLM and keeps the tool schema stable during each reasoning cycle. 4) Are there examples in the LiveKit Agents repo of dynamic tool injection during a conversation? • There are examples showing MCP usage, dynamic construction of function tools, and returning new agent instances (handoffs). However, there is no canonical example that demonstrates activating an MCP server mid-tool-call and then immediately calling the newly injected remote tool within the same turn. The practical options in the repo lean toward either multi-turn flows or using pre-defined wrapper tools. Practical implementation patterns you can use right now • Pattern A — Two-step activate-then-run (simple, reliable) a. Tool
activate_mcp(service="google_contacts")
activates the server and calls
context.session.update_agent(updated_agent)
. b. The tool returns a message like: “Activated Google Contacts. I will fetch your contacts now.” c. Your orchestration code or the agent calls
context.session.generate_reply()
with instructions to call
list_contacts
(or you add a follow-up user message). The new turn sees
list_contacts
and calls it. • Pattern B — Stable wrapper tool (single-turn) a. Register a stable tool
fetch_contacts(query)
at agent startup. b. Implementation of `fetch_contacts`: • If
google_contacts
MCP isn’t active, activate it and fetch the remote tool handle via
server.list_tools()
. • Call the remote
list_contacts
tool (directly invoke the MCP server or the tool implementation) and return the result. a. The LLM calls
fetch_contacts
in the same turn; the wrapper handles all activation and delegation, so no replan is needed. Which to pick? • If you need immediate same-turn chaining, use wrapper/proxy tools (Pattern B). • If you prefer to keep MCP servers as explicit agent-level tools, use Pattern A (activate then trigger a new turn). Suggested code/flow sketch (wrapper tool idea) • Register
@function_tool
or equivalent named
fetch_google_contacts(query)
. • Inside it: ◦ If the server is not active, create/activate the MCP server. ◦ Use
server.list_tools()
to discover
list_contacts
. ◦ Call
list_contacts
on the MCP server programmatically and return results to the agent. Closing / Offer to help If you’d like, I can: • Help you draft a concrete wrapper tool implementation sketch in your language of choice (Python/Node). • Show an example flow that uses
context.session.generate_reply()
to trigger the second turn after activation. Which would be most helpful — a quick Python example of the wrapper tool or a small orchestrator snippet that runs the two-step activate-then-run flow? Sources: Tool definition and use | LiveKit Documentation | Agent session | LiveKit Documentation | Agents and handoffs | LiveKit Documentation | MCP Agent | LiveKit Documentation
s
can't i update de tool registry during the agent loop? I got to a point where I'm forcing a "generate reply" to update the tool registry snapshot so that the agent is able to "see" the new tools that it has activated.
t
Thanks — I hear your frustration. You’re right that forcing a
generate_reply
to refresh the tool snapshot is a clunky workaround. Here’s a clear explanation of why that’s necessary, plus the practical options you can use going forward. Why you can’t update the tool registry mid‑loop • The planner/LLM needs a consistent function/tool schema for each reasoning cycle. That schema is captured (snapshotted) at the start of the turn so the model can plan reliably. • Because of that snapshotting, adding tools via
context.session.update_agent()
during the same reasoning pass does update the session state, but the LLM’s active view of tools does not change until a new pass/turn begins. You therefore must start a new pass for the model to “see” the new tools. Why a forced
generate_reply
works • Forcing a new LLM pass (e.g.,
generate_reply()
or programmatically enqueuing a follow-up message) creates a fresh reasoning cycle in which the tool registry is reloaded — so newly injected tools become visible and callable in that new turn. Recommended, practical solutions • Wrap activation behind stable proxy tools (best for same-turn behavior) ◦ Register a persistent tool like
fetch_google_contacts(query)
up front. ◦ Have that tool internally activate the MCP server if needed, discover the remote tool via
server.list_tools()
, call it, and return results. ◦ The LLM calls the wrapper in one turn and the wrapper handles activation + delegation without changing the LLM’s tool schema mid-turn. • Use a controlled two-step flow (explicit replan) ◦ Keep
activate_mcp
as a separate tool. After it runs, immediately start a new LLM pass (programmatically) that instructs the agent to call the newly available tool. This is reliable and simple to implement. • Orchestrate an automated follow-up rather than waiting for the user ◦ After activation finishes, your orchestrator can append a follow-up user/system message or call
generate_reply()
so the agent continues seamlessly without manual user input. When to prefer each pattern • Need true single-turn chaining (activate → call new tool): use stable wrapper tools. • Want explicit lifecycle separation and simpler reasoning: use activate-then-new-turn flows. • Want best UX without exposing dynamic tool churn to the LLM: use wrappers as the primary interface. If you’d like, I can: • Sketch a short Python example of a wrapper tool that activates an MCP server and calls its
list_contacts
in the same function, or • Show a minimal orchestrator snippet that runs activate_mcp and then programmatically triggers the follow-up
generate_reply()
so the agent uses the new tools. Which would you prefer?