Skip to content

Commit 5a5cae2

Browse files
committed
chore: release v0.9.24 — fix 6 UX issues
- connect_app: detect DTD protocol (-32601) and show clear error with fix hint - connect_by_pid: new tool — connect by PID using lsof port discovery - scan_and_connect: expand default port range from 50000-50100 to 48000-65000 - screenshot: change default save_to_file to true to prevent token overflow - connect_app/scan_and_connect: auto-cleanup stale sessions on connect - _cleanupStaleSessions: new helper to remove disconnected sessions
1 parent cadf5aa commit 5a5cae2

20 files changed

Lines changed: 212 additions & 28 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.24
2+
3+
**fix 6 UX issues: DTD error detection, wider port scan, connect_by_pid, stale session cleanup, screenshot save_to_file default**
4+
5+
### Changes
6+
- TODO: Add your changes here
7+
8+
---
9+
110
## 0.9.23
211

312
**fix null check crash and stop auto-opening browser on RPC errors from target app**

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.23
443+
flutter_skill: ^0.9.24
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.23"
8+
version = "0.9.24"
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.23</version>
4+
<version>0.9.24</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: 12 additions & 1 deletion
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.23';
71+
const String currentVersion = '0.9.24';
7272

7373
/// Session information for multi-session support
7474
class SessionInfo {
@@ -835,6 +835,17 @@ class FlutterMcpServer {
835835
return uri;
836836
}
837837

838+
/// Check whether a URI looks like a DTD (Dart Tooling Daemon) URI.
839+
/// DTD URIs end with `/ws` but respond to DTD protocol, not VM Service.
840+
/// We detect this heuristically: if connecting returns -32601 "Unknown method"
841+
/// on getVM, it is a DTD endpoint.
842+
bool _looksLikeDtdUri(String error) {
843+
final e = error.toLowerCase();
844+
return e.contains('-32601') ||
845+
e.contains('unknown method') ||
846+
e.contains('method not found');
847+
}
848+
838849
static const Map<String, Map<String, dynamic>> _gesturePresets = {
839850
'drawer_open': {
840851
'from_x': 0.0,

lib/src/cli/tool_handlers/bf_screenshot.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ extension _BfScreenshot on FlutterMcpServer {
99
final quality = (args['quality'] as num?)?.toDouble() ?? 0.8;
1010
final maxWidth = args['max_width'] as int? ?? 800;
1111
final saveToFile =
12-
args['save_to_file'] ?? false; // Return base64 by default for speed
12+
args['save_to_file'] ?? true; // Save to file by default to avoid token overflow
1313

1414
var imageBase64 =
1515
await client!.takeScreenshot(quality: quality, maxWidth: maxWidth);

lib/src/cli/tool_handlers/connection_handlers.dart

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ extension _ConnectionHandlers on FlutterMcpServer {
106106
// Create a new session for this connection
107107
final sessionId = args['session_id'] as String? ?? _generateSessionId();
108108

109+
// Clean up all stale (disconnected) sessions before connecting
110+
await _cleanupStaleSessions();
111+
109112
// If session already exists, disconnect it first
110113
if (_clients.containsKey(sessionId)) {
111114
await _clients[sessionId]!.disconnect();
@@ -152,6 +155,30 @@ extension _ConnectionHandlers on FlutterMcpServer {
152155
lastError = e is Exception ? e : Exception(e.toString());
153156
_clients.remove(sessionId);
154157

158+
// Detect DTD protocol confusion early — no point retrying
159+
if (_looksLikeDtdUri(e.toString())) {
160+
final vmServiceUri = uri.endsWith('/ws') ? uri : '$uri/ws';
161+
return {
162+
"success": false,
163+
"error": {
164+
"code": "E_DTD_PROTOCOL",
165+
"message":
166+
"❌ Detected DTD protocol (not VM Service protocol)\n\n"
167+
"The URI you provided connects to the Dart Tooling Daemon,\n"
168+
"not the VM Service. flutter-skill requires a VM Service URI.\n\n"
169+
"Expected format: ws://host:port/token=/ws\n"
170+
"Your URI: $uri",
171+
},
172+
"uri": uri,
173+
"suggestions": [
174+
"Use the VM Service URI (ends with /ws), not the DTD URI",
175+
"Try: connect_app(uri: '$vmServiceUri')",
176+
"Or let flutter-skill find it: scan_and_connect()",
177+
"Or launch fresh: launch_app(project_path: '.')",
178+
],
179+
};
180+
}
181+
155182
if (attempt < maxRetries) {
156183
// Wait before retry (100ms, 200ms, 400ms)
157184
await Future.delayed(
@@ -441,8 +468,8 @@ extension _ConnectionHandlers on FlutterMcpServer {
441468
}
442469

443470
if (name == 'scan_and_connect') {
444-
final portStart = args['port_start'] ?? 50000;
445-
final portEnd = args['port_end'] ?? 50100;
471+
final portStart = args['port_start'] ?? 48000;
472+
final portEnd = args['port_end'] ?? 65000;
446473
final sessionId = args['session_id'] as String? ?? _generateSessionId();
447474
final scanFlavor = args['flavor'] as String?;
448475
final scanDeviceId = args['device_id'] as String?;
@@ -573,7 +600,8 @@ extension _ConnectionHandlers on FlutterMcpServer {
573600
uri = vmServices.first;
574601
}
575602

576-
// Disconnect old client for this session if exists
603+
// Clean up stale sessions, then disconnect old client for this session
604+
await _cleanupStaleSessions();
577605
if (_clients.containsKey(sessionId)) {
578606
await _clients[sessionId]!.disconnect();
579607
}
@@ -606,6 +634,87 @@ extension _ConnectionHandlers on FlutterMcpServer {
606634
};
607635
}
608636

637+
if (name == 'connect_by_pid') {
638+
final pid = args['pid'] as int;
639+
final sessionId = args['session_id'] as String? ?? _generateSessionId();
640+
641+
// Use lsof to find which TCP port(s) the process is listening on
642+
final lsofResult = await Process.run('lsof', [
643+
'-a', '-p', '$pid', '-i', 'TCP', '-s', 'TCP:LISTEN', '-Fn',
644+
]);
645+
final ports = <int>[];
646+
for (final line in lsofResult.stdout.toString().split('\n')) {
647+
// Lines like: n*:50000
648+
final match = RegExp(r':(\d+)$').firstMatch(line.trim());
649+
if (match != null) {
650+
final p = int.tryParse(match.group(1)!);
651+
if (p != null) ports.add(p);
652+
}
653+
}
654+
655+
if (ports.isEmpty) {
656+
return {
657+
"success": false,
658+
"error": {
659+
"code": "E_PID_NO_PORT",
660+
"message": "No listening TCP ports found for PID $pid",
661+
},
662+
"suggestions": [
663+
"Verify the app is still running: kill -0 $pid",
664+
"Try scan_and_connect() instead",
665+
],
666+
};
667+
}
668+
669+
// Try each port — find the one that speaks VM Service
670+
await _cleanupStaleSessions();
671+
for (final port in ports) {
672+
final vmUris = await _scanVmServices(port, port);
673+
final vmUri = vmUris.isNotEmpty ? vmUris.first : null;
674+
if (vmUri == null) continue;
675+
676+
try {
677+
if (_clients.containsKey(sessionId)) {
678+
await _clients[sessionId]!.disconnect();
679+
}
680+
final client = FlutterSkillClient(vmUri);
681+
await client.connect();
682+
_clients[sessionId] = client;
683+
_sessions[sessionId] = SessionInfo(
684+
id: sessionId,
685+
name: args['name'] as String? ?? 'PID $pid',
686+
projectPath: 'unknown',
687+
deviceId: 'unknown',
688+
port: port,
689+
vmServiceUri: vmUri,
690+
);
691+
_activeSessionId = sessionId;
692+
return {
693+
"success": true,
694+
"message": "Connected via PID $pid",
695+
"pid": pid,
696+
"uri": vmUri,
697+
"session_id": sessionId,
698+
"active_session": true,
699+
};
700+
} catch (_) {
701+
continue;
702+
}
703+
}
704+
705+
return {
706+
"success": false,
707+
"error": {
708+
"code": "E_PID_CONNECT_FAILED",
709+
"message": "Found ports $ports for PID $pid but could not connect to VM Service",
710+
},
711+
"suggestions": [
712+
"The app may not have FlutterSkillBinding initialized",
713+
"Try: scan_and_connect()",
714+
],
715+
};
716+
}
717+
609718
if (name == 'list_running_apps') {
610719
final portStart = args['port_start'] ?? 50000;
611720
final portEnd = args['port_end'] ?? 50100;
@@ -712,4 +821,25 @@ extension _ConnectionHandlers on FlutterMcpServer {
712821

713822
return null; // Not handled by this group
714823
}
824+
825+
/// Remove sessions whose underlying connection is no longer alive.
826+
Future<void> _cleanupStaleSessions() async {
827+
final staleIds = <String>[];
828+
for (final entry in _clients.entries) {
829+
if (!entry.value.isConnected) {
830+
staleIds.add(entry.key);
831+
}
832+
}
833+
for (final id in staleIds) {
834+
try {
835+
await _clients[id]!.disconnect();
836+
} catch (_) {}
837+
_clients.remove(id);
838+
_sessions.remove(id);
839+
if (_activeSessionId == id) _activeSessionId = null;
840+
}
841+
if (_activeSessionId == null && _sessions.isNotEmpty) {
842+
_activeSessionId = _sessions.keys.first;
843+
}
844+
}
715845
}

lib/src/cli/tool_handlers/tool_definitions.dart

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ Omitting session_id in other tools will use the active session.""",
291291
connect to app | find running app | auto-connect | connect to running Flutter | find app | detect app | scan for app | discover app
292292
293293
[PRIMARY PURPOSE]
294-
Automatically scan for and connect to a running Flutter app (scans VM Service ports 50000-50100).
294+
Automatically scan for and connect to a running Flutter app (scans VM Service ports 48000-65000).
295295
296296
[USE WHEN]
297297
• App is already running and you want to connect
@@ -314,11 +314,11 @@ Omitting session_id in other tools will use the active session.""",
314314
"properties": {
315315
"port_start": {
316316
"type": "integer",
317-
"description": "Start of port range (default: 50000)"
317+
"description": "Start of port range (default: 48000)"
318318
},
319319
"port_end": {
320320
"type": "integer",
321-
"description": "End of port range (default: 50100)"
321+
"description": "End of port range (default: 65000)"
322322
},
323323
"project_path": {
324324
"type": "string",
@@ -337,6 +337,40 @@ Omitting session_id in other tools will use the active session.""",
337337
},
338338
},
339339
},
340+
{
341+
"name": "connect_by_pid",
342+
"description": """Connect to a running Flutter app by its process PID.
343+
344+
[USE WHEN]
345+
• You know the app's PID but not the VM Service URI
346+
• You got the PID from 'ps aux', Activity Monitor, or launch_app output
347+
348+
[HOW IT WORKS]
349+
Uses lsof to find which port the process is listening on, then auto-constructs
350+
and connects to the VM Service URI.
351+
352+
[MULTI-SESSION]
353+
Returns a session_id that can be used to target this specific app in subsequent tool calls.""",
354+
"inputSchema": {
355+
"type": "object",
356+
"properties": {
357+
"pid": {
358+
"type": "integer",
359+
"description": "Process ID of the running Flutter app"
360+
},
361+
"session_id": {
362+
"type": "string",
363+
"description":
364+
"Optional session ID (auto-generated if not provided)"
365+
},
366+
"name": {
367+
"type": "string",
368+
"description": "Optional session name for identification"
369+
},
370+
},
371+
"required": ["pid"],
372+
},
373+
},
340374
{
341375
"name": "list_running_apps",
342376
"description":

packaging/homebrew/flutter-skill.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
class FlutterSkill < Formula
22
desc "MCP Server for Flutter app automation - AI Agent control for Flutter apps"
33
homepage "https://github.com/ai-dashboad/flutter-skill"
4-
version "0.9.23"
4+
version "0.9.24"
55
license "MIT"
66

77
# Platform-specific native binaries
88
on_macos do
99
on_arm do
10-
url "https://github.com/ai-dashboad/flutter-skill/releases/download/v0.9.23/flutter-skill-macos-arm64"
10+
url "https://github.com/ai-dashboad/flutter-skill/releases/download/v0.9.24/flutter-skill-macos-arm64"
1111
sha256 "PLACEHOLDER_ARM64_SHA256"
1212
end
1313
on_intel do
14-
url "https://github.com/ai-dashboad/flutter-skill/releases/download/v0.9.23/flutter-skill-macos-x64"
14+
url "https://github.com/ai-dashboad/flutter-skill/releases/download/v0.9.24/flutter-skill-macos-x64"
1515
sha256 "PLACEHOLDER_X64_SHA256"
1616
end
1717
end
1818

1919
on_linux do
20-
url "https://github.com/ai-dashboad/flutter-skill/releases/download/v0.9.23/flutter-skill-linux-x64"
20+
url "https://github.com/ai-dashboad/flutter-skill/releases/download/v0.9.24/flutter-skill-linux-x64"
2121
sha256 "PLACEHOLDER_LINUX_SHA256"
2222
end
2323

@@ -48,7 +48,7 @@ def caveats
4848
Note: Your Flutter app needs to include the flutter_skill package.
4949
Add to pubspec.yaml:
5050
dependencies:
51-
flutter_skill: ^0.9.23
51+
flutter_skill: ^0.9.24
5252
EOS
5353
end
5454

packaging/npm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "flutter-skill",
33
"mcpName": "io.github.ai-dashboad/flutter-skill",
4-
"version": "0.9.23",
4+
"version": "0.9.24",
55
"description": "MCP Server for app automation - Give your AI Agent eyes and hands inside any app (Flutter, React, Web, Native)",
66
"main": "index.js",
77
"bin": {

0 commit comments

Comments
 (0)