118118_TOOLBOX_FEATURES = os .getenv (
119119 "FOUNDRY_AGENT_TOOLBOX_FEATURES" , "Toolboxes=V1Preview" )
120120
121+ # Platform-injected per-request call identifier (container protocol v2.0.0).
122+ # Extracted from the inbound invocation request and forwarded verbatim on every
123+ # egress call to the Foundry toolbox MCP proxy so the platform can correlate the
124+ # downstream tool calls with the originating invocation. Never parse this value.
125+ _FOUNDRY_CALL_ID_HEADER = "x-agent-foundry-call-id"
126+
121127_credential = DefaultAzureCredential ()
122128_project_client = AIProjectClient (endpoint = _endpoint , credential = _credential )
123129_responses_client = _project_client .get_openai_client ().responses
@@ -142,13 +148,16 @@ def __init__(self, endpoint: str, token_provider):
142148 self ._session_id : str | None = None
143149 self ._req_id = 0
144150
145- def _headers (self ) -> dict :
151+ def _headers (self , call_id : str | None = None ) -> dict :
146152 h = {
147153 "Content-Type" : "application/json" ,
148154 "Authorization" : f"Bearer { self ._get_token ()} " ,
149155 }
150156 if _TOOLBOX_FEATURES :
151157 h ["Foundry-Features" ] = _TOOLBOX_FEATURES
158+ # Forward the per-request call ID extracted from the inbound invocation.
159+ if call_id :
160+ h [_FOUNDRY_CALL_ID_HEADER ] = call_id
152161 if self ._session_id :
153162 h ["mcp-session-id" ] = self ._session_id
154163 return h
@@ -157,12 +166,12 @@ def _next_id(self) -> int:
157166 self ._req_id += 1
158167 return self ._req_id
159168
160- def initialize (self ) -> str :
169+ def initialize (self , call_id : str | None = None ) -> str :
161170 """Send MCP initialize + initialized notification."""
162171 with httpx .Client (timeout = 60 ) as client :
163172 resp = client .post (
164173 self .endpoint ,
165- headers = self ._headers (),
174+ headers = self ._headers (call_id ),
166175 json = {
167176 "jsonrpc" : "2.0" ,
168177 "id" : self ._next_id (),
@@ -181,29 +190,29 @@ def initialize(self) -> str:
181190 # Send initialized notification
182191 client .post (
183192 self .endpoint ,
184- headers = self ._headers (),
193+ headers = self ._headers (call_id ),
185194 json = {"jsonrpc" : "2.0" , "method" : "notifications/initialized" },
186195 )
187196 return data .get ("result" , {}).get ("serverInfo" , {}).get ("name" , "unknown" )
188197
189- def list_tools (self ) -> list [dict ]:
198+ def list_tools (self , call_id : str | None = None ) -> list [dict ]:
190199 """Call tools/list and return tool definitions."""
191200 with httpx .Client (timeout = 60 ) as client :
192201 resp = client .post (
193202 self .endpoint ,
194- headers = self ._headers (),
203+ headers = self ._headers (call_id ),
195204 json = {"jsonrpc" : "2.0" , "id" : self ._next_id (
196205 ), "method" : "tools/list" , "params" : {}},
197206 )
198207 resp .raise_for_status ()
199208 return resp .json ().get ("result" , {}).get ("tools" , [])
200209
201- def call_tool (self , name : str , arguments : dict ) -> str :
210+ def call_tool (self , name : str , arguments : dict , call_id : str | None = None ) -> str :
202211 """Call a tool and return the text result."""
203212 with httpx .Client (timeout = 120 ) as client :
204213 resp = client .post (
205214 self .endpoint ,
206- headers = self ._headers (),
215+ headers = self ._headers (call_id ),
207216 json = {
208217 "jsonrpc" : "2.0" ,
209218 "id" : self ._next_id (),
@@ -235,13 +244,13 @@ def call_tool(self, name: str, arguments: dict) -> str:
235244_tools_initialized = False
236245
237246
238- def _ensure_tools ():
247+ def _ensure_tools (call_id : str | None = None ):
239248 global _mcp_client , _tool_definitions , _tools_initialized
240249 if _tools_initialized :
241250 return
242251 _mcp_client = _McpToolboxClient (TOOLBOX_ENDPOINT , _token_provider )
243- server_name = _mcp_client .initialize ()
244- mcp_tools = _mcp_client .list_tools ()
252+ server_name = _mcp_client .initialize (call_id )
253+ mcp_tools = _mcp_client .list_tools (call_id )
245254 logger .info ("Toolbox '%s' connected: %d tool(s) discovered" ,
246255 server_name , len (mcp_tools ))
247256 for t in mcp_tools :
@@ -265,9 +274,9 @@ def _ensure_tools():
265274_MAX_TOOL_ROUNDS = 10
266275
267276
268- def _call_model (input_items : list [dict ]) -> object :
277+ def _call_model (input_items : list [dict ], call_id : str | None = None ) -> object :
269278 """Call the model with tool definitions and return the response."""
270- _ensure_tools ()
279+ _ensure_tools (call_id )
271280 return _responses_client .create (
272281 model = _model ,
273282 instructions = _SYSTEM_PROMPT ,
@@ -277,15 +286,15 @@ def _call_model(input_items: list[dict]) -> object:
277286 )
278287
279288
280- def _run_agent_loop (input_items : list [dict ]) -> str :
289+ def _run_agent_loop (input_items : list [dict ], call_id : str | None = None ) -> str :
281290 """Execute the agentic tool-calling loop synchronously.
282291
283292 Calls the model, checks for tool calls, executes them, feeds results
284293 back, and repeats until the model produces a text response or we hit
285294 the max rounds limit.
286295 """
287296 for _ in range (_MAX_TOOL_ROUNDS ):
288- response = _call_model (input_items )
297+ response = _call_model (input_items , call_id )
289298
290299 # Check if the model wants to call tools
291300 tool_calls = [
@@ -302,7 +311,7 @@ def _run_agent_loop(input_items: list[dict]) -> str:
302311 try :
303312 arguments = json .loads (tc .arguments ) if isinstance (
304313 tc .arguments , str ) else tc .arguments
305- result_text = _mcp_client .call_tool (tc .name , arguments )
314+ result_text = _mcp_client .call_tool (tc .name , arguments , call_id )
306315 logger .info ("Tool '%s' returned %d chars" ,
307316 tc .name , len (result_text ))
308317 except Exception as e :
@@ -326,10 +335,10 @@ def _run_agent_loop(input_items: list[dict]) -> str:
326335 return "(Reached maximum tool call rounds)"
327336
328337
329- async def _stream_agent_reply (input_items : list [dict ]):
338+ async def _stream_agent_reply (input_items : list [dict ], call_id : str | None = None ):
330339 """Run the agent loop in a thread and yield the result as SSE events."""
331340 loop = asyncio .get_running_loop ()
332- result = await loop .run_in_executor (None , _run_agent_loop , input_items )
341+ result = await loop .run_in_executor (None , _run_agent_loop , input_items , call_id )
333342 yield f"data: { json .dumps ({'type' : 'token' , 'content' : result })} \n \n "
334343
335344
@@ -374,8 +383,12 @@ async def handle_invoke(request: Request):
374383 session_id = request .state .session_id
375384 invocation_id = request .state .invocation_id
376385
377- logger .info ("Processing invocation %s (session %s)" ,
378- invocation_id , session_id )
386+ # Extract the per-request call ID (container protocol v2.0.0) directly from
387+ # the inbound request headers so it can be forwarded on toolbox egress calls.
388+ call_id = request .headers .get (_FOUNDRY_CALL_ID_HEADER )
389+
390+ logger .info ("Processing invocation %s (session %s, call_id %s)" ,
391+ invocation_id , session_id , call_id )
379392
380393 # Retrieve or create conversation history for this session.
381394 history = _sessions .setdefault (session_id , [])
@@ -385,7 +398,7 @@ async def handle_invoke(request: Request):
385398 async def event_generator ():
386399 full_reply = ""
387400 try :
388- async for delta in _stream_agent_reply (input_items ):
401+ async for delta in _stream_agent_reply (input_items , call_id ):
389402 # Parse the SSE data to extract the content for history
390403 try :
391404 event_data = json .loads (delta .split (
0 commit comments