From 5216a5d77e49525e4eb1706be88b8156ed893db1 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Fri, 24 Apr 2026 13:02:44 +0200 Subject: [PATCH 1/2] Harden MCP tools: batch exception handling, gesture null params, capabilities message - Wrap BatchAsync in try/catch for HttpRequestException, TaskCanceledException, and JsonException so batch tool returns a friendly error instead of crashing - Omit null distance/durationMs from gesture JsonObject to prevent agent-side deserialization failure on non-nullable DTO properties - Improve capabilities error message to distinguish unreachable agents from older agents that lack the capabilities endpoint Follow-up to #115. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DevFlow/Mcp/Tools/AgentTools.cs | 2 +- .../DevFlow/Mcp/Tools/BatchTools.cs | 13 ++++++++++--- .../AgentClient.cs | 17 ++++++++++------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/AgentTools.cs b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/AgentTools.cs index 08db73cb3..1528ed534 100644 --- a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/AgentTools.cs +++ b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/AgentTools.cs @@ -96,7 +96,7 @@ public static async Task Capabilities( var agent = await session.GetAgentClientAsync(agentPort); var capabilities = await agent.GetCapabilitiesAsync(); if (capabilities.ValueKind == System.Text.Json.JsonValueKind.Undefined) - return "Agent not responding. Is the app running?"; + return "Unable to retrieve capabilities. The agent may not support this feature or may be running an older version."; return CliJson.SerializeUntyped(capabilities, indented: false); } diff --git a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/BatchTools.cs b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/BatchTools.cs index a69f2a68e..df029ce8e 100644 --- a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/BatchTools.cs +++ b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/BatchTools.cs @@ -66,8 +66,15 @@ public static async Task Batch( actions.Add(obj); } - var agent = await session.GetAgentClientAsync(agentPort); - var result = await agent.BatchAsync(actions, continueOnError); - return CliJson.SerializeUntyped(result, indented: false); + try + { + var agent = await session.GetAgentClientAsync(agentPort); + var result = await agent.BatchAsync(actions, continueOnError); + return CliJson.SerializeUntyped(result, indented: false); + } + catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or System.Text.Json.JsonException) + { + return $"Batch request failed: {ex.Message}. Is the app running?"; + } } } diff --git a/src/DevFlow/Microsoft.Maui.DevFlow.Driver/AgentClient.cs b/src/DevFlow/Microsoft.Maui.DevFlow.Driver/AgentClient.cs index 5467b0735..f47935baf 100644 --- a/src/DevFlow/Microsoft.Maui.DevFlow.Driver/AgentClient.cs +++ b/src/DevFlow/Microsoft.Maui.DevFlow.Driver/AgentClient.cs @@ -169,14 +169,17 @@ public async Task KeyAsync(string key, string? elementId = null, string? t public async Task GestureAsync(string type, string? elementId = null, string? direction = null, double? distance = null, int? durationMs = null) { - return await PostActionAsync($"{UiApi}/actions/gesture", new JsonObject + var payload = new JsonObject { - ["elementId"] = elementId, - ["type"] = type, - ["direction"] = direction, - ["distance"] = distance, - ["durationMs"] = durationMs - }); + ["type"] = type + }; + + if (elementId is not null) payload["elementId"] = elementId; + if (direction is not null) payload["direction"] = direction; + if (distance.HasValue) payload["distance"] = distance.Value; + if (durationMs.HasValue) payload["durationMs"] = durationMs.Value; + + return await PostActionAsync($"{UiApi}/actions/gesture", payload); } public async Task BatchAsync(IEnumerable actions, bool continueOnError = false) From f338e5b4b11cda81b96d5c6ce9a5790edcea4a1b Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Fri, 24 Apr 2026 13:21:15 +0200 Subject: [PATCH 2/2] Address review feedback: ScrollAsync nulls, OperationCanceledException, error messages - Fix ScrollAsync to omit null itemIndex/groupIndex/scrollToPosition/elementId from JsonObject (same pattern as the GestureAsync fix) - Widen batch catch filter from TaskCanceledException to OperationCanceledException (the base class) - Improve batch and capabilities error messages to mention both connectivity and version/feature support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DevFlow/Mcp/Tools/AgentTools.cs | 2 +- .../DevFlow/Mcp/Tools/BatchTools.cs | 4 ++-- .../AgentClient.cs | 18 +++++++++++------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/AgentTools.cs b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/AgentTools.cs index 1528ed534..c1c78a362 100644 --- a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/AgentTools.cs +++ b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/AgentTools.cs @@ -96,7 +96,7 @@ public static async Task Capabilities( var agent = await session.GetAgentClientAsync(agentPort); var capabilities = await agent.GetCapabilitiesAsync(); if (capabilities.ValueKind == System.Text.Json.JsonValueKind.Undefined) - return "Unable to retrieve capabilities. The agent may not support this feature or may be running an older version."; + return "Unable to retrieve capabilities. The agent may not be running, or may not support this feature (older version)."; return CliJson.SerializeUntyped(capabilities, indented: false); } diff --git a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/BatchTools.cs b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/BatchTools.cs index df029ce8e..904f0d7c7 100644 --- a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/BatchTools.cs +++ b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/BatchTools.cs @@ -72,9 +72,9 @@ public static async Task Batch( var result = await agent.BatchAsync(actions, continueOnError); return CliJson.SerializeUntyped(result, indented: false); } - catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException or System.Text.Json.JsonException) + catch (Exception ex) when (ex is HttpRequestException or OperationCanceledException or System.Text.Json.JsonException) { - return $"Batch request failed: {ex.Message}. Is the app running?"; + return $"Batch request failed: {ex.Message}. Verify the app is running and the agent supports the batch endpoint."; } } } diff --git a/src/DevFlow/Microsoft.Maui.DevFlow.Driver/AgentClient.cs b/src/DevFlow/Microsoft.Maui.DevFlow.Driver/AgentClient.cs index f47935baf..d65299e18 100644 --- a/src/DevFlow/Microsoft.Maui.DevFlow.Driver/AgentClient.cs +++ b/src/DevFlow/Microsoft.Maui.DevFlow.Driver/AgentClient.cs @@ -207,16 +207,20 @@ public async Task ScrollAsync(string? elementId = null, double deltaX = 0, { var url = $"{UiApi}/actions/scroll"; if (window != null) url += $"?window={window}"; - return await PostActionAsync(url, new JsonObject + + var payload = new JsonObject { - ["elementId"] = elementId, ["deltaX"] = deltaX, ["deltaY"] = deltaY, - ["animated"] = animated, - ["itemIndex"] = itemIndex, - ["groupIndex"] = groupIndex, - ["scrollToPosition"] = scrollToPosition - }); + ["animated"] = animated + }; + + if (elementId is not null) payload["elementId"] = elementId; + if (itemIndex.HasValue) payload["itemIndex"] = itemIndex.Value; + if (groupIndex.HasValue) payload["groupIndex"] = groupIndex.Value; + if (scrollToPosition is not null) payload["scrollToPosition"] = scrollToPosition; + + return await PostActionAsync(url, payload); } ///