Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8787cf1
feat: Add reflection invoke for DevFlow Actions and arbitrary methods
Redth Apr 24, 2026
35638ac
fix: Use Nullable.GetUnderlyingType for IsRequired parameter check
Redth Apr 24, 2026
4648fb5
fix: Avoid double-invocation of async element methods in invoke handler
Redth Apr 24, 2026
ff2eb88
fix: Detect and deduplicate shadowed DevFlowAction names at discovery
Redth Apr 24, 2026
4c7d72e
fix: Handle ValueTask and ValueTask<T> returns in invoke method
Redth Apr 24, 2026
95663b4
fix: Use Lazy<T> for thread-safe action discovery cache
Redth Apr 24, 2026
89a5575
fix: Use ConcurrentDictionary for type resolution cache
Redth Apr 24, 2026
1f43336
fix: Filter framework assemblies in ResolveType to prevent arbitrary …
Redth Apr 24, 2026
bfc8f0b
fix: Guard against null GetString in ConvertInvokeArg parse fallbacks
Redth Apr 24, 2026
303e346
test: Add invoke tests for array, enum, and nullable parameter conver…
Redth Apr 24, 2026
2baf9b8
fix: Invalidate action cache on assembly load and hot reload
Redth Apr 24, 2026
725e566
feat: Add DFA005 duplicate action name diagnostic and analyzer unit t…
Redth Apr 24, 2026
7450fd7
test: Add MCP-style integration tests for invoke pipeline
Redth Apr 24, 2026
539c809
test: Add element invoke and DI service resolution tests
Redth Apr 24, 2026
255f4d9
fix: Address review feedback — ConvertInvokeArg robustness, TPA-based…
Redth Apr 27, 2026
f4b409e
fix: Clear invoke type cache on invalidation
Redth Apr 28, 2026
441f37a
fix: Return error status for invoke failures
Redth Apr 28, 2026
c2ab5e1
Fix invoke handlers to dispatch reflected calls
Redth Apr 28, 2026
2d3ac48
fix: Block MAUI framework invoke assemblies
Redth Apr 28, 2026
caa79ec
fix: Dispatch service invoke async work
Redth Apr 28, 2026
dd1c5f7
fix: Restrict element invoke to app methods
Redth Apr 28, 2026
b3e0648
fix: Score invoke overload candidates
Redth Apr 28, 2026
06139d3
Narrow DevFlow invoke to explicit actions
Redth Apr 28, 2026
9f61813
Fix DevFlow action CI failures
Redth Apr 28, 2026
549b3d7
Merge origin/main into feature branch
Redth Apr 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
<!-- DevFlow dependencies (pinned, third party) -->
<ItemGroup Label="DevFlow">
<PackageVersion Include="Fizzler" Version="$(FizzlerVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisCSharpVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisCSharpVersion)" />
<PackageVersion Include="SkiaSharp" Version="$(SkiaSharpVersion)" />
<PackageVersion Include="Spectre.Console" Version="$(SpectreConsoleVersion)" />
<PackageVersion Include="Spectre.Console.Testing" Version="$(SpectreConsoleVersion)" />
Expand Down Expand Up @@ -60,6 +62,7 @@
<PackageVersion Include="coverlet.collector" Version="$(CoverletCollectorVersion)" />
<PackageVersion Include="Microsoft.OpenApi" Version="$(MicrosoftOpenApiVersion)" />
<PackageVersion Include="YamlDotNet" Version="$(YamlDotNetVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
</ItemGroup>

<!-- Source Link -->
Expand Down
1 change: 1 addition & 0 deletions MauiLabs.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
</Folder>
<Folder Name="/src/DevFlow/">
<Project Path="src/DevFlow/Microsoft.Maui.DevFlow.Agent.Core/Microsoft.Maui.DevFlow.Agent.Core.csproj" />
<Project Path="src/DevFlow/Microsoft.Maui.DevFlow.Analyzers/Microsoft.Maui.DevFlow.Analyzers.csproj" />
<Project Path="src/DevFlow/Microsoft.Maui.DevFlow.Agent.Gtk/Microsoft.Maui.DevFlow.Agent.Gtk.csproj" />
<Project Path="src/DevFlow/Microsoft.Maui.DevFlow.Agent/Microsoft.Maui.DevFlow.Agent.csproj" />
<Project Path="src/DevFlow/Microsoft.Maui.DevFlow.Agent.IntegrationTests/Microsoft.Maui.DevFlow.Agent.IntegrationTests.csproj" />
Expand Down
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<!-- Pinned dependencies (third party, not flowing via maestro) -->
<PropertyGroup Label="Third Party">
<FizzlerVersion>1.3.1</FizzlerVersion>
<MicrosoftCodeAnalysisCSharpVersion>4.12.0</MicrosoftCodeAnalysisCSharpVersion>
<SkiaSharpVersion>3.119.2</SkiaSharpVersion>
<SpectreConsoleVersion>0.54.0</SpectreConsoleVersion>
<SystemCommandLineStableVersion>2.0.5</SystemCommandLineStableVersion>
Expand Down
4 changes: 2 additions & 2 deletions plugins/dotnet-maui/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dotnet-maui",
"version": "0.1.0",
"description": "Skills for .NET MAUI development: DevFlow automation, platform bindings, workload diagnostics, profiling, and accessibility.",
"version": "0.2.0",
"description": "Skills for .NET MAUI development: DevFlow automation, platform bindings, workload diagnostics, profiling, and accessibility. Some skills require the maui CLI tool.",
"skills": ["./skills/"]
}
153 changes: 153 additions & 0 deletions plugins/dotnet-maui/skills/devflow-automation/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
---
name: devflow-automation
description: >-
Automate .NET MAUI app state via explicitly registered DevFlow Actions. USE
FOR: discovering and invoking [DevFlowAction] shortcuts, logging in test
users, seeding data, navigating to deep screens, bypassing long UI flows to
reach target state quickly. DO NOT USE FOR: calling arbitrary methods,
invoking DI services or framework types, basic UI interaction (tap/fill/scroll
- use DevFlow MCP tools directly), visual tree inspection, screenshot capture,
connectivity issues, or build/deployment problems.
---

# DevFlow Automation - Actions

DevFlow Actions are named shortcuts that a .NET MAUI app explicitly exposes for automation with `[DevFlowAction]`. Use them to reach useful app states quickly, such as logging in a test user, seeding data, toggling a feature flag, or navigating to a deep screen.

Actions are opt-in. DevFlow does not expose arbitrary reflection invoke; if you need a new shortcut, add an attributed method in app debug/test code, let Hot Reload apply it, then list and invoke the action.

## Start by Listing Actions

Always check for available actions early in a DevFlow session:

```
maui_list_actions
```

Look for action names and descriptions that match your goal. Common patterns:

- `login-*` for authentication shortcuts
- `seed-*` for data population
- `navigate-*` for deep links or screen setup
- `set-*` for feature flags or configuration
- `reset-*` for state cleanup

## Invoke an Action

Arguments are passed as a JSON array in parameter order. Omit trailing optional parameters to use their defaults.

```
maui_invoke_action actionName="login-test-user"
maui_invoke_action actionName="login-test-user" argsJson='["alice@test.com", "secret"]'
maui_invoke_action actionName="seed-catalog" argsJson='[100]'
```

After invoking an action, verify the state with a screenshot, tree query, or other DevFlow tools:

```
maui_screenshot
```

## Hot Reload Workflow

If no useful action exists and you can edit the app:

1. Add a public static method annotated with `[DevFlowAction]`.
2. Add `[Description]` to each parameter so agents know what to pass.
3. Save and let C# Hot Reload apply the change.
4. Call `maui_list_actions` again.
5. Invoke the new action with `maui_invoke_action`.

Example:

```csharp
using System.ComponentModel;
using Microsoft.Maui.DevFlow.Agent.Core;

public static class DebugHelpers
{
[DevFlowAction("login-test-user", Description = "Log in as the standard test account")]
public static async Task LoginTestUser(
[Description("Email address for the test account")] string email = "test@example.com",
[Description("Password for the test account")] string password = "password123")
{
await AuthService.LoginAsync(email, password);
}
}
```

## Supported Parameter Types

Arguments are converted from JSON to these action parameter types:

| Type | JSON example |
|------|--------------|
| `string` | `"hello"` |
| `bool` | `true` or `false` |
| `int`, `long`, `short`, `byte` | `42` |
| `float`, `double`, `decimal` | `3.14` |
| `enum` | `"MemberName"` (case-insensitive) |
| arrays and supported list interfaces | `["a", "b"]` or `[1, 2, 3]` |
| nullable types | `null` or the value |

## Batch Support

Use `invoke-action` in batches when setup needs several steps:

```json
{
"actions": [
{ "action": "invoke-action", "name": "login-test-user" },
{ "action": "invoke-action", "name": "seed-catalog", "args": [100] },
{ "action": "tap", "elementId": "btn-advanced" }
]
}
```

## Rules for App Developers

- Methods must be `public static`.
- Parameters should be simple supported types, enums, nullable supported types, arrays, or supported list interfaces.
- Add `[Description]` to parameters so AI agents know what to pass.
- Prefer returning `void`, `Task`, `ValueTask`, `Task<T>`, or `ValueTask<T>` with simple return values.
- Action names should be unique and intention-revealing.

The DevFlow analyzer validates attributed methods:

| Diagnostic | Severity | Description |
|------------|----------|-------------|
| MAUI_DFA001 | Error | Unsupported parameter type |
| MAUI_DFA002 | Error | Method must be public static |
| MAUI_DFA003 | Warning | Return type may not serialize cleanly |
| MAUI_DFA004 | Info | Missing `[Description]` on parameter |
| MAUI_DFA005 | Warning | Duplicate `[DevFlowAction]` name |

## Common Patterns

### Authentication Bypass

```
maui_list_actions
maui_invoke_action actionName="login-test-user"
maui_screenshot
```

### Data Seeding

```
maui_invoke_action actionName="seed-catalog" argsJson='[200]'
maui_invoke_action actionName="seed-orders" argsJson='[50, true]'
```

### Feature Flag Override

```
maui_invoke_action actionName="set-feature-flag" argsJson='["dark-mode", true]'
maui_invoke_action actionName="set-feature-flag" argsJson='["experimental-ui", true]'
```

### Navigate to a Deep Screen

```
maui_invoke_action actionName="navigate-to" argsJson='["//settings/advanced/network"]'
```
3 changes: 2 additions & 1 deletion src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/McpServerHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ public static async Task RunAsync()
.WithTools<JobTools>()
.WithTools<FileTools>()
.WithTools<BleTools>()
.WithTools<BatchTools>();
.WithTools<BatchTools>()
.WithTools<InvokeTools>();

await builder.Build().RunAsync();
}
Expand Down
81 changes: 81 additions & 0 deletions src/Cli/Microsoft.Maui.Cli/DevFlow/Mcp/Tools/InvokeTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 InvokeTools
{
[McpServerTool(Name = "maui_list_actions"), Description("""
List all registered DevFlow Actions — named shortcuts the app developer has exposed
for automation. Each action has a name, description, and typed parameters.

Actions are methods annotated with [DevFlowAction] in the app's code. They're designed
to be called by AI agents to quickly set up app state (e.g., login, seed data,
navigate to a specific screen) without stepping through the UI manually.

Call this tool early when starting a DevFlow session — available actions can
dramatically reduce the number of steps needed to reach a desired app state.
""")]
public static async Task<string> ListActions(
McpAgentSession session,
[Description("Agent HTTP port (optional if only one agent connected)")] int? agentPort = null)
{
var agent = await session.GetAgentClientAsync(agentPort);
var result = await agent.ListActionsAsync();
return CliJson.SerializeUntyped(result, indented: false);
}

[McpServerTool(Name = "maui_invoke_action"), Description("""
Invoke a registered DevFlow Action by name. Actions are named shortcuts the app
developer has exposed — use maui_list_actions first to discover what's available.

Arguments are passed as a JSON array matching the action's parameter order.
Parameters with default values can be omitted. Supported types: string, bool,
int, long, float, double, decimal, enum values (by name), and arrays of these.

Example: To invoke "login-test-user" with email and password:
actionName: "login-test-user"
argsJson: '["alice@example.com", "secret123"]'

Example: To invoke "seed-catalog" with just a count (using default for other params):
actionName: "seed-catalog"
argsJson: '[100]'
""")]
public static async Task<string> InvokeAction(
McpAgentSession session,
[Description("Name of the DevFlow Action to invoke (from maui_list_actions)")] string actionName,
[Description("JSON array of arguments matching the action's parameter order. Omit trailing optional params. Example: '[\"hello\", 42, true]'")] string? argsJson = null,
[Description("Agent HTTP port (optional if only one agent connected)")] int? agentPort = null)
{
JsonArray? args = null;
if (!string.IsNullOrWhiteSpace(argsJson))
{
try
{
var node = JsonNode.Parse(argsJson);
if (node is not JsonArray array)
return $"Invalid argsJson: expected a JSON array, got {node?.GetValueKind().ToString() ?? "null"}.";
args = array;
}
catch (JsonException ex)
{
return $"Invalid JSON in argsJson: {ex.Message}";
}
}

var agent = await session.GetAgentClientAsync(agentPort);
var result = await agent.InvokeActionAsync(actionName, args);

if (result == null)
return $"Failed to invoke action '{actionName}'. Verify the app is running and the agent supports invoke.";

return result.Success
? $"Action '{actionName}' completed.{(result.ReturnValue != null ? $" Result: {result.ReturnValue}" : "")}"
: $"Action '{actionName}' failed: {result.Error}";
}

}
1 change: 1 addition & 0 deletions src/DevFlow/DevFlow.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"path": "../../MauiLabs.slnx",
"projects": [
"src\\DevFlow\\Microsoft.Maui.DevFlow.Agent.Core\\Microsoft.Maui.DevFlow.Agent.Core.csproj",
"src\\DevFlow\\Microsoft.Maui.DevFlow.Analyzers\\Microsoft.Maui.DevFlow.Analyzers.csproj",
"src\\DevFlow\\Microsoft.Maui.DevFlow.Agent\\Microsoft.Maui.DevFlow.Agent.csproj",
"src\\DevFlow\\Microsoft.Maui.DevFlow.Agent.Gtk\\Microsoft.Maui.DevFlow.Agent.Gtk.csproj",
"src\\DevFlow\\Microsoft.Maui.DevFlow.Blazor\\Microsoft.Maui.DevFlow.Blazor.csproj",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
namespace Microsoft.Maui.DevFlow.Agent.Core;

/// <summary>
/// Marks a method as a DevFlow Action — a named, discoverable shortcut that AI agents
/// and the <c>maui devflow invoke</c> CLI can call at runtime.
///
/// <para>
/// Use this to expose debug/test helpers (e.g., auto-login, seed data, navigate to a
/// deep screen) so that AI agents can discover and invoke them instead of manually
/// stepping through the UI.
/// </para>
///
/// <para>
/// Annotate parameters with <see cref="System.ComponentModel.DescriptionAttribute"/>
/// to provide AI-visible documentation for each parameter.
/// </para>
///
/// <example>
/// <code>
/// [DevFlowAction("login-test-user", Description = "Log in as the standard test account")]
/// public static async Task LoginTestUser(
/// [Description("Email address for the test account")] string email = "test@example.com",
/// [Description("Password for the test account")] string password = "password123")
/// {
/// await AuthService.LoginAsync(email, password);
/// }
/// </code>
/// </example>
/// </summary>
/// <remarks>
/// <para><b>Supported parameter types:</b></para>
/// <list type="bullet">
/// <item><description><c>string</c>, <c>bool</c></description></item>
/// <item><description><c>int</c>, <c>long</c>, <c>short</c>, <c>byte</c></description></item>
/// <item><description><c>float</c>, <c>double</c>, <c>decimal</c></description></item>
/// <item><description>Any <c>enum</c> type</description></item>
/// <item><description>Arrays or lists of the above: <c>string[]</c>, <c>List&lt;int&gt;</c>, etc.</description></item>
/// <item><description><c>Nullable&lt;T&gt;</c> of any supported value type</description></item>
/// </list>
///
/// <para>
/// Methods must be <c>public static</c>. Return type should be <c>void</c>,
/// <c>Task</c>, or <c>Task&lt;T&gt;</c> where T is a supported type or any
/// type whose <c>ToString()</c> produces a meaningful result.
/// </para>
///
/// <para>
/// The Roslyn analyzer <c>Microsoft.Maui.DevFlow.Analyzers</c> validates these
/// constraints at compile time when the analyzer is present.
/// </para>
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class DevFlowActionAttribute : Attribute
{
/// <summary>
/// The unique action name used to invoke this method via DevFlow tooling.
/// Use kebab-case (e.g., "login-test-user", "seed-catalog").
/// </summary>
public string Name { get; }

/// <summary>
/// A human-readable description of what this action does.
/// AI agents see this when discovering available actions.
/// </summary>
public string? Description { get; set; }

/// <summary>
/// Creates a new DevFlow Action attribute.
/// </summary>
/// <param name="name">
/// Unique action name in kebab-case (e.g., "login-test-user").
/// </param>
public DevFlowActionAttribute(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
}
Loading
Loading