Skip to content

Commit f71c1a4

Browse files
committed
chore: Release v0.9.34
notifications/tools/list_changed support + idb integration for native iOS automation
1 parent e67299e commit f71c1a4

21 files changed

Lines changed: 325 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 0.9.34
2+
3+
**notifications/tools/list_changed support + idb integration for native iOS automation**
4+
5+
### Changes
6+
- TODO: Add your changes here
7+
8+
---
9+
110
## 0.9.33
211

312
**fix dart format CI check across all files**

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ Then batch multiple actions in one call:
440440

441441
```yaml
442442
dependencies:
443-
flutter_skill: ^0.9.33
443+
flutter_skill: ^0.9.34
444444
```
445445
446446
```dart

intellij-plugin/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
}
66

77
group = "com.aidashboad"
8-
version = "0.9.33"
8+
version = "0.9.34"
99

1010
repositories {
1111
mavenCentral()

intellij-plugin/src/main/resources/META-INF/plugin.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<idea-plugin>
22
<id>com.aidashboad.flutterskill</id>
33
<name>Flutter Skill - AI App Automation</name>
4-
<version>0.9.33</version>
4+
<version>0.9.34</version>
55
<vendor email="support@ai-dashboad.com" url="https://github.com/ai-dashboad/flutter-skill">ai-dashboad</vendor>
66

77
<description><![CDATA[

lib/src/cli/server.dart

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ part 'tool_handlers/bug_report_handlers.dart';
6868
part 'tool_handlers/fixture_handlers.dart';
6969
part 'tool_handlers/explore_handlers.dart';
7070

71-
const String currentVersion = '0.9.33';
71+
const String currentVersion = '0.9.34';
7272

7373
/// Session information for multi-session support
7474
class SessionInfo {
@@ -388,7 +388,10 @@ class FlutterMcpServer {
388388
try {
389389
if (method == 'initialize') {
390390
_sendResult(id, {
391-
"capabilities": {"tools": {}, "resources": {}},
391+
"capabilities": {
392+
"tools": {"listChanged": true},
393+
"resources": {}
394+
},
392395
"protocolVersion": "2024-11-05",
393396
"serverInfo": {"name": "flutter-skill", "version": currentVersion},
394397
});
@@ -406,6 +409,7 @@ class FlutterMcpServer {
406409
'launch_chrome': true,
407410
});
408411
stderr.writeln('CDP auto-connect: $result');
412+
_sendNotification('notifications/tools/list_changed');
409413
} catch (e) {
410414
stderr.writeln('CDP auto-connect failed: $e');
411415
}
@@ -443,6 +447,27 @@ class FlutterMcpServer {
443447
{"type": "text", "text": jsonEncode(result)},
444448
],
445449
});
450+
// Notify MCP clients to refresh tool list after connection state changes.
451+
// This allows clients to discover the full automation toolkit after connecting.
452+
const _connectionStateTools = {
453+
'connect_app',
454+
'launch_app',
455+
'scan_and_connect',
456+
'connect_cdp',
457+
'connect_openclaw_browser',
458+
'connect_webmcp',
459+
'disconnect',
460+
};
461+
if (_connectionStateTools.contains(name)) {
462+
final succeeded = result is Map
463+
? (result['success'] == true ||
464+
result['connected'] == true ||
465+
name == 'disconnect')
466+
: false;
467+
if (succeeded) {
468+
_sendNotification('notifications/tools/list_changed');
469+
}
470+
}
446471
}
447472
} catch (e, stackTrace) {
448473
if (id != null) {
@@ -1042,6 +1067,14 @@ class FlutterMcpServer {
10421067
10431068
// ==================== End Build Error Helpers ====================
10441069

1070+
void _sendNotification(String method, [Map<String, dynamic>? params]) {
1071+
stdout.writeln(jsonEncode({
1072+
"jsonrpc": "2.0",
1073+
"method": method,
1074+
if (params != null && params.isNotEmpty) "params": params,
1075+
}));
1076+
}
1077+
10451078
void _sendResult(dynamic id, dynamic result) {
10461079
if (id == null) return;
10471080
stdout.writeln(jsonEncode({"jsonrpc": "2.0", "id": id, "result": result}));

lib/src/cli/tool_handlers/native_handlers.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,19 @@ extension _NativeHandlers on FlutterMcpServer {
449449
return {"success": true, ...devices};
450450
}
451451

452+
if (name == 'idb_describe') {
453+
final driver = await NativeDriver.create(null);
454+
if (driver is! IosSimulatorDriver) {
455+
return {
456+
"success": false,
457+
"error":
458+
"No booted iOS Simulator found. idb requires a running simulator.",
459+
};
460+
}
461+
final info = await driver.describe();
462+
return {"success": info['idb_available'] == true, ...info};
463+
}
464+
452465
// Auth tools (system commands, no bridge connection required)
453466

454467
return null; // Not handled by this group

lib/src/drivers/native_driver.dart

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@ class _Point {
236236
class IosSimulatorDriver extends NativeDriver {
237237
String? _cachedUdid;
238238
String? _cachedBridgePath;
239+
// idb availability and scale factor are detected once and cached.
240+
bool? _idbAvailable;
241+
double? _cachedLogicalScale;
239242

240243
@override
241244
NativePlatform get platform => NativePlatform.iosSimulator;
@@ -300,6 +303,84 @@ class IosSimulatorDriver extends NativeDriver {
300303
/// Check if HID bridge is available (preferred over osascript)
301304
Future<bool> _hasBridge() async => (await _getBridgePath()) != null;
302305

306+
// ========== idb (iOS Development Bridge) integration ==========
307+
308+
/// Run an idb command, using python3.13 to work around the Python 3.14
309+
/// asyncio regression in the fb-idb package.
310+
Future<ProcessResult> _runIdb(List<String> args) async {
311+
// Prefer python3.13 which is compatible with fb-idb 1.1.7.
312+
// Fall back to the idb binary in case a fixed version is installed.
313+
const py313 = '/opt/homebrew/bin/python3.13';
314+
final py313Exists = await File(py313).exists();
315+
if (py313Exists) {
316+
return Process.run(py313, [
317+
'-c',
318+
'import asyncio; asyncio.set_event_loop(asyncio.new_event_loop()); '
319+
'from idb.cli.main import main; main()',
320+
'--',
321+
...args,
322+
]).timeout(const Duration(seconds: 10),
323+
onTimeout: () => ProcessResult(0, 1, '', 'idb timeout'));
324+
}
325+
return Process.run('idb', args).timeout(
326+
const Duration(seconds: 10),
327+
onTimeout: () => ProcessResult(0, 1, '', 'idb timeout'),
328+
);
329+
}
330+
331+
/// Returns true if idb is usable on this machine.
332+
Future<bool> _hasIdb() async {
333+
if (_idbAvailable != null) return _idbAvailable!;
334+
try {
335+
final result = await _runIdb(['list-targets', '--json']);
336+
_idbAvailable = result.exitCode == 0;
337+
} catch (_) {
338+
_idbAvailable = false;
339+
}
340+
return _idbAvailable!;
341+
}
342+
343+
/// Get the logical scale factor (pixels per point) for the booted simulator.
344+
/// Used to convert device-pixel coordinates to idb logical-point coordinates.
345+
Future<double> _getLogicalScale() async {
346+
if (_cachedLogicalScale != null) return _cachedLogicalScale!;
347+
try {
348+
final udid = await _getBootedSimulatorUdid();
349+
final result = await _runIdb(['describe', '--udid', udid]);
350+
if (result.exitCode == 0) {
351+
final out = result.stdout as String;
352+
// Parse "Screen: <w> x <h> (logical)" or similar idb output.
353+
final match = RegExp(r'(\d+)\s*x\s*(\d+)\s*\(?logical').firstMatch(out);
354+
if (match != null) {
355+
final logicalWidth = double.parse(match.group(1)!);
356+
// Get pixel width from a simctl screenshot (fast, cached result).
357+
final shotPath = '${Directory.systemTemp.path}/fs_scale_probe.png';
358+
final shot = await Process.run(
359+
'xcrun', ['simctl', 'io', udid, 'screenshot', shotPath]);
360+
if (shot.exitCode == 0) {
361+
// Use `sips` (built-in macOS tool) to read pixel dimensions.
362+
final sips = await Process.run('sips', [
363+
'-g',
364+
'pixelWidth',
365+
shotPath,
366+
]);
367+
final sipsOut = sips.stdout as String;
368+
final pixelMatch =
369+
RegExp(r'pixelWidth:\s*(\d+)').firstMatch(sipsOut);
370+
if (pixelMatch != null) {
371+
final pixelWidth = double.parse(pixelMatch.group(1)!);
372+
_cachedLogicalScale = pixelWidth / logicalWidth;
373+
return _cachedLogicalScale!;
374+
}
375+
}
376+
}
377+
}
378+
} catch (_) {}
379+
// Safe default: most modern iPhones are @3x.
380+
_cachedLogicalScale = 3.0;
381+
return _cachedLogicalScale!;
382+
}
383+
303384
/// Get the UDID of the first booted simulator
304385
Future<String> _getBootedSimulatorUdid() async {
305386
if (_cachedUdid != null) return _cachedUdid!;
@@ -369,6 +450,30 @@ class IosSimulatorDriver extends NativeDriver {
369450
}
370451
}
371452

453+
// idb is more reliable than the Accessibility API: it uses XCTest under the
454+
// hood and requires no coordinate mapping (operates in logical points).
455+
if (await _hasIdb()) {
456+
final udid = await _getBootedSimulatorUdid();
457+
final scale = await _getLogicalScale();
458+
final lx = (x / scale).roundToDouble();
459+
final ly = (y / scale).roundToDouble();
460+
final idbResult =
461+
await _runIdb(['ui', 'tap', '$lx', '$ly', '--udid', udid]);
462+
if (idbResult.exitCode == 0) {
463+
return NativeResult(
464+
success: true,
465+
message:
466+
'Tapped at device (${x.round()}, ${y.round()}) via idb (logical: $lx, $ly)',
467+
metadata: {
468+
'device_coords': {'x': x, 'y': y},
469+
'logical_coords': {'x': lx, 'y': ly},
470+
'backend': 'idb',
471+
},
472+
);
473+
}
474+
// idb failed — fall through to Accessibility API.
475+
}
476+
372477
// Fallback: osascript approach
373478
// Map device pixel coordinates to screen coordinates for hit-testing
374479
final screenCoords = await _mapToScreenCoordinates(x, y);
@@ -514,6 +619,19 @@ end tell
514619
Future<NativeResult> inputText(String text) async {
515620
final udid = await _getBootedSimulatorUdid();
516621

622+
// idb text injection is the cleanest approach: no clipboard side-effects
623+
// and no "Allow Paste" dialog to dismiss.
624+
if (await _hasIdb()) {
625+
final idbResult = await _runIdb(['ui', 'text', text, '--udid', udid]);
626+
if (idbResult.exitCode == 0) {
627+
return NativeResult(
628+
success: true,
629+
message: 'Entered text via idb: "$text"',
630+
metadata: {'backend': 'idb'},
631+
);
632+
}
633+
}
634+
517635
// Copy text to simulator pasteboard
518636
final process = await Process.start(
519637
'xcrun',
@@ -588,6 +706,37 @@ end tell
588706
}
589707
}
590708

709+
// idb swipe is more reliable (XCTest-backed, no coordinate mapping needed).
710+
if (await _hasIdb()) {
711+
final udid = await _getBootedSimulatorUdid();
712+
final scale = await _getLogicalScale();
713+
final lx1 = (startX / scale).roundToDouble();
714+
final ly1 = (startY / scale).roundToDouble();
715+
final lx2 = (endX / scale).roundToDouble();
716+
final ly2 = (endY / scale).roundToDouble();
717+
final duration =
718+
(durationMs / 1000.0).toStringAsFixed(2); // idb expects seconds
719+
final idbResult = await _runIdb([
720+
'ui',
721+
'swipe',
722+
'$lx1',
723+
'$ly1',
724+
'$lx2',
725+
'$ly2',
726+
'--duration',
727+
duration,
728+
'--udid',
729+
udid,
730+
]);
731+
if (idbResult.exitCode == 0) {
732+
return NativeResult(
733+
success: true,
734+
message: 'Swiped via idb',
735+
metadata: {'backend': 'idb'},
736+
);
737+
}
738+
}
739+
591740
// Fallback: osascript approach
592741
// Determine scroll direction from the swipe vector
593742
final deltaY = endY - startY;
@@ -692,6 +841,36 @@ end tell
692841
return {
693842
'xcrun_simctl': await _isCommandAvailable('xcrun'),
694843
'osascript': await _isCommandAvailable('osascript'),
844+
'idb': await _hasIdb(),
845+
};
846+
}
847+
848+
/// Return device description and idb availability for the MCP `idb_describe` tool.
849+
Future<Map<String, dynamic>> describe() async {
850+
final idbOk = await _hasIdb();
851+
if (!idbOk) {
852+
return {
853+
'idb_available': false,
854+
'error': 'idb not available. Install with: pip3 install fb-idb\n'
855+
'Requires Python 3.13 at /opt/homebrew/bin/python3.13',
856+
'fallback':
857+
'native_tap/swipe/input_text will use macOS Accessibility API instead.',
858+
};
859+
}
860+
861+
final udid = await _getBootedSimulatorUdid();
862+
final scale = await _getLogicalScale();
863+
final idbResult = await _runIdb(['describe', '--udid', udid]);
864+
final rawDescription =
865+
idbResult.exitCode == 0 ? (idbResult.stdout as String).trim() : null;
866+
867+
return {
868+
'idb_available': true,
869+
'udid': udid,
870+
'scale_factor': scale,
871+
'description': rawDescription,
872+
'note':
873+
'native_tap/swipe/input_text automatically use idb when available.',
695874
};
696875
}
697876

lib/src/engine/tool_registry.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class ToolRegistry {
4141
'get_connection_status',
4242
'disconnect',
4343
'diagnose_project',
44+
'native_list_simulators',
45+
'idb_describe',
4446
};
4547

4648
/// CDP-only tools that don't apply to bridge/Flutter platforms.
@@ -2435,6 +2437,19 @@ This captures the ENTIRE device screen, not just the Flutter app content.""",
24352437
},
24362438
},
24372439

2440+
// ======================== idb (iOS Development Bridge) ========================
2441+
{
2442+
"name": "idb_describe",
2443+
"description": "Describe the booted iOS Simulator via idb (iOS Development Bridge). "
2444+
"Returns device model, OS version, logical screen resolution, scale factor, "
2445+
"and idb availability. Use this to verify idb is working before using "
2446+
"native_tap / native_swipe / native_input_text (which prefer idb when available).",
2447+
"inputSchema": {
2448+
"type": "object",
2449+
"properties": {},
2450+
},
2451+
},
2452+
24382453
// ======================== Diagnose ========================
24392454
{
24402455
"name": "diagnose_project",

0 commit comments

Comments
 (0)