connect method
FR-CONN-001~003, 010
Implementation
Future<ConnectionResult> connect(ServerConfig server) async {
final existing = _connections[server.id];
if (existing != null) {
if (existing.state == ConnectionState.connected) {
_logger.debug('Reusing connection', {'serverId': server.id});
return ConnectionResult.success(existing);
}
if (existing.state == ConnectionState.connecting) {
_logger.debug('Awaiting in-flight connection',
{'serverId': server.id});
return _waitForConnection(server.id);
}
}
_logger.debug('Creating connection', {'serverId': server.id});
final info = ConnectionInfo(
serverId: server.id,
serverName: server.name,
serverConfig: server,
state: ConnectionState.connecting,
);
_connections[server.id] = info;
notifyListeners();
try {
final transport = _transportFactory.create(server);
final client = await _connector(transport);
info.client = client;
info.state = ConnectionState.connected;
info.connectedAt = DateTime.now();
// Liveness: when the transport drops on its own — BLE supervision
// timeout, a server closing the socket — mcp_client fires onDisconnect.
// Without reacting, this entry stays `connected` forever: the launcher
// badge stays lit and a retry reuses the dead client (readResource hangs
// on a link that is gone) instead of dialing again. React by dropping
// the entry so hasConnection()/isServerConnected() tell the truth and the
// next connect() starts fresh.
info.disconnectSub = client.onDisconnect.listen(
(reason) => _handleTransportDrop(server.id, reason),
);
notifyListeners();
_logger.info('Connected', {'serverId': server.id});
return ConnectionResult.success(info);
} catch (e, st) {
info.state = ConnectionState.error;
info.error = e.toString();
notifyListeners();
_logger.logError('Connect failed', e, st, {'serverId': server.id});
return ConnectionResult.failure(e.toString());
}
}