Skip to content

Commit 0ebd192

Browse files
tendaurootCopilot
authored
Lazy-init toolbox MCP connections on first request (#46525)
Move toolbox MCP connect_toolboxes() from initialize() (startup) to _get_or_create_session() (first request) to fix race condition where tool discovery fails because platform-injected user context is not yet available at startup time. Uses double-checked locking with asyncio.Lock to prevent concurrent initialization from parallel requests. Co-authored-by: root <root@CPC-cearl-W9ZSG.localdomain> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fd299d0 commit 0ebd192

1 file changed

Lines changed: 27 additions & 6 deletions

File tree

  • sdk/agentserver/azure-ai-agentserver-githubcopilot/azure/ai/agentserver/githubcopilot

sdk/agentserver/azure-ai-agentserver-githubcopilot/azure/ai/agentserver/githubcopilot/_copilot_adapter.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,8 @@ def __init__(
679679

680680
# Track MCP bridges for cleanup
681681
self._toolbox_bridges: list = []
682+
self._toolboxes_connected = False
683+
self._toolbox_lock = asyncio.Lock()
682684

683685
# AGENTS.md persona injection — load the project's AGENTS.md and use it
684686
# as the system message so the agent fully embodies its persona
@@ -834,8 +836,25 @@ async def connect_toolboxes(self):
834836
except Exception:
835837
logger.warning("Failed to connect toolbox %r at %s", name, url, exc_info=True)
836838

839+
async def _ensure_toolboxes(self):
840+
"""Lazily connect to toolbox MCP servers on first request.
841+
842+
Deferred from ``initialize()`` to the first request so that user
843+
context (auth tokens injected by the platform) is available when
844+
the MCP ``initialize`` and ``tools/list`` calls are made. An
845+
``asyncio.Lock`` prevents concurrent initialization from parallel
846+
requests.
847+
"""
848+
if self._toolboxes_connected:
849+
return
850+
async with self._toolbox_lock:
851+
if self._toolboxes_connected:
852+
return
853+
await self.connect_toolboxes()
854+
self._toolboxes_connected = True
855+
837856
async def initialize(self):
838-
"""Discover deployments, configure the model, and connect toolboxes.
857+
"""Discover deployments and configure the model.
839858
840859
Call after construction and before ``run()``. Discovery always runs
841860
to validate the configured model against available deployments and to
@@ -844,11 +863,10 @@ async def initialize(self):
844863
model is matched against discovered deployments; if not found, the
845864
best available model is auto-selected.
846865
847-
Also connects to any discovered MCP toolbox servers and registers
848-
their tools as regular SDK custom tools.
866+
Toolbox MCP connections are deferred to the first request
867+
(via ``_ensure_toolboxes``) so that platform-injected user context
868+
is available.
849869
"""
850-
# Connect toolbox MCP servers (independent of model discovery)
851-
await self.connect_toolboxes()
852870

853871
resource_url = self._session_config.get("_foundry_resource_url")
854872
if not resource_url:
@@ -1047,7 +1065,10 @@ async def _load_conversation_history(self, conversation_id: str) -> Optional[str
10471065
return None
10481066

10491067
async def _get_or_create_session(self, conversation_id=None):
1050-
"""Override to add conversation history bootstrap on cold start."""
1068+
"""Override to add lazy toolbox init and conversation history bootstrap."""
1069+
# Lazy-connect toolboxes on first request (deferred from initialize)
1070+
await self._ensure_toolboxes()
1071+
10511072
if conversation_id and conversation_id not in self._sessions:
10521073
history = await self._load_conversation_history(conversation_id)
10531074
if history:

0 commit comments

Comments
 (0)