1515using System . Threading . Tasks ;
1616using Microsoft . Extensions . AI ;
1717
18+ #pragma warning disable MEAI001 // MCP-related types are currently marked as [Experimental]
19+
1820namespace Azure . AI . Agents . Persistent
1921{
2022 /// <summary>Represents an <see cref="IChatClient"/> for an Azure.AI.Agents.Persistent <see cref="PersistentAgentsClient"/>.</summary>
@@ -74,7 +76,7 @@ public virtual async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAs
7476 Argument . AssertNotNull ( messages , nameof ( messages ) ) ;
7577
7678 // Extract necessary state from messages and options.
77- ( ThreadAndRunOptions runOptions , List < FunctionResultContent > ? toolResults ) =
79+ ( ThreadAndRunOptions runOptions , List < FunctionResultContent > ? toolResults , List < McpServerToolApprovalResponseContent > ? approvalResults ) =
7880 await CreateRunOptionsAsync ( messages , options , cancellationToken ) . ConfigureAwait ( false ) ;
7981
8082 // Get the thread ID.
@@ -100,15 +102,15 @@ public virtual async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAs
100102
101103 // Submit the request.
102104 IAsyncEnumerable < StreamingUpdate > updates ;
103- if ( toolResults is not null &&
105+ if ( ( toolResults is not null || approvalResults is not null ) &&
104106 threadRun is not null &&
105- ConvertFunctionResultsToToolOutput ( toolResults , out List < ToolOutput > ? toolOutputs ) is { } toolRunId &&
107+ ConvertFunctionResultsToToolOutput ( toolResults , approvalResults , out List < ToolOutput > toolOutputs , out List < ToolApproval > toolApprovals ) is { } toolRunId &&
106108 toolRunId == threadRun . Id )
107109 {
108110 // There's an active run and we have tool results to submit, so submit the results and continue streaming.
109111 // This is going to ignore any additional messages in the run options, as we are only submitting tool outputs,
110112 // but there doesn't appear to be a way to submit additional messages, and having such additional messages is rare.
111- updates = _client ! . Runs . SubmitToolOutputsToStreamAsync ( threadRun , toolOutputs , cancellationToken ) ;
113+ updates = _client ! . Runs . SubmitToolOutputsToStreamAsync ( threadRun , toolOutputs , toolApprovals , cancellationToken ) ;
112114 }
113115 else
114116 {
@@ -191,13 +193,23 @@ threadRun is not null &&
191193 } ) ) ;
192194 }
193195
194- if ( ru is RequiredActionUpdate rau && rau . ToolCallId is string toolCallId && rau . FunctionName is string functionName )
196+ switch ( ru )
195197 {
196- ruUpdate . Contents . Add (
197- new FunctionCallContent (
198+ case RequiredActionUpdate rau when rau . ToolCallId is string toolCallId && rau . FunctionName is string functionName :
199+ ruUpdate . Contents . Add ( new FunctionCallContent (
198200 JsonSerializer . Serialize ( [ ru . Value . Id , toolCallId ] , AgentsChatClientJsonContext . Default . StringArray ) ,
199201 functionName ,
200202 JsonSerializer . Deserialize ( rau . FunctionArguments , AgentsChatClientJsonContext . Default . IDictionaryStringObject ) ! ) ) ;
203+ break ;
204+
205+ case SubmitToolApprovalUpdate stau :
206+ ruUpdate . Contents . Add ( new McpServerToolApprovalRequestContent (
207+ JsonSerializer . Serialize ( [ stau . Value . Id , stau . ToolCallId ] , AgentsChatClientJsonContext . Default . StringArray ) ,
208+ new McpServerToolCallContent ( stau . ToolCallId , stau . Name , stau . ServerLabel )
209+ {
210+ Arguments = JsonSerializer . Deserialize ( stau . Arguments , AgentsChatClientJsonContext . Default . IReadOnlyDictionaryStringObject ) ! ,
211+ } ) ) ;
212+ break ;
201213 }
202214
203215 yield return ruUpdate ;
@@ -274,7 +286,7 @@ public void Dispose() { }
274286 /// Creates the <see cref="ThreadAndRunOptions"/> to use for the request and extracts any function result contents
275287 /// that need to be submitted as tool results.
276288 /// </summary>
277- private async ValueTask < ( ThreadAndRunOptions RunOptions , List < FunctionResultContent > ? ToolResults ) > CreateRunOptionsAsync (
289+ private async ValueTask < ( ThreadAndRunOptions RunOptions , List < FunctionResultContent > ? ToolResults , List < McpServerToolApprovalResponseContent > ? ApprovalResults ) > CreateRunOptionsAsync (
278290 IEnumerable < ChatMessage > messages , ChatOptions ? options , CancellationToken cancellationToken )
279291 {
280292 // Create the options instance to populate, either a fresh or using one the caller provides.
@@ -324,7 +336,11 @@ public void Dispose() { }
324336 {
325337 switch ( tool )
326338 {
327- case AIFunction aiFunction :
339+ case ToolDefinitionAITool rawTool :
340+ toolDefinitions . Add ( rawTool . Tool ) ;
341+ break ;
342+
343+ case AIFunctionDeclaration aiFunction :
328344 toolDefinitions . Add ( new FunctionToolDefinition (
329345 aiFunction . Name ,
330346 aiFunction . Description ,
@@ -372,6 +388,44 @@ public void Dispose() { }
372388 case HostedWebSearchTool webSearch when webSearch . AdditionalProperties ? . TryGetValue ( "connectionId" , out object ? connectionId ) is true :
373389 toolDefinitions . Add ( new BingGroundingToolDefinition ( new BingGroundingSearchToolParameters ( [ new BingGroundingSearchConfiguration ( connectionId ! . ToString ( ) ) ] ) ) ) ;
374390 break ;
391+
392+ case HostedMcpServerTool mcpTool :
393+ MCPToolDefinition mcp = new ( mcpTool . ServerName , mcpTool . ServerAddress ) ;
394+
395+ if ( mcpTool . AllowedTools is { Count : > 0 } )
396+ {
397+ foreach ( string toolName in mcpTool . AllowedTools )
398+ {
399+ mcp . AllowedTools . Add ( toolName ) ;
400+ }
401+ }
402+
403+ MCPToolResource mcpResource = ! string . IsNullOrEmpty ( mcpTool . AuthorizationToken ) ?
404+ new ( mcpTool . ServerName , new Dictionary < string , string > ( ) { [ "Authorization" ] = $ "Bearer { mcpTool . AuthorizationToken } " } ) :
405+ new ( mcpTool . ServerName ) ;
406+
407+ switch ( mcpTool . ApprovalMode )
408+ {
409+ case HostedMcpServerToolAlwaysRequireApprovalMode :
410+ mcpResource . RequireApproval = new MCPApproval ( "always" ) ;
411+ break ;
412+
413+ case HostedMcpServerToolNeverRequireApprovalMode :
414+ mcpResource . RequireApproval = new MCPApproval ( "never" ) ;
415+ break ;
416+
417+ case HostedMcpServerToolRequireSpecificApprovalMode requireSpecific :
418+ mcpResource . RequireApproval = new MCPApproval ( new MCPApprovalPerTool ( )
419+ {
420+ Always = requireSpecific . AlwaysRequireApprovalToolNames is { Count : > 0 } alwaysRequireNames ? new ( alwaysRequireNames ) : null ,
421+ Never = requireSpecific . NeverRequireApprovalToolNames is { Count : > 0 } neverRequireNames ? new ( neverRequireNames ) : null ,
422+ } ) ;
423+ break ;
424+ }
425+
426+ ( toolResources ??= new ( ) ) . Mcp . Add ( mcpResource ) ;
427+ toolDefinitions . Add ( mcp ) ;
428+ break ;
375429 }
376430 }
377431
@@ -453,6 +507,7 @@ public void Dispose() { }
453507 // and everything else as user messages.
454508 StringBuilder ? instructions = null ;
455509 List < FunctionResultContent > ? functionResults = null ;
510+ List < McpServerToolApprovalResponseContent > ? approvalResults = null ;
456511
457512 runOptions . ThreadOptions ??= new ( ) ;
458513
@@ -504,6 +559,10 @@ public void Dispose() { }
504559 ( functionResults ??= [ ] ) . Add ( result ) ;
505560 break ;
506561
562+ case McpServerToolApprovalResponseContent mcpApproval :
563+ ( approvalResults ??= [ ] ) . Add ( mcpApproval ) ;
564+ break ;
565+
507566 default :
508567 if ( content . RawRepresentation is MessageInputContentBlock rawContent )
509568 {
@@ -534,56 +593,104 @@ public void Dispose() { }
534593 runOptions . OverrideInstructions = instructions . ToString ( ) ;
535594 }
536595
537- return ( runOptions , functionResults ) ;
596+ return ( runOptions , functionResults , approvalResults ) ;
538597 }
539598
540599 /// <summary>Convert <see cref="FunctionResultContent"/> instances to <see cref="ToolOutput"/> instances.</summary>
541- /// <param name="toolResults">The tool results to process.</param>
600+ /// <param name="functionResults">The function results to process.</param>
601+ /// <param name="approvalResults">The MCP tool approval results to process.</param>
542602 /// <param name="toolOutputs">The generated list of tool outputs, if any could be created.</param>
603+ /// <param name="toolApprovals">The generated list of tool approvals, if any could be created.</param>
543604 /// <returns>The run ID associated with the corresponding function call requests.</returns>
544- private static string ? ConvertFunctionResultsToToolOutput ( List < FunctionResultContent > ? toolResults , out List < ToolOutput > ? toolOutputs )
605+ private static string ? ConvertFunctionResultsToToolOutput (
606+ List < FunctionResultContent > ? functionResults ,
607+ List < McpServerToolApprovalResponseContent > ? approvalResults ,
608+ out List < ToolOutput > toolOutputs ,
609+ out List < ToolApproval > toolApprovals )
545610 {
546611 string ? runId = null ;
547- toolOutputs = null ;
548- if ( toolResults ? . Count > 0 )
612+ toolOutputs = [ ] ;
613+ toolApprovals = [ ] ;
614+
615+ if ( functionResults ? . Count > 0 )
549616 {
550- foreach ( FunctionResultContent frc in toolResults )
617+ foreach ( FunctionResultContent frc in functionResults )
551618 {
552- // When creating the FunctionCallContext, we created it with a CallId == [runId, callId].
553- // We need to extract the run ID and ensure that the ToolOutput we send back to Azure
554- // is only the call ID.
555- string [ ] ? runAndCallIDs ;
556- try
557- {
558- runAndCallIDs = JsonSerializer . Deserialize ( frc . CallId , AgentsChatClientJsonContext . Default . StringArray ) ;
559- }
560- catch
619+ if ( TryParseRunAndCallIds ( frc . CallId , out string ? parsedRunId , out string ? callId ) &&
620+ ( runId is null || runId == parsedRunId ) )
561621 {
562- continue ;
622+ runId = parsedRunId ;
623+ toolOutputs . Add ( new ( callId , frc . Result ? . ToString ( ) ?? string . Empty ) ) ;
563624 }
625+ }
626+ }
564627
565- if ( runAndCallIDs is null ||
566- runAndCallIDs . Length != 2 ||
567- string . IsNullOrWhiteSpace ( runAndCallIDs [ 0 ] ) || // run ID
568- string . IsNullOrWhiteSpace ( runAndCallIDs [ 1 ] ) || // call ID
569- ( runId is not null && runId != runAndCallIDs [ 0 ] ) )
628+ if ( approvalResults ? . Count > 0 )
629+ {
630+ foreach ( McpServerToolApprovalResponseContent trc in approvalResults )
631+ {
632+ if ( TryParseRunAndCallIds ( trc . Id , out string ? parsedRunId , out string ? callId ) &&
633+ ( runId is null || runId == parsedRunId ) )
570634 {
571- continue ;
635+ runId = parsedRunId ;
636+ toolApprovals . Add ( new ( callId , trc . Approved ) ) ;
572637 }
573-
574- runId = runAndCallIDs [ 0 ] ;
575- ( toolOutputs ??= [ ] ) . Add ( new ( runAndCallIDs [ 1 ] , frc . Result ? . ToString ( ) ?? string . Empty ) ) ;
576638 }
577639 }
578640
579641 return runId ;
642+
643+ static bool TryParseRunAndCallIds ( string id , out string ? runId , out string ? callId )
644+ {
645+ // When creating the AIContent instances, we created it with a CallId == [runId, callId].
646+ // We need to extract the run ID and ensure that the ToolOutput we send back to Azure
647+ // is only the call ID.
648+ runId = null ;
649+ callId = null ;
650+
651+ string [ ] ? runAndCallIDs ;
652+ try
653+ {
654+ runAndCallIDs = JsonSerializer . Deserialize ( id , AgentsChatClientJsonContext . Default . StringArray ) ;
655+ }
656+ catch
657+ {
658+ return false ;
659+ }
660+
661+ if ( runAndCallIDs is null ||
662+ runAndCallIDs . Length != 2 ||
663+ string . IsNullOrWhiteSpace ( runAndCallIDs [ 0 ] ) || // run ID
664+ string . IsNullOrWhiteSpace ( runAndCallIDs [ 1 ] ) ) // call ID
665+ {
666+ return false ;
667+ }
668+
669+ runId = runAndCallIDs [ 0 ] ;
670+ callId = runAndCallIDs [ 1 ] ;
671+ return true ;
672+ }
673+ }
674+
675+ /// <summary>
676+ /// <see cref="AITool"/> type that allows for any <see cref="ToolDefinition"/> to be
677+ /// passed into the <see cref="IChatClient"/> via <see cref="ChatOptions.Tools"/>.
678+ /// </summary>
679+ internal sealed class ToolDefinitionAITool ( ToolDefinition tool ) : AITool
680+ {
681+ public override string Name => tool . GetType ( ) . Name ;
682+ public ToolDefinition Tool => tool ;
683+ public override object ? GetService ( Type serviceType , object ? serviceKey ) =>
684+ serviceKey is null && serviceType ? . IsInstanceOfType ( Tool ) is true ? Tool :
685+ base . GetService ( serviceType ! , serviceKey ) ;
580686 }
581687
582688 [ JsonSerializable ( typeof ( JsonElement ) ) ]
583689 [ JsonSerializable ( typeof ( JsonNode ) ) ]
584690 [ JsonSerializable ( typeof ( JsonObject ) ) ]
585691 [ JsonSerializable ( typeof ( string [ ] ) ) ]
586692 [ JsonSerializable ( typeof ( IDictionary < string , object > ) ) ]
693+ [ JsonSerializable ( typeof ( IReadOnlyDictionary < string , object > ) ) ]
587694 private sealed partial class AgentsChatClientJsonContext : JsonSerializerContext ;
588695 }
589696}
0 commit comments