mcpTools method

  1. @override
List<McpToolDescriptor> mcpTools()

Returns the MCP tool descriptors this provider contributes.

Defaults to an empty list so existing providers compile without modification. Override to expose VM-Service-backed tools to the MCP server (see McpToolDescriptor for the descriptor contract).

Implementation

@override
List<McpToolDescriptor> mcpTools() => const <McpToolDescriptor>[
      // ---------------------------------------------------------------------
      // 1. Snapshot: captures the Semantics tree, no required params.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_snap',
        description: 'Capture a YAML snapshot of the running Flutter app\'s '
            'Semantics tree with stable `[ref=eN]` tokens.\n'
            '\n'
            'Walks the app\'s Semantics tree from the root, emits one '
            'YAML node per widget with role / label / actions / bounds, '
            'and assigns each node a stable `eN` ref. Every action tool '
            '(dusk_tap, dusk_hover, dusk_drag, dusk_type) takes one of '
            'these refs to identify its target. Call this FIRST, then '
            'pass the returned ref tokens to subsequent action calls.\n'
            '\n'
            'Nodes inside a currently-overflowing render ancestor carry '
            'an additive `overflow: true` sub-line (live '
            '`RenderFlex.toStringShort()` check). This is a current-state '
            'signal only; call dusk_exceptions for the full non-fatal '
            'error history including overflow details.\n'
            '\n'
            'Usage:\n'
            '- Call before any dusk_* action tool; refs become stale '
            'after navigation, modal open/close, or significant widget '
            'tree rebuild.\n'
            '- Pass `depth: <n>` to limit tree traversal depth (default '
            'unlimited). Useful when the tree is huge and you only need '
            'the visible region.\n'
            '- Returns YAML; the model parses ref tokens out of the '
            'shape `[ref=e<N>]` next to each widget.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'depth': <String, dynamic>{
              'type': 'integer',
              'description': 'Max tree-traversal depth from the root. '
                  'Omit for full tree. Use a small number (5-10) when '
                  'snapshotting only the focused screen.',
            },
          },
        },
        extensionMethod: 'ext.dusk.snap',
      ),
      // ---------------------------------------------------------------------
      // 2. Tap: pointer Down+50ms+Up at the widget identified by ref.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_tap',
        description: 'Tap a widget by ref token from a prior dusk_snap.\n'
            '\n'
            'Synthesizes a pointer Down + 50ms hold + Up sequence at the '
            'center of the widget identified by `ref`. Triggers '
            '`GestureDetector.onTap`, `InkWell.onTap`, button onPressed, '
            'and any other gesture handler bound to that widget. For '
            'TextField widgets the tap also requests keyboard focus.\n'
            '\n'
            'Usage:\n'
            '- Call dusk_snap first to get a ref token; the ref string '
            'has shape `e<N>`.\n'
            '- Returns the ref of the tapped widget on success; errors '
            'when the ref is unknown or stale (re-snap to refresh).\n'
            '- For drag use dusk_drag; for typing use dusk_type after '
            'dusk_tap to focus the field.\n'
            '- Set verify=true to confirm the tap produced an observable '
            'effect: the response gains a `changed` boolean (true when the '
            "target's route or semantics subtree changed, false when "
            'nothing did).',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'ref': <String, dynamic>{
              'type': 'string',
              'description': 'Widget ref token from a prior dusk_snap '
                  'call. Shape: `e<N>` (e.g. `e5`, `e23`).',
            },
            'verify': <String, dynamic>{
              'type': 'boolean',
              'description': 'When true, capture a target-scoped before/'
                  'after signal (route + semantics-subtree hash) and add a '
                  '`changed` boolean to the response reporting whether the '
                  'tap had an observable effect. Defaults to false, which '
                  'keeps the response shape unchanged.',
            },
            'until': <String, dynamic>{
              'type': 'string',
              'description': 'After the tap settles, poll the live element '
                  'tree for a Text whose data equals this value and add an '
                  '`untilMatched` boolean to the response. Omit to keep the '
                  'response shape unchanged.',
            },
            'untilTimeoutMs': <String, dynamic>{
              'type': 'integer',
              'description': 'Timeout in milliseconds for the `until` poll '
                  '(default 3000). Ignored when `until` is omitted.',
            },
          },
          'required': <String>['ref'],
        },
        extensionMethod: 'ext.dusk.tap',
      ),
      // ---------------------------------------------------------------------
      // 3. Screenshot: captures a JPEG/PNG frame, no required params.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_screenshot',
        description: 'Capture a screenshot of the running Flutter app as a '
            'base64-encoded image.\n'
            '\n'
            'Renders the current frame to a JPEG or PNG and returns the '
            'image bytes inline so the model can see the UI directly. '
            'This MCP tool always dispatches the in-isolate '
            '`ext.dusk.screenshot` extension.\n'
            '\n'
            'Web limitation: the in-isolate path can hang under '
            'CanvasKit+DWDS (the toImage() future never completes). For '
            'reliable web screenshots, run the CLI command '
            '`dusk:screenshot --output=<path>` instead: when artisan was '
            'started with `--cdp-port`, the CLI falls back to CDP '
            '`Page.captureScreenshot` for a full-viewport capture. That CDP '
            'fallback is CLI-only; it does not apply to this MCP tool.\n'
            '\n'
            'Usage:\n'
            '- No required params; defaults to JPEG at quality 70.\n'
            '- Pass `format: "png"` for a lossless (larger) payload.\n'
            '- Captures the WHOLE app surface; for region screenshots use '
            'dusk_snap to locate a widget first.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'format': <String, dynamic>{
              'type': 'string',
              'enum': <String>['jpeg', 'png'],
              'description': 'Image format. `jpeg` (default) is smaller '
                  '(good for quick visual checks); `png` is lossless but '
                  'larger (use when pixel-perfect detail matters).',
            },
            'quality': <String, dynamic>{
              'type': 'integer',
              'description': 'JPEG quality 0-100 (higher is better). '
                  'Default 70. Ignored when format is `png`.',
            },
          },
        },
        extensionMethod: 'ext.dusk.screenshot',
      ),
      // ---------------------------------------------------------------------
      // 4. Hover: PointerHoverEvent (mouse kind) at the widget center.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_hover',
        description: 'Hover a mouse cursor over a widget by ref token (mouse-'
            'only, no touch equivalent).\n'
            '\n'
            'Synthesizes a `PointerHoverEvent` of `PointerDeviceKind.'
            'mouse` at the center of the widget identified by `ref`. '
            'Triggers `MouseRegion.onEnter`, tooltip reveal, hover '
            'state changes. Web + desktop apps only; mobile devices '
            'ignore hover events.\n'
            '\n'
            'Usage:\n'
            '- Use to reveal tooltips, dropdown previews, or hover-'
            'state widgets before dusk_tap.\n'
            '- Call dusk_snap first to get a ref token.\n'
            '- No-op on touch-only devices (Android, iOS).',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'ref': <String, dynamic>{
              'type': 'string',
              'description': 'Widget ref token from a prior dusk_snap '
                  'call. Shape: `e<N>`.',
            },
          },
          'required': <String>['ref'],
        },
        extensionMethod: 'ext.dusk.hover',
      ),
      // ---------------------------------------------------------------------
      // 5. Drag: Down+5xMove+Up sequence from startRef to endRef.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_drag',
        description: 'Drag from one widget to another by ref tokens.\n'
            '\n'
            'Synthesizes a pointer Down + 5x intermediate Move events + '
            'Up sequence from `startRef`\'s center to `endRef`\'s '
            'center. Use for reorder lists, slider scrubbing, drawer '
            'pulls, swipe-to-dismiss gestures.\n'
            '\n'
            'Usage:\n'
            '- Both refs come from dusk_snap; widget positions are '
            'frozen at snap time.\n'
            '- For pure-direction swipes without a target widget, use '
            'dusk_scroll (when available) or wrap a region in dusk_snap '
            'and use a phantom endRef.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'startRef': <String, dynamic>{
              'type': 'string',
              'description': 'Source widget ref token (`e<N>`). The '
                  'drag begins at this widget\'s center.',
            },
            'endRef': <String, dynamic>{
              'type': 'string',
              'description': 'Target widget ref token (`e<N>`). The '
                  'drag ends at this widget\'s center.',
            },
          },
          'required': <String>['startRef', 'endRef'],
        },
        extensionMethod: 'ext.dusk.drag',
      ),
      // ---------------------------------------------------------------------
      // 6. Type: sets text into a focused text field identified by ref.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_type',
        description: 'Type text into a TextField widget by ref token.\n'
            '\n'
            'Targets the `TextField` / `TextFormField` widget identified '
            'by `ref` and fires `userUpdateTextEditingValue` so '
            '`onChanged` callbacks fire and form validators run. Falls '
            'back to direct controller mutation when the editable cannot '
            'accept input events. Replaces existing text (does not '
            'append).\n'
            '\n'
            'Usage:\n'
            '- Call dusk_tap on the field FIRST to focus it, then '
            'dusk_type to enter text.\n'
            '- To clear a field, pass `text: ""`.\n'
            '- Submits character by character via the keyboard event '
            'pipeline; multi-line text uses `\\n` line breaks.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'ref': <String, dynamic>{
              'type': 'string',
              'description': 'TextField widget ref token (`e<N>`).',
            },
            'text': <String, dynamic>{
              'type': 'string',
              'description': 'Text to enter. Replaces existing field '
                  'content. Pass empty string to clear.',
            },
          },
          'required': <String>['ref', 'text'],
        },
        extensionMethod: 'ext.dusk.type',
      ),
      // ---------------------------------------------------------------------
      // 7. Scroll: drives a Scrollable widget identified by ref.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_scroll',
        description: 'Scroll a Scrollable widget by ref token.\n'
            '\n'
            'Drives the nearest `Scrollable` ancestor of the widget '
            'identified by `ref` to bring lazily-built children into '
            'view, page through long lists, or reveal off-screen content '
            'before a follow-up dusk_tap. Pair with dusk_snap before AND '
            'after the scroll: post-scroll refs differ from pre-scroll '
            'refs because new widgets enter the Semantics tree.\n'
            '\n'
            'Usage:\n'
            '- Pass `ref` of any widget inside (or below) the target '
            'Scrollable; the extension walks up to the nearest '
            '`Scrollable` ancestor.\n'
            '- Optional `direction` (default `down`) accepts `up`, '
            '`down`, `left`, `right`.\n'
            '- Optional `pixels` (default 300) tunes the scroll '
            'distance; pass larger values to page faster.\n'
            '- Re-snap after the scroll completes; off-screen widgets '
            'gain refs only once Flutter builds them.\n'
            '\n'
            'Example: `{ "ref": "e12", "direction": "down", "pixels": 500 }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'ref': <String, dynamic>{
              'type': 'string',
              'description': 'Widget ref token (`e<N>`) inside the '
                  'target Scrollable. The extension walks up to the '
                  'nearest ancestor Scrollable.',
            },
            'direction': <String, dynamic>{
              'type': 'string',
              'enum': <String>['up', 'down', 'left', 'right'],
              'description': 'Scroll direction. Default `down`.',
            },
            'pixels': <String, dynamic>{
              'type': 'number',
              'description': 'Logical pixels to scroll. Default 300. '
                  'Larger values page faster but may overshoot.',
            },
          },
          'required': <String>['ref'],
        },
        extensionMethod: 'ext.dusk.scroll',
      ),
      // ---------------------------------------------------------------------
      // 8. Wait for: polls a condition until satisfied or the timeout
      // expires. One-of condition validated inside the handler.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_wait_for',
        description: 'Wait until a UI condition is satisfied or the timeout '
            'expires.\n'
            '\n'
            'Polls the running app at a fixed interval and returns once '
            'one of three one-of conditions is met: `text` (a Semantics '
            'node with that label exists), `textGone` (no Semantics node '
            'with that label exists), or `expression` (a Dart expression '
            'evaluated via the Tinker bridge returns truthy). Useful for '
            'bridging async UI transitions before a follow-up dusk_tap / '
            'dusk_snap.\n'
            '\n'
            'Usage:\n'
            '- Pass exactly ONE of `text`, `textGone`, `expression`; the '
            'handler errors when zero or multiple are passed.\n'
            '- Optional `timeoutMs` (default 5000) caps the wait; on '
            'timeout the call returns an error result, never silently '
            'continues.\n'
            '- Polling interval is 100 ms; the handler returns as soon '
            'as the condition flips, never blocks the full timeout.\n'
            '\n'
            'Example: `{ "text": "Welcome", "timeoutMs": 3000 }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'text': <String, dynamic>{
              'type': 'string',
              'description': 'Wait until a Semantics node with this '
                  'exact label exists. One-of: pair with neither '
                  '`textGone` nor `expression`.',
            },
            'textGone': <String, dynamic>{
              'type': 'string',
              'description': 'Wait until NO Semantics node with this '
                  'label exists. One-of: pair with neither `text` nor '
                  '`expression`.',
            },
            'expression': <String, dynamic>{
              'type': 'string',
              'description': 'Wait until this Dart expression (evaluated '
                  'via the Tinker bridge) returns a truthy value. '
                  'One-of: pair with neither `text` nor `textGone`.',
            },
            'timeoutMs': <String, dynamic>{
              'type': 'integer',
              'description': 'Wait timeout in milliseconds. Default '
                  '5000. On timeout the call returns an error, never '
                  'continues silently.',
            },
          },
        },
        extensionMethod: 'ext.dusk.wait_for',
      ),
      // ---------------------------------------------------------------------
      // 9. Dismiss modals: pops every dialog / bottom sheet / route on top
      // of the first persistent route.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_dismiss_modals',
        description: 'Pop every modal route (dialog, bottom sheet, popup) '
            'currently above the first persistent route.\n'
            '\n'
            'Walks the active Navigator stack and pops each modal route '
            'in LIFO order until only the first non-modal route remains. '
            'Useful for resetting the UI to a known state between test '
            'flows, dismissing left-over confirm dialogs, or clearing a '
            'stack of bottom sheets without per-modal taps.\n'
            '\n'
            'Usage:\n'
            '- No parameters.\n'
            '- Idempotent: safe to call when no modals are open '
            '(returns immediately with zero pops).\n'
            '- Does NOT pop the root route; the underlying screen '
            'remains.\n'
            '- For dismissing a single specific dialog, prefer dusk_tap '
            'on the dialog\'s explicit Cancel button.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{},
        },
        extensionMethod: 'ext.dusk.dismiss_modals',
      ),
      // ---------------------------------------------------------------------
      // 10. Navigate: pushes a named route onto the active router.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_navigate',
        description: 'Navigate the running Flutter app to a route path.\n'
            '\n'
            'Pushes the supplied route onto the active router stack. '
            'Resolves through `MagicRoute.to(...)` when the Magic '
            'framework is installed, falling back to '
            '`Navigator.of(root).pushNamed(...)` otherwise. Useful for '
            'driving the app to a specific screen before taking a '
            'snapshot or action, without traversing the UI by hand.\n'
            '\n'
            'Usage:\n'
            '- Pass `route: "/monitors/123"` (must start with `/`).\n'
            '- ALWAYS re-snapshot after navigation; refs from a prior '
            'dusk_snap are invalidated by the route change.\n'
            '- For going back use dusk_navigate_back; to list known '
            'routes use dusk_get_routes.\n'
            '\n'
            'Example: `{ "route": "/login" }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'route': <String, dynamic>{
              'type': 'string',
              'description': 'Route path to push. Must start with `/`. '
                  'Example: `/monitors/123`.',
            },
          },
          'required': <String>['route'],
        },
        extensionMethod: 'ext.dusk.navigate',
      ),
      // ---------------------------------------------------------------------
      // 11. Navigate back: pops the top route off the navigator stack.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_navigate_back',
        description: 'Pop the top route off the active navigator stack.\n'
            '\n'
            'Equivalent to pressing the system Back button: calls '
            '`MagicRoute.back()` when Magic is installed, falling back '
            'to `Navigator.of(root).pop()` otherwise. Useful for '
            'returning from a detail screen to its list without '
            'snapshotting and tapping a Back AppBar button.\n'
            '\n'
            'Usage:\n'
            '- No parameters.\n'
            '- No-op when the stack has only one route; safe to call '
            'speculatively.\n'
            '- Re-snapshot after the pop; refs from the previous screen '
            'are stale.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{},
        },
        extensionMethod: 'ext.dusk.navigate_back',
      ),
      // ---------------------------------------------------------------------
      // 12. Get routes: enumerates the declared routes in the active
      // router.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_get_routes',
        description: 'List the route paths declared by the running app\'s '
            'router.\n'
            '\n'
            'Walks the active `MagicRouter` (when Magic is installed) '
            'and emits every registered route path with its name and '
            'any path parameters. Useful before a dusk_navigate call '
            'when the available routes are not known upfront, or when '
            'auditing the surface area of the app.\n'
            '\n'
            'Usage:\n'
            '- No parameters.\n'
            '- Returns a list of `{ path, name }` records; static and '
            'parameterised paths are both included (parameters render '
            'as `:id`-style placeholders).\n'
            '- Returns an empty list when no Magic router is installed.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{},
        },
        extensionMethod: 'ext.dusk.get_routes',
      ),
      // ---------------------------------------------------------------------
      // 13. Press key: synthesises a hardware key event with optional
      // modifiers.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_press_key',
        description: 'Press a hardware key (optionally with modifiers).\n'
            '\n'
            'Synthesises a `KeyDownEvent` + `KeyUpEvent` through '
            '`ServicesBinding.instance.keyboard.handleKeyEvent` so '
            'shortcut intents, `Focus.onKeyEvent` handlers, and '
            '`CallbackShortcuts` widgets fire. Use for keyboard-driven '
            'flows that have no equivalent tap target: Escape to close, '
            'Enter to submit, arrow keys to navigate a list, Ctrl+S to '
            'save.\n'
            '\n'
            'Usage:\n'
            '- Pass `key: "Enter"` (key name from '
            '`LogicalKeyboardKey.keyLabel`). Common values: `Enter`, '
            '`Escape`, `Tab`, `ArrowDown`, `Backspace`.\n'
            '- Optional `modifiers: ["control", "shift"]` chord the '
            'key with modifier keys held during the press.\n'
            '- For text input prefer dusk_type; this tool is for '
            'shortcut keys only.\n'
            '\n'
            'Example: `{ "key": "S", "modifiers": ["control"] }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'key': <String, dynamic>{
              'type': 'string',
              'description': 'Logical key label. Examples: `Enter`, '
                  '`Escape`, `Tab`, `ArrowDown`, `S`, `Backspace`.',
            },
            'modifiers': <String, dynamic>{
              'type': 'array',
              'items': <String, dynamic>{
                'type': 'string',
                'enum': <String>['control', 'shift', 'alt', 'meta'],
              },
              'description': 'Modifier keys held during the press. '
                  'Accepts any subset of `control`, `shift`, `alt`, '
                  '`meta`.',
            },
          },
          'required': <String>['key'],
        },
        extensionMethod: 'ext.dusk.press_key',
      ),
      // ---------------------------------------------------------------------
      // 14. Select option: drives a DropdownButton / DropdownButtonFormField
      // to the matching item.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_select_option',
        description: 'Select an option in a DropdownButton by ref + value.\n'
            '\n'
            'Opens the `DropdownButton` / `DropdownButtonFormField` '
            'identified by `ref`, finds the item whose value equals '
            '`value`, and taps it. Bridges the gap between dusk_tap '
            '(which opens the menu) and a second dusk_snap+dusk_tap '
            'pair (which would otherwise be needed to pick the item).\n'
            '\n'
            'Usage:\n'
            '- Pass `ref` of the dropdown widget from a prior '
            'dusk_snap.\n'
            '- Pass `value` matching the option text or the underlying '
            'value (the handler tries label match first, then '
            '`toString()` of the underlying value).\n'
            '- Re-snap after the selection; the dropdown closes and '
            'downstream widgets may re-render.\n'
            '\n'
            'Example: `{ "ref": "e7", "value": "GET" }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'ref': <String, dynamic>{
              'type': 'string',
              'description': 'Dropdown widget ref token (`e<N>`) from '
                  'a prior dusk_snap.',
            },
            'value': <String, dynamic>{
              'type': 'string',
              'description': 'Option to select. Matches against the '
                  'displayed label first, then `toString()` of the '
                  'underlying value.',
            },
          },
          'required': <String>['ref', 'value'],
        },
        extensionMethod: 'ext.dusk.select_option',
      ),
      // ---------------------------------------------------------------------
      // 15. Evaluate: runs a Dart expression through the Tinker bridge.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_evaluate',
        description:
            'Evaluate a Dart expression in the running app isolate.\n'
            '\n'
            'Forwards `expression` to the Tinker bridge '
            '(`ext.tinker.evaluate`) and returns the stringified result. '
            'Useful for inspecting controller state, asserting an '
            'invariant from the model layer, or reading a private '
            'getter that has no UI surface. Reaches anything visible '
            'to the Magic facade autocomplete (Auth, Http, Cache, '
            'controllers via `Magic.find<T>()`).\n'
            '\n'
            'Usage:\n'
            '- Pass a single Dart expression (no statements, no '
            'semicolons). Example: `Magic.find<MonitorController>().'
            'rxState.value.length`.\n'
            '- Errors when the Tinker plugin is not installed (returns '
            'an MCP error result, never crashes the app).\n'
            '- For UI assertions prefer dusk_snap; this tool is for '
            'state inspection that lives below the Semantics tree.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'expression': <String, dynamic>{
              'type': 'string',
              'description': 'Single Dart expression to evaluate. No '
                  'statements; no trailing semicolon. Example: '
                  '`Auth.user?.email`.',
            },
          },
          'required': <String>['expression'],
        },
        extensionMethod: 'ext.dusk.evaluate',
      ),
      // ---------------------------------------------------------------------
      // 16. Close app: requests a graceful shutdown of the running app.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_close_app',
        description: 'Request a graceful shutdown of the running Flutter '
            'app.\n'
            '\n'
            'Calls `SystemNavigator.pop()` which routes through the '
            'platform back-button channel. On mobile + desktop this '
            'terminates the app; on web the call is a no-op (browsers '
            'do not allow programmatic tab close). Useful at the end '
            'of a long-running E2E session when the next test run '
            'wants a clean process.\n'
            '\n'
            'Usage:\n'
            '- No parameters.\n'
            '- After calling this, the next dusk_* tool will fail '
            'because the VM Service URI is gone; restart the app '
            'before issuing further commands.\n'
            '- No-op on web; pair with manual tab close if you need a '
            'fresh browser session.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{},
        },
        extensionMethod: 'ext.dusk.close_app',
      ),
      // ---------------------------------------------------------------------
      // 17. Find: Playwright-Locator-style query handle. q-shape refs
      // re-execute the stored predicates on each action call.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_find',
        description:
            'Find a widget by semantic query (text / semanticsLabel / '
            'key) and return a re-resolvable handle.\n'
            '\n'
            'Mints a `q<N>` handle backed by the supplied predicates. '
            'Unlike `e<N>` refs from dusk_snap (which freeze the widget '
            'position at snap time and stale on rebuild), q-handles '
            're-execute the Semantics + Element tree walk on every '
            'subsequent dusk_tap / dusk_hover / dusk_drag / dusk_type '
            'call, so they survive widget rebuilds, route pushes, and '
            'snapshot disposal as long as the predicates still match.\n'
            '\n'
            'Usage:\n'
            '- Pass at least one of `text`, `semanticsLabel`, or `key`. '
            'When multiple are supplied they form an intersection.\n'
            '- Prefer dusk_find when the target survives re-renders '
            '(stable Text, stable accessibility label, stable Key); '
            'prefer dusk_snap+`eN` when you want a positional snapshot '
            'of the whole tree.\n'
            '- Returns `{ref: "q<N>", matched: true}` on first match, '
            'or `{ref: null, matched: false}` when no node matches.\n'
            '- When a follow-up action call finds zero live matches, '
            'the handler returns a "stale handle" error — agent must '
            're-find or re-snap, NOT retry.\n'
            '\n'
            'Example: `{ "text": "Submit" }` or `{ "key": "monitor-row-7" }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'text': <String, dynamic>{
              'type': 'string',
              'description': 'Exact match against an accessibility label '
                  'first, then against `Text.data` as fallback. Example: '
                  '`"Submit"` matches a labelled button or a visible '
                  '`Text("Submit")` widget.',
            },
            'semanticsLabel': <String, dynamic>{
              'type': 'string',
              'description': 'Exact match against `SemanticsNode.label` '
                  'ONLY (no Text fallback). Use when the visible text '
                  'differs from the accessibility label.',
            },
            'key': <String, dynamic>{
              'type': 'string',
              'description': 'Match against a widget Key. For ValueKey, '
                  'pass the inner value\'s `toString()` (e.g. '
                  '`"monitor-row-7"`); for arbitrary Keys, pass the '
                  'full `Key.toString()` value.',
            },
          },
        },
        extensionMethod: 'ext.dusk.find',
      ),
      // ---------------------------------------------------------------------
      // 18. Wait for network idle (Step 3.4). Polls
      // TelescopeStore.pendingHttpCount via the pendingHttpCountReader
      // function-pointer indirection (set by the host when telescope is
      // installed; defaults to () => 0 otherwise ; missing-telescope
      // graceful path returns matched=true immediately).
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_wait_for_network_idle',
        description: 'Wait until the running app reports zero in-flight HTTP '
            'requests for a contiguous idleMs window.\n'
            '\n'
            'Polls the host\'s in-flight HTTP counter (wired by '
            'fluttersdk_telescope\'s `MagicHttpFacadeAdapter.pendingCount` '
            'when both packages are present) every `pollIntervalMs` ms. '
            'When the counter reaches 0 a continuous-zero countdown of '
            '`idleMs` starts; any spike back to a positive count fully '
            'resets the accumulator (Playwright `waitForLoadState` '
            'network-idle semantics). On success returns '
            '`{matched: true, idleAchievedMs: <int>}`; on timeout returns '
            'a structured error envelope with `type: "timeout"` and the '
            'max in-flight count observed inside the wire `message` field.\n'
            '\n'
            'Usage:\n'
            '- No required params; sensible defaults '
            '(`timeoutMs=5000`, `idleMs=500`, `pollIntervalMs=200`).\n'
            '- Call AFTER dusk_tap / dusk_navigate / dusk_select_option to '
            'bridge async network round-trips before a follow-up dusk_snap. '
            'Replaces the `dusk_wait_for textGone=Loading...` pattern when '
            'the loading affordance is not Semantically labelled.\n'
            '- Missing-telescope graceful: when the host has not wired '
            'telescope the counter is constantly 0 and the call returns '
            'idle immediately, so callers do not need to branch on the '
            'host\'s install state.\n'
            '- `pollIntervalMs` must be >= 100ms (CPU constraint); the '
            'handler asserts this internally.\n'
            '\n'
            'Example: `{ "timeoutMs": 8000, "idleMs": 750 }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'timeoutMs': <String, dynamic>{
              'type': 'integer',
              'description': 'Maximum total wait time in milliseconds. '
                  'Default 5000.',
            },
            'idleMs': <String, dynamic>{
              'type': 'integer',
              'description': 'Contiguous-zero window the loop must observe '
                  'before declaring idle. Default 500.',
            },
            'pollIntervalMs': <String, dynamic>{
              'type': 'integer',
              'description': 'Poll cadence in milliseconds. Minimum 100; '
                  'default 200.',
            },
          },
        },
        extensionMethod: 'ext.dusk.wait_for_network_idle',
      ),
      // ---------------------------------------------------------------------
      // 19. Console: reads recent log entries from the telescope store.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_console',
        description: 'Read recent log entries from the running app\'s '
            'telescope store.\n'
            '\n'
            'Reads structured log entries recorded by the telescope HTTP '
            'adapter and log watcher when `fluttersdk_telescope` is '
            'installed and wired. When telescope is absent the call '
            'returns an empty list immediately (missing-telescope graceful '
            'path), so callers do not need to branch on install state.\n'
            '\n'
            'Usage:\n'
            '- No required params; defaults to the 50 most recent entries '
            'at any severity level.\n'
            '- Pass `limit: <n>` to cap the returned count.\n'
            '- Pass `minLevel: "WARNING"` (or `"ERROR"`) to filter by '
            'minimum severity level.\n'
            '- Each log entry contains `level`, `message`, `time`, and '
            '`logger` fields.\n'
            '- Useful for asserting that a controller logged an expected '
            'message without needing to inspect the UI tree.\n'
            '\n'
            'Example: `{ "limit": 10, "minLevel": "ERROR" }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'limit': <String, dynamic>{
              'type': 'integer',
              'description': 'Maximum number of log entries to return. '
                  'Default 50.',
            },
            'minLevel': <String, dynamic>{
              'type': 'string',
              'description': 'Minimum severity level to include. Examples: '
                  '`INFO`, `WARNING`, `ERROR`. Omit to include all levels.',
            },
          },
        },
        extensionMethod: 'ext.dusk.console',
      ),
      // ---------------------------------------------------------------------
      // 20. Exceptions: reads recent exception entries from telescope.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_exceptions',
        description: 'Read recent exceptions recorded by the running app, '
            'including non-fatal FlutterErrors.\n'
            '\n'
            'Returns the merged union of two sources: the in-package '
            'non-fatal `FlutterError.onError` capture buffer (installed '
            'by `DuskPlugin.install()`) and the telescope exception '
            'watcher (when `fluttersdk_telescope` is present). Entries '
            'are deduped by `(type, message, stackHead)`, sorted '
            'newest-first, and clipped to `limit`. RenderFlex overflow '
            'errors appear with `type: "overflow"`. Missing-telescope '
            'graceful: the in-package buffer alone ensures non-fatal '
            'errors are visible even without telescope.\n'
            '\n'
            'Usage:\n'
            '- No required params; defaults to the 20 most recent exceptions.\n'
            '- Pass `limit: <n>` to cap the returned count.\n'
            '- Pass `since: "<iso8601>"` to return only exceptions strictly '
            'after that timestamp (e.g. capture the time before an action, '
            'then call with `since` to see only new exceptions).\n'
            '- Each entry contains `type`, `message`, `stackHead` (first '
            '3 lines of the stack trace), `library`, `fatal`, and `time`.\n'
            '- Useful for asserting that an action did not trigger an '
            'unexpected exception, or for diagnosing overflow errors '
            'flagged in a prior dusk_snap `overflow: true` annotation.\n'
            '\n'
            'Example: `{ "limit": 5, "since": "2024-01-01T10:00:00.000Z" }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'limit': <String, dynamic>{
              'type': 'integer',
              'description': 'Maximum number of exception entries to '
                  'return. Default 20.',
            },
            'since': <String, dynamic>{
              'type': 'string',
              'description': 'ISO8601 timestamp. When set, only exceptions '
                  'whose `time` is strictly after this value are returned. '
                  'Omit to return the full cumulative list. Unparseable '
                  'values are silently ignored (treated as absent).',
            },
          },
        },
        extensionMethod: 'ext.dusk.exceptions',
      ),
      // ---------------------------------------------------------------------
      // 21. Double-click: two tap sequences at ref center (~100ms apart).
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_dblclick',
        description: 'Double-click a widget by ref token from a prior '
            'dusk_snap.\n'
            '\n'
            'Synthesizes two pointer Down+50ms+Up sequences at the center '
            'of the widget identified by `ref`, with ~100ms between the '
            'two taps, matching Playwright\'s double-click model. Triggers '
            '`GestureDetector.onDoubleTap` and any double-tap handlers '
            'registered on that widget. The 4-gate actionability check '
            '(enabled, non-zero rect, in-viewport, stable) runs before the '
            'first tap; the post-action snapshot is captured once after the '
            'second tap completes.\n'
            '\n'
            'Usage:\n'
            '- Call dusk_snap first to get a ref token; the ref string '
            'has shape `e<N>` or `q<N>`.\n'
            '- For single tap use dusk_tap; for drag use dusk_drag.\n'
            '- Returns the ref of the clicked widget on success; errors '
            'when the ref is unknown, stale, or the widget fails the '
            'actionability gate.\n'
            '\n'
            'Example: `{ "ref": "e7" }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'ref': <String, dynamic>{
              'type': 'string',
              'description': 'Widget ref token from a prior dusk_snap '
                  'call. Shape: `e<N>` (e.g. `e5`, `e23`).',
            },
          },
          'required': <String>['ref'],
        },
        extensionMethod: 'ext.dusk.dblclick',
      ),
      // ---------------------------------------------------------------------
      // 22. Set checkbox: read + conditionally toggle a Checkbox or Switch.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_set_checkbox',
        description: 'Set the checked state of a Checkbox or Switch widget '
            'by ref.\n'
            '\n'
            'Reads the widget\'s current checked state via an element/Semantics '
            'walk. When the current value already matches `value` the call '
            'returns an idempotent success without tapping (safe to call '
            'speculatively). When the current value differs, injects a tap '
            'at the widget\'s center to toggle it.\n'
            '\n'
            'Usage:\n'
            '- Call dusk_snap first to get a ref token for the target '
            'Checkbox or Switch widget.\n'
            '- Pass `value: "true"` to check or `value: "false"` to uncheck.\n'
            '- Returns `{ref, previousValue, value, toggled: bool}` — use '
            '`toggled` to confirm whether a tap was actually issued.\n'
            '- Re-snap after a toggle to see the updated Semantics tree.\n'
            '\n'
            'Example: `{ "ref": "e4", "value": "true" }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'ref': <String, dynamic>{
              'type': 'string',
              'description': 'Checkbox or Switch widget ref token (`e<N>`) '
                  'from a prior dusk_snap.',
            },
            'value': <String, dynamic>{
              'type': 'string',
              'enum': <String>['true', 'false'],
              'description': 'Target checked state. `"true"` checks the '
                  'widget; `"false"` unchecks it.',
            },
          },
          'required': <String>['ref', 'value'],
        },
        extensionMethod: 'ext.dusk.set_checkbox',
      ),
      // ---------------------------------------------------------------------
      // 23. Observe: structured candidate list (Stagehand observe-once-
      // act-many; no server-side LLM).
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_observe',
        description:
            'Return a structured candidate list of every interactive widget '
            'on screen.\n'
            '\n'
            'Walks the live Semantics + Element tree once, finds every '
            'interactive node (buttons, text fields, links, checkboxes, '
            'dropdowns — same role detection as dusk_find), and mints a '
            're-resolvable `q<N>` ref for each candidate via the '
            'Playwright-Locator pattern. NO server-side LLM is invoked: '
            'the agent reads the candidate list and decides which refs to '
            'act on. Implements Stagehand\'s observe-once-act-many pattern '
            '(model-agnostic locked decision).\n'
            '\n'
            'Each candidate carries: `ref`, `role`, `label`, `value`, '
            '`bounds` (x/y/w/h), `isEnabled`, `isVisible`, plus a subset '
            'of per-candidate enricher fields when '
            '`includeEnrichers=true` (default: `magicFormField`, '
            '`magicRoute`, `magicGateResult`, `wind.breakpoint`, '
            '`wind.states`).\n'
            '\n'
            'Usage:\n'
            '- No required params; sensible defaults observe every role '
            'with `limit=50` and the default enricher subset.\n'
            '- Pass `intent: "<hint>"` to record what the agent is looking '
            'for (NOT used server-side; accepted purely for caller-side '
            'audit logging).\n'
            '- Pass `roles: "button,textbox"` to filter the candidate '
            'list to a comma-separated subset.\n'
            '- Pass `limit: <n>` to cap the candidate count.\n'
            '- Pass `includeEnrichers: "false"` to skip enricher fields '
            'entirely; pass `includeEnrichers: "full"` for every enricher '
            'field (incl. full `wind` block).\n'
            '- q-refs survive widget rebuilds; reuse a single observe '
            'output across many follow-up dusk_tap / dusk_type / dusk_drag '
            'calls without re-observing.\n'
            '\n'
            'Examples:\n'
            '- `{ "roles": "button", "limit": 20 }`\n'
            '- `{ "intent": "login form", "roles": "textbox,button" }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'intent': <String, dynamic>{
              'type': 'string',
              'description': 'Free-form caller hint describing what the '
                  'agent is looking for (e.g. "login form"). NOT used '
                  'server-side; echoed in audit logs only.',
            },
            'roles': <String, dynamic>{
              'type': 'string',
              'description': 'Comma-separated role filter '
                  '(e.g. `"button,textbox"`). Roles match the same '
                  'vocabulary as dusk_snap: `button`, `textbox`, `link`, '
                  '`checkbox`, `heading`, `image`. Omit for every role.',
            },
            'limit': <String, dynamic>{
              'type': 'integer',
              'description': 'Maximum number of candidates to return. '
                  'Default 50.',
            },
            'includeEnrichers': <String, dynamic>{
              'type': 'string',
              'enum': <String>['true', 'false', 'full'],
              'description': 'Toggle per-candidate enricher fields. '
                  '`"true"` (default) projects the default subset '
                  '(magicFormField, magicRoute, magicGateResult, '
                  'wind.breakpoint+states); `"false"` projects no '
                  'enricher fields; `"full"` projects every field '
                  'including all wind sub-fields.',
            },
          },
        },
        extensionMethod: 'ext.dusk.observe',
      ),
      // ---------------------------------------------------------------------
      // 24. Hot reload and snap (Step 4.2): mcp_flutter's
      // fmt_hot_reload_and_capture pattern. Triggers a VM Service hot reload
      // from outside the running isolate (a same-isolate handler would block
      // on reloadSources against itself), then bundles snapshot + screenshot
      // + recent exceptions into one response.
      //
      // extensionMethod routes through the `artisan:` substrate dispatch
      // prefix instead of `ext.dusk.*`; the MCP server executes the CLI
      // command (`dusk:hot_reload_and_snap`) in-process so the orchestration
      // can drive `vm.reloadSources` against the target isolate.
      // ---------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_hot_reload_and_snap',
        description: 'Hot reload the running Flutter app, then capture a '
            'snapshot, screenshot, and recent exceptions in one '
            'round-trip.\n'
            '\n'
            'Triggers `reloadSources` over the VM Service against the '
            'running app, waits for completion, then calls dusk_snap + '
            'dusk_screenshot + dusk_exceptions and bundles every result '
            'into a single response. Equivalent to mcp_flutter\'s '
            '`fmt_hot_reload_and_capture`; saves three round-trips when '
            'the agent needs to validate that a code change took effect.\n'
            '\n'
            'Usage:\n'
            '- No required params; sensible defaults capture the '
            'screenshot.\n'
            '- Pass `screenshot: "false"` to skip the screenshot step '
            '(useful when the agent only needs the Semantics tree).\n'
            '- On reload success the response carries `reloaded: true`, '
            '`durationMs`, `snapshot`, `screenshot` (or `screenshotError` '
            'when capture failed; the snapshot still lands), and '
            '`recentExceptions`.\n'
            '- On compile error the response carries `reloaded: false`, '
            '`durationMs`, `error` (the compile message), and '
            '`recentExceptions`. snapshot + screenshot are omitted.\n'
            '- Recent exceptions reuse dusk_exceptions wiring; missing '
            'telescope is graceful (empty list).\n'
            '\n'
            'Example: `{ "screenshot": "false" }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'screenshot': <String, dynamic>{
              'type': 'boolean',
              'description': 'Capture a screenshot after the reload '
                  '(default true). Pass `false` to skip the screenshot '
                  'step.',
            },
          },
        },
        extensionMethod: 'artisan:dusk:hot_reload_and_snap',
      ),
      // -------------------------------------------------------------------
      // P4 (Playwright parity): focus / blur / clear / right_click /
      // triple_click. Five short descriptors; the behavior mirrors the
      // matching Playwright Locator methods (focus, blur, clear,
      // click({button:right}), click({clickCount:3})).
      // -------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_focus',
        description: 'Request keyboard focus on the widget identified by '
            '`ref`.\n\n'
            'Playwright parity: `locator.focus()`. Walks the resolved '
            'element to find the nearest Focus ancestor and calls '
            'requestFocus(). Returns `{ref, focused: true}`.\n\n'
            'Usage:\n'
            '- `ref` (required): widget ref token from a prior dusk_snap.\n'
            '- `includeSnapshot` (default false): embed post-focus snapshot.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'ref': <String, dynamic>{
              'type': 'string',
              'description': 'Widget ref token (e.g. "e5").',
            },
            'includeSnapshot': <String, dynamic>{
              'type': 'boolean',
              'description': 'Embed the post-focus snapshot (default false).',
            },
          },
          'required': <String>['ref'],
        },
        extensionMethod: 'ext.dusk.focus',
      ),
      McpToolDescriptor(
        name: 'dusk_blur',
        description: 'Clear keyboard focus from whatever currently holds it.'
            '\n\n'
            'Playwright parity: `locator.blur()` / `document.activeElement'
            '.blur()`. Returns `{blurred: true, hadFocus: bool}`.\n\n'
            'Usage:\n'
            '- No required params; blurs the primary focused node.\n'
            '- `includeSnapshot` (default false): embed post-blur snapshot.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'includeSnapshot': <String, dynamic>{
              'type': 'boolean',
              'description': 'Embed the post-blur snapshot (default false).',
            },
          },
        },
        extensionMethod: 'ext.dusk.blur',
      ),
      McpToolDescriptor(
        name: 'dusk_clear',
        description: 'Empty the TextEditingController backing the resolved '
            'text field.\n\n'
            'Playwright parity: `locator.clear()`. Walks the resolved '
            'element to find an EditableText descendant, extracts its '
            'controller, calls clear(). Returns `{ref, text: ""}`.\n\n'
            'Usage:\n'
            '- `ref` (required): widget ref of a TextField / TextFormField '
            '/ EditableText.\n'
            '- `includeSnapshot` (default false): embed post-clear snapshot.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'ref': <String, dynamic>{
              'type': 'string',
              'description': 'Widget ref of the text field (e.g. "e5").',
            },
            'includeSnapshot': <String, dynamic>{
              'type': 'boolean',
              'description': 'Embed the post-clear snapshot (default false).',
            },
          },
          'required': <String>['ref'],
        },
        extensionMethod: 'ext.dusk.clear',
      ),
      McpToolDescriptor(
        name: 'dusk_right_click',
        description: 'Fire a right (secondary mouse button) click at the '
            'widget identified by `ref`.\n\n'
            'Playwright parity: `locator.click({ button: "right" })`. '
            'Useful for context menus. Runs the 5-gate actionability check '
            'before injecting the PointerDownEvent + 50ms hold + '
            'PointerUpEvent (mouse kind, kSecondaryButton).\n\n'
            'Usage:\n'
            '- `ref` (required): widget ref token.\n'
            '- `includeSnapshot` (default false): embed post-action snapshot.\n'
            '- `checkStable`/`checkReceivesEvents` (both default true): '
            'gate opt-outs.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'ref': <String, dynamic>{
              'type': 'string',
              'description': 'Widget ref token (e.g. "e5").',
            },
            'includeSnapshot': <String, dynamic>{
              'type': 'boolean',
              'description':
                  'Embed the post-action snapshot (default false).',
            },
            'checkStable': <String, dynamic>{
              'type': 'boolean',
              'description':
                  'Run the Stable actionability gate (default true).',
            },
            'checkReceivesEvents': <String, dynamic>{
              'type': 'boolean',
              'description': 'Run the Receives-Events actionability gate '
                  '(default true).',
            },
          },
          'required': <String>['ref'],
        },
        extensionMethod: 'ext.dusk.right_click',
      ),
      McpToolDescriptor(
        name: 'dusk_triple_click',
        description: 'Fire three primary clicks (~100ms apart) at the '
            'widget identified by `ref`.\n\n'
            'Playwright parity: `locator.click({ clickCount: 3 })`. In '
            'Material text fields this selects an entire paragraph. Runs '
            'the 5-gate actionability check once before the first tap; '
            'subsequent taps assume the target is still actionable.\n\n'
            'Usage:\n'
            '- `ref` (required): widget ref token.\n'
            '- `includeSnapshot` (default false): embed post-action snapshot.\n'
            '- `checkStable`/`checkReceivesEvents` (both default true): '
            'gate opt-outs.',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'ref': <String, dynamic>{
              'type': 'string',
              'description': 'Widget ref token (e.g. "e5").',
            },
            'includeSnapshot': <String, dynamic>{
              'type': 'boolean',
              'description':
                  'Embed the post-action snapshot (default false).',
            },
            'checkStable': <String, dynamic>{
              'type': 'boolean',
              'description':
                  'Run the Stable actionability gate (default true).',
            },
            'checkReceivesEvents': <String, dynamic>{
              'type': 'boolean',
              'description': 'Run the Receives-Events actionability gate '
                  '(default true).',
            },
          },
          'required': <String>['ref'],
        },
        extensionMethod: 'ext.dusk.triple_click',
      ),
      // -------------------------------------------------------------------
      // 25. Resize viewport: set dimensions, DPR, mobile, touch via CDP.
      // -------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_resize_viewport',
        description: 'Resize the running Flutter web app viewport via Chrome '
            'DevTools Protocol.\n'
            '\n'
            'Sets browser viewport dimensions, device pixel ratio, mobile '
            'profile, and touch emulation flags via the Emulation.* CDP '
            'methods. Allows testing responsive layouts and mobile-only UI '
            'without DevTools manual intervention. Requires artisan to have '
            'been started with --cdp-port (which pre-launches Chrome with a '
            'debug port); fail-loudly if cdpPort is not set.\n'
            '\n'
            'Usage:\n'
            '- Requires `width` (integer, CSS pixels; e.g. 375 for iPhone) '
            'and `height` (integer, CSS pixels; e.g. 812).\n'
            '- Optional `deviceScaleFactor` (number; default 1.0) simulates '
            'device DPR. Use 2.0 for Retina, 3.0 for iPhone Pro.\n'
            '- Optional `mobile` (boolean; default false) enables mobile '
            'device profile (affects text sizing, viewport meta, scrolling '
            'UX).\n'
            '- Optional `touch` (boolean; default false) enables touch event '
            'synthesis (PointerDown/Move/Up sequences).\n'
            '- Optional `reset` (boolean; default false) clears ALL viewport '
            'overrides (metrics, touch, user agent) in one call. When true, '
            'all other params are ignored.\n'
            '\n'
            'Examples:\n'
            '- `{"width": 375, "height": 812, "deviceScaleFactor": 3.0, '
            '"mobile": true, "touch": true}` (iPhone-X emulation).\n'
            '- `{"width": 1440, "height": 900, "reset": false}` (desktop '
            'viewport).\n'
            '- `{"reset": true}` (clear overrides, revert to host defaults).',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'width': <String, dynamic>{
              'type': 'integer',
              'description': 'Viewport width in CSS pixels (required). '
                  'Example: 375 for mobile, 1440 for desktop.',
            },
            'height': <String, dynamic>{
              'type': 'integer',
              'description': 'Viewport height in CSS pixels (required). '
                  'Example: 812 for mobile, 900 for desktop.',
            },
            'deviceScaleFactor': <String, dynamic>{
              'type': 'number',
              'description': 'Device pixel ratio (optional; default 1.0). '
                  'Use 2.0 for Retina, 3.0 for iPhone Pro DPR. Must be '
                  'greater than 0.',
            },
            'mobile': <String, dynamic>{
              'type': 'boolean',
              'description': 'Enable mobile device profile (optional; default '
                  'false). Affects viewport meta tag behavior, text autosizing, '
                  'and scrolling UX.',
            },
            'touch': <String, dynamic>{
              'type': 'boolean',
              'description': 'Enable touch event synthesis (optional; default '
                  'false). When true, browser fires touch events instead of '
                  'mouse events.',
            },
            'reset': <String, dynamic>{
              'type': 'boolean',
              'description': 'Clear all viewport overrides (optional; default '
                  'false). When true, metrics + touch + user agent are reset. '
                  'All other params ignored when reset is true.',
            },
          },
          'required': <String>['width', 'height'],
        },
        extensionMethod: 'artisan:dusk:resize',
      ),
      // -------------------------------------------------------------------
      // 26. Device profile: emulate a named device preset via CDP.
      // -------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_device_profile',
        description: 'Emulate a named device profile (viewport + DPR + touch '
            '+ user agent) via Chrome DevTools Protocol.\n'
            '\n'
            'Applies a curated device preset (iphone-x, pixel-5, ipad-pro, '
            'desktop-1440, etc.) in a single call. Each preset bundles the '
            'correct viewport dimensions, device pixel ratio, touch '
            'emulation flag, and user agent string so the app renders as it '
            'would on that actual device. Requires artisan to have been '
            'started with --cdp-port (which pre-launches Chrome with a debug '
            'port).\n'
            '\n'
            'Usage:\n'
            '- Call with `list: true` (no `preset` required) to enumerate '
            'all available device presets with dimensions.\n'
            '- Call with `preset: "<name>"` (string; no list) to apply a '
            'preset. Valid preset names: iphone-x, iphone-13, iphone-15-pro, '
            'pixel-5, pixel-8, ipad-pro-12.9, desktop-1440, desktop-1920.\n'
            '- Call with `reset: true` (optional; no preset required) to '
            'clear all overrides (metrics, touch, user agent). Equivalent to '
            'dusk_resize_viewport with reset=true.\n'
            '- When `preset` is unknown, exits with error and suggests '
            'running with `list: true` to see available options.\n'
            '\n'
            'Examples:\n'
            '- `{"preset": "iphone-x"}` (apply iPhone-X emulation).\n'
            '- `{"list": true}` (show all 8 presets).\n'
            '- `{"reset": true}` (clear overrides).',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'preset': <String, dynamic>{
              'type': 'string',
              'enum': <String>[
                'iphone-x',
                'iphone-13',
                'iphone-15-pro',
                'pixel-5',
                'pixel-8',
                'ipad-pro-12.9',
                'desktop-1440',
                'desktop-1920',
              ],
              'description': 'Device preset name (optional). One of: '
                  'iphone-x, iphone-13, iphone-15-pro, pixel-5, pixel-8, '
                  'ipad-pro-12.9, desktop-1440, desktop-1920. Omit when '
                  'using list or reset.',
            },
            'list': <String, dynamic>{
              'type': 'boolean',
              'description': 'List all available device presets (optional; '
                  'default false). When true, preset and reset are ignored.',
            },
            'reset': <String, dynamic>{
              'type': 'boolean',
              'description': 'Clear all viewport overrides (optional; default '
                  'false). When true, metrics + touch + user agent are reset. '
                  'Preset is ignored when reset is true.',
            },
          },
        },
        extensionMethod: 'artisan:dusk:device',
      ),
      // -------------------------------------------------------------------
      // 27. Fill (D7): focus + clear + type + settle + stale-retry in one
      // call. Composes the gated focus / clear / type handlers.
      // -------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_fill',
        description: 'Fill a text field by ref: focus, clear, type, settle '
            'in one call.\n'
            '\n'
            'Replaces the manual focus + clear + type + wait dance every '
            'agent otherwise re-discovers. Composes the gated focus, clear, '
            'and type handlers (so the actionability gate, IME focus, and '
            '`onChanged`/validator firing all run) and retries the whole '
            'sequence once when the ref goes stale mid-fill. Use after a '
            'dusk_snap (or dusk_find for a re-resolvable q-handle) to enter '
            'text without three separate round-trips.\n'
            '\n'
            'Usage:\n'
            '- Pass `ref` of the target TextField (`e<N>` or `q<N>`) and '
            '`text` to set. Pass `text: ""` to clear the field.\n'
            '- Prefer a q-handle (dusk_find) when the field may rebuild '
            'between snapshot and fill; the stale-retry then re-resolves '
            'cleanly on the second pass.\n'
            '- Returns `{ref, text, filled: true}` plus an optional '
            'post-fill snapshot.\n'
            '\n'
            'Example: `{ "ref": "e7", "text": "alice@example.com" }`',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{
            'ref': <String, dynamic>{
              'type': 'string',
              'description': 'Text-field ref token (`e<N>` or `q<N>`) from '
                  'a prior dusk_snap / dusk_find.',
            },
            'text': <String, dynamic>{
              'type': 'string',
              'description': 'Value to set. Replaces existing content. Pass '
                  'an empty string to clear the field.',
            },
            'includeSnapshot': <String, dynamic>{
              'type': 'boolean',
              'description': 'Embed the post-fill snapshot (default true).',
            },
          },
          'required': <String>['ref', 'text'],
        },
        extensionMethod: 'ext.dusk.fill',
      ),
      // -------------------------------------------------------------------
      // 28. Reset overlays (D7): dismiss + Escape + Cancel-tap fallback.
      // Idempotent; promotes the manual overlay-reset dance.
      // -------------------------------------------------------------------
      McpToolDescriptor(
        name: 'dusk_reset_overlays',
        description: 'Reset the app to a clean screen: dismiss every modal, '
            'press Escape, then tap a Cancel/Dismiss affordance.\n'
            '\n'
            'Three escalating layers run in order, each a no-op when the '
            'prior already cleared the overlays: (1) pop every PopupRoute '
            '(dialogs, bottom sheets, popups) without touching the page '
            'stack; (2) an Escape key press for overlays driven by the '
            'dismiss shortcut; (3) a Cancel/Dismiss/Close/OK/Done labelled '
            'tap for modal barriers that need an explicit affordance. '
            'Idempotent: safe to call speculatively between flows.\n'
            '\n'
            'Usage:\n'
            '- No parameters.\n'
            '- Returns `{popped: N, escaped: bool, dismissTapped: bool}` so '
            'you can see which layer cleared the screen.\n'
            '- Prefer this over dusk_dismiss_modals when an overlay is not '
            'a PopupRoute (custom OverlayEntry, dropdown menu, barrier '
            'dialog).',
        inputSchema: <String, dynamic>{
          'type': 'object',
          'properties': <String, dynamic>{},
        },
        extensionMethod: 'ext.dusk.reset_overlays',
      ),
    ];