// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows.Declarative.Events;
using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
using Microsoft.Agents.AI.Workflows.Declarative.Kit;
using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
using Microsoft.Agents.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
///
/// Executor for the action.
/// This executor invokes MCP tools on remote servers and handles approval flows.
///
internal sealed class InvokeMcpToolExecutor(
InvokeMcpTool model,
IMcpToolHandler mcpToolHandler,
ResponseAgentProvider agentProvider,
WorkflowFormulaState state) :
DeclarativeActionExecutor(model, state)
{
///
/// Step identifiers for the MCP tool invocation workflow.
///
public static class Steps
{
///
/// Step for waiting for external input (approval or direct response).
///
public static string ExternalInput(string id) => $"{id}_{nameof(ExternalInput)}";
///
/// Step for resuming after receiving external input.
///
public static string Resume(string id) => $"{id}_{nameof(Resume)}";
}
///
/// Determines if the message indicates external input is required.
///
public static bool RequiresInput(object? message) =>
message is ExternalInputRequest || (message is PortableValue pv && pv.IsType(out ExternalInputRequest? _));
///
/// Determines if the message indicates no external input is required.
///
public static bool RequiresNothing(object? message) =>
message is ActionExecutorResult || (message is PortableValue pv && pv.IsType(out ActionExecutorResult? _));
///
protected override bool EmitResultEvent => false;
///
protected override bool IsDiscreteAction => false;
///
[SendsMessage(typeof(ExternalInputRequest))]
protected override async ValueTask ExecuteAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
{
string serverUrl = this.GetServerUrl();
string? serverLabel = this.GetServerLabel();
string toolName = this.GetToolName();
bool requireApproval = this.GetRequireApproval();
Dictionary? arguments = this.GetArguments();
Dictionary? headers = this.GetHeaders();
string? connectionName = this.GetConnectionName();
if (requireApproval)
{
// Create tool call content for approval request
McpServerToolCallContent toolCall = new(this.Id, toolName, serverLabel ?? serverUrl)
{
Arguments = arguments
};
if (headers != null)
{
toolCall.AdditionalProperties ??= [];
toolCall.AdditionalProperties.Add(headers);
}
ToolApprovalRequestContent approvalRequest = new(this.Id, toolCall);
ChatMessage requestMessage = new(ChatRole.Assistant, [approvalRequest]);
AgentResponse agentResponse = new([requestMessage]);
// Yield to the caller for approval
ExternalInputRequest inputRequest = new(agentResponse);
await context.SendMessageAsync(inputRequest, cancellationToken).ConfigureAwait(false);
return default;
}
// No approval required - invoke the tool directly
McpServerToolResultContent resultContent = await mcpToolHandler.InvokeToolAsync(
serverUrl,
serverLabel,
toolName,
arguments,
headers,
connectionName,
cancellationToken).ConfigureAwait(false);
await this.ProcessResultAsync(context, resultContent, cancellationToken).ConfigureAwait(false);
// Signal completion so the workflow routes via RequiresNothing
await context.SendResultMessageAsync(this.Id, result: null, cancellationToken).ConfigureAwait(false);
return default;
}
///
/// Captures the external input response and processes the MCP tool result.
///
/// The workflow context.
/// The external input response.
/// A cancellation token.
/// A representing the asynchronous operation.
public async ValueTask CaptureResponseAsync(
IWorkflowContext context,
ExternalInputResponse response,
CancellationToken cancellationToken)
{
ToolApprovalResponseContent? approvalResponse = response.Messages
.SelectMany(m => m.Contents)
.OfType()
.FirstOrDefault(r => r.RequestId == this.Id);
if (approvalResponse?.Approved != true)
{
// Tool call was rejected
await this.AssignErrorAsync(context, "MCP tool invocation was not approved by user.").ConfigureAwait(false);
return;
}
// Approved - now invoke the tool
string serverUrl = this.GetServerUrl();
string? serverLabel = this.GetServerLabel();
string toolName = this.GetToolName();
Dictionary? arguments = this.GetArguments();
Dictionary? headers = this.GetHeaders();
string? connectionName = this.GetConnectionName();
McpServerToolResultContent resultContent = await mcpToolHandler.InvokeToolAsync(
serverUrl,
serverLabel,
toolName,
arguments,
headers,
connectionName,
cancellationToken).ConfigureAwait(false);
await this.ProcessResultAsync(context, resultContent, cancellationToken).ConfigureAwait(false);
}
///
/// Completes the MCP tool invocation by raising the completion event.
///
public async ValueTask CompleteAsync(IWorkflowContext context, ActionExecutorResult message, CancellationToken cancellationToken)
{
await context.RaiseCompletionEventAsync(this.Model, cancellationToken).ConfigureAwait(false);
}
private async ValueTask ProcessResultAsync(IWorkflowContext context, McpServerToolResultContent resultContent, CancellationToken cancellationToken)
{
bool autoSend = this.GetAutoSendValue();
string? conversationId = this.GetConversationId();
await this.AssignResultAsync(context, resultContent).ConfigureAwait(false);
ChatMessage resultMessage = new(ChatRole.Tool, resultContent.Outputs);
// Store messages if output path is configured
if (this.Model.Output?.Messages is not null)
{
await this.AssignAsync(this.Model.Output.Messages?.Path, resultMessage.ToFormula(), context).ConfigureAwait(false);
}
// Auto-send the result if configured
if (autoSend)
{
AgentResponse resultResponse = new([resultMessage]);
await context.AddEventAsync(new AgentResponseEvent(this.Id, resultResponse), cancellationToken).ConfigureAwait(false);
}
// Add messages to conversation if conversationId is provided
if (conversationId is not null)
{
ChatMessage assistantMessage = new(ChatRole.Assistant, resultContent.Outputs);
await agentProvider.CreateMessageAsync(conversationId, assistantMessage, cancellationToken).ConfigureAwait(false);
}
}
private async ValueTask AssignResultAsync(IWorkflowContext context, McpServerToolResultContent toolResult)
{
if (this.Model.Output?.Result is null || toolResult.Outputs is null || toolResult.Outputs.Count == 0)
{
return;
}
List parsedResults = [];
foreach (AIContent resultContent in toolResult.Outputs)
{
object? resultValue = resultContent switch
{
TextContent text => text.Text,
DataContent data => data.Uri,
_ => resultContent.ToString(),
};
// Convert JsonElement to its raw JSON string for processing
if (resultValue is JsonElement jsonElement)
{
resultValue = jsonElement.GetRawText();
}
// Attempt to parse as JSON if it's a string (or was converted from JsonElement)
if (resultValue is string jsonString)
{
try
{
using JsonDocument jsonDocument = JsonDocument.Parse(jsonString);
// Handle different JSON value kinds
object? parsedValue = jsonDocument.RootElement.ValueKind switch
{
JsonValueKind.Object => jsonDocument.ParseRecord(VariableType.RecordType),
JsonValueKind.Array => jsonDocument.ParseList(jsonDocument.RootElement.GetListTypeFromJson()),
JsonValueKind.String => jsonDocument.RootElement.GetString(),
JsonValueKind.Number => jsonDocument.RootElement.TryGetInt64(out long l) ? l : jsonDocument.RootElement.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
_ => jsonString,
};
parsedResults.Add(parsedValue);
continue;
}
catch (JsonException)
{
// Not a valid JSON
}
}
parsedResults.Add(resultValue);
}
await this.AssignAsync(this.Model.Output.Result?.Path, parsedResults.ToFormula(), context).ConfigureAwait(false);
}
private async ValueTask AssignErrorAsync(IWorkflowContext context, string errorMessage)
{
// Store error in result if configured (as a simple string)
if (this.Model.Output?.Result is not null)
{
await this.AssignAsync(this.Model.Output.Result?.Path, $"Error: {errorMessage}".ToFormula(), context).ConfigureAwait(false);
}
}
private string GetServerUrl() =>
this.Evaluator.GetValue(
Throw.IfNull(
this.Model.ServerUrl,
$"{nameof(this.Model)}.{nameof(this.Model.ServerUrl)}")).Value;
private string? GetServerLabel()
{
if (this.Model.ServerLabel is null)
{
return null;
}
string value = this.Evaluator.GetValue(this.Model.ServerLabel).Value;
return value.Length == 0 ? null : value;
}
private string GetToolName() =>
this.Evaluator.GetValue(
Throw.IfNull(
this.Model.ToolName,
$"{nameof(this.Model)}.{nameof(this.Model.ToolName)}")).Value;
private string? GetConversationId()
{
if (this.Model.ConversationId is null)
{
return null;
}
string value = this.Evaluator.GetValue(this.Model.ConversationId).Value;
return value.Length == 0 ? null : value;
}
private bool GetRequireApproval()
{
if (this.Model.RequireApproval is null)
{
return false;
}
return this.Evaluator.GetValue(this.Model.RequireApproval).Value;
}
private bool GetAutoSendValue()
{
if (this.Model.Output?.AutoSend is null)
{
return true;
}
return this.Evaluator.GetValue(this.Model.Output.AutoSend).Value;
}
private string? GetConnectionName()
{
if (this.Model.Connection?.Name is null)
{
return null;
}
string value = this.Evaluator.GetValue(this.Model.Connection.Name).Value;
return value.Length == 0 ? null : value;
}
private Dictionary? GetArguments()
{
if (this.Model.Arguments is null)
{
return null;
}
Dictionary result = [];
foreach (KeyValuePair argument in this.Model.Arguments)
{
result[argument.Key] = this.Evaluator.GetValue(argument.Value).Value.ToObject();
}
return result;
}
private Dictionary? GetHeaders()
{
if (this.Model.Headers is null)
{
return null;
}
Dictionary result = [];
foreach (KeyValuePair header in this.Model.Headers)
{
string value = this.Evaluator.GetValue(header.Value).Value;
if (!string.IsNullOrEmpty(value))
{
result[header.Key] = value;
}
}
return result;
}
}