diff --git a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/McpServerHost.cs b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/McpServerHost.cs index 3f9b3ea1b..3e6ea87b1 100644 --- a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/McpServerHost.cs +++ b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/McpServerHost.cs @@ -36,7 +36,8 @@ public static async Task RunAsync() .WithTools() .WithTools() .WithTools() - .WithTools(); + .WithTools() + .WithTools(); await builder.Build().RunAsync(); } 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 6b64a7157..08db73cb3 100644 --- a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/AgentTools.cs +++ b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/AgentTools.cs @@ -88,6 +88,18 @@ public static async Task Wait( return $"Timeout after {timeout}s — no agent connected" + (app != null ? $" matching '{app}'" : "") + "."; } + [McpServerTool(Name = "maui_capabilities"), Description("Get the capabilities supported by the connected agent. Returns a JSON object describing available features (e.g., profiler, sensors, webview). Use this to check what the agent supports before calling other tools.")] + public static async Task Capabilities( + McpAgentSession session, + [Description("Agent HTTP port (optional if only one agent connected)")] int? agentPort = null) + { + 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 CliJson.SerializeUntyped(capabilities, indented: false); + } + [McpServerTool(Name = "maui_select_agent"), Description("Set the default agent for this MCP session. Subsequent tool calls will use this agent automatically without needing agentPort.")] public static string SelectAgent( McpAgentSession session, diff --git a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/BatchTools.cs b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/BatchTools.cs new file mode 100644 index 000000000..a69f2a68e --- /dev/null +++ b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/BatchTools.cs @@ -0,0 +1,73 @@ +using System.ComponentModel; +using System.Text.Json; +using System.Text.Json.Nodes; +using ModelContextProtocol.Server; +using Microsoft.Maui.Cli.DevFlow.Mcp; + +namespace Microsoft.Maui.Cli.DevFlow.Mcp.Tools; + +[McpServerToolType] +public sealed class BatchTools +{ + [McpServerTool(Name = "maui_batch"), Description(""" + Execute multiple UI actions in a single request. Actions run sequentially and are not transactional. + Earlier actions are applied even if a later action fails. + The 'actionsJson' parameter must be a JSON array of action objects. + Each action object must have an "action" (or "type") field specifying the operation. + + Supported actions and their fields: + - {"action":"tap", "elementId":""} + - {"action":"fill", "elementId":"", "text":""} + - {"action":"clear", "elementId":""} + - {"action":"key", "key":"enter", "elementId":""} + - {"action":"focus", "elementId":""} + - {"action":"scroll", "elementId":"", "deltaX":0, "deltaY":200} + - {"action":"gesture", "type":"swipe", "elementId":"", "direction":"up"} + - {"action":"navigate", "route":"//page"} + - {"action":"back"} + + Note: The backend accepts both "action" and "type" fields. For gesture actions, + use "action":"gesture" with a separate "type" field for the gesture kind. + + Example: [{"action":"fill","elementId":"entry1","text":"hello"},{"action":"tap","elementId":"btn1"}] + """)] + public static async Task Batch( + McpAgentSession session, + [Description("JSON array of action objects. Each must have an 'action' or 'type' field (see tool description for schema)")] string actionsJson, + [Description("If true, continue executing remaining actions after a failure (default: false)")] bool continueOnError = false, + [Description("Agent HTTP port (optional if only one agent connected)")] int? agentPort = null) + { + JsonArray parsed; + try + { + var node = JsonNode.Parse(actionsJson); + if (node is not JsonArray array) + return "Invalid input: 'actionsJson' must be a JSON array, not " + (node?.GetValueKind().ToString() ?? "null") + "."; + + parsed = array; + } + catch (JsonException ex) + { + return $"Invalid JSON in 'actionsJson': {ex.Message}"; + } + + if (parsed.Count == 0) + return "Empty actions array — nothing to execute."; + + var actions = new List(); + for (int i = 0; i < parsed.Count; i++) + { + if (parsed[i] is not JsonObject obj) + return $"Invalid action at index {i}: expected a JSON object, got {parsed[i]?.GetValueKind().ToString() ?? "null"}."; + + if (obj["action"] == null && obj["type"] == null) + return $"Invalid action at index {i}: must have an 'action' or 'type' field (e.g., 'tap', 'fill', 'navigate')."; + + actions.Add(obj); + } + + var agent = await session.GetAgentClientAsync(agentPort); + var result = await agent.BatchAsync(actions, continueOnError); + return CliJson.SerializeUntyped(result, indented: false); + } +} diff --git a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/InteractionTools.cs b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/InteractionTools.cs index 1bc84d842..fbc411634 100644 --- a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/InteractionTools.cs +++ b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/InteractionTools.cs @@ -47,6 +47,61 @@ public static async Task Clear( : $"Failed to clear element '{elementId}'."; } + [McpServerTool(Name = "maui_key"), Description("Send a key press to an element. Supported keys for Entry/Editor/SearchBar: 'enter' (submit or newline), 'backspace' (delete last character). Use 'text' parameter to type characters. For reliable behavior, provide an element ID; omitting it may have no effect depending on the agent/platform implementation.")] + public static async Task Key( + McpAgentSession session, + [Description("Key to press: 'enter', 'return', 'backspace', 'delete'")] string key, + [Description("Target element ID. Optional, but omitting it may result in no action; provide an element ID for reliable behavior.")] string? elementId = null, + [Description("Text to type character by character into the element")] string? text = null, + [Description("Agent HTTP port (optional if only one agent connected)")] int? agentPort = null) + { + var agent = await session.GetAgentClientAsync(agentPort); + var success = await agent.KeyAsync(key, elementId, text); + return success + ? elementId is not null + ? $"Sent key '{key}' to element '{elementId}'." + : $"Sent key '{key}' without a target element; it may have had no effect." + : $"Failed to send key '{key}'. The target element may not support keyboard input, or no target element was provided."; + } + + [McpServerTool(Name = "maui_gesture"), Description("Perform a touch gesture on the app. Supported gesture types: 'swipe' (requires direction), 'tap', 'longpress', and 'long-press'. Use maui_tap for simple taps — this tool is for advanced gestures like swiping.")] + public static async Task Gesture( + McpAgentSession session, + [Description("Gesture type: 'swipe', 'tap', 'longpress', or 'long-press'")] string type, + [Description("Target element ID (optional)")] string? elementId = null, + [Description("Swipe direction: 'up', 'down', 'left', or 'right' (required for swipe)")] string? direction = null, + [Description("Swipe distance in pixels (optional, uses default if omitted)")] double? distance = null, + [Description("Gesture duration in milliseconds (optional)")] int? durationMs = null, + [Description("Agent HTTP port (optional if only one agent connected)")] int? agentPort = null) + { + var normalizedType = (type ?? string.Empty).Trim().ToLowerInvariant(); + if (normalizedType == "long-press") + normalizedType = "longpress"; + + var validTypes = new[] { "swipe", "tap", "longpress" }; + if (Array.IndexOf(validTypes, normalizedType) < 0) + return $"Unsupported gesture type '{type}'. Supported types: swipe, tap, longpress, long-press."; + + string? normalizedDirection = null; + if (normalizedType == "swipe") + { + normalizedDirection = direction?.Trim().ToLowerInvariant(); + var validDirections = new[] { "up", "down", "left", "right" }; + + if (string.IsNullOrEmpty(normalizedDirection)) + return "Swipe gesture requires a 'direction' parameter ('up', 'down', 'left', 'right')."; + + if (Array.IndexOf(validDirections, normalizedDirection) < 0) + return $"Unsupported swipe direction '{direction}'. Supported directions: up, down, left, right."; + } + + var agent = await session.GetAgentClientAsync(agentPort); + var success = await agent.GestureAsync(normalizedType, elementId, normalizedDirection, distance, durationMs); + return success + ? elementId is not null ? $"Performed {normalizedType} gesture on element '{elementId}'." : $"Performed {normalizedType} gesture." + : $"Failed to perform {normalizedType} gesture."; + } + [McpServerTool(Name = "maui_scroll"), Description("Scroll a ScrollView, CollectionView, or ListView. Supports delta-based scrolling, scrolling to an item index, or scrolling an element into view.")] public static async Task Scroll( McpAgentSession session, diff --git a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/NavigationTools.cs b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/NavigationTools.cs index 8812326dc..97d9db9d3 100644 --- a/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/NavigationTools.cs +++ b/src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/NavigationTools.cs @@ -20,6 +20,18 @@ public static async Task Navigate( : $"Failed to navigate to '{route}'. Route may not exist in the Shell."; } + [McpServerTool(Name = "maui_back"), Description("Go back in the app navigation stack. Equivalent to pressing the system back button.")] + public static async Task Back( + McpAgentSession session, + [Description("Agent HTTP port (optional if only one agent connected)")] int? agentPort = null) + { + var agent = await session.GetAgentClientAsync(agentPort); + var success = await agent.BackAsync(); + return success + ? "Navigated back successfully." + : "Failed to navigate back. Navigation stack may be empty."; + } + [McpServerTool(Name = "maui_focus"), Description("Set focus to a UI element.")] public static async Task Focus( McpAgentSession session,