@@ -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}
0 commit comments