From cb4a6af42ab357dd4fff4a0dd7ad84b8ed055334 Mon Sep 17 00:00:00 2001 From: arenukvern Date: Mon, 24 Aug 2026 22:21:02 +0300 Subject: [PATCH 1/3] feat: dtd handshake --- .../vm_connections/connection_context.dart | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/mcp_server_dart/lib/src/shared_core/vm_connections/connection_context.dart b/mcp_server_dart/lib/src/shared_core/vm_connections/connection_context.dart index e52cedbb..49e423e4 100644 --- a/mcp_server_dart/lib/src/shared_core/vm_connections/connection_context.dart +++ b/mcp_server_dart/lib/src/shared_core/vm_connections/connection_context.dart @@ -613,10 +613,22 @@ final class ConnectionContext { ); try { - final dtdFuture = DartToolingDaemon.connect(wsUri); - _dartToolingDaemon = timeout == Duration.zero - ? await dtdFuture - : await dtdFuture.timeout(timeout); + // DDS endpoints do not speak the DTD protocol, so a failed DTD + // handshake must not block VM-service connections. DTD-dependent + // features degrade gracefully when this remains null. + try { + final dtdFuture = DartToolingDaemon.connect(wsUri); + _dartToolingDaemon = timeout == Duration.zero + ? await dtdFuture + : await dtdFuture.timeout(timeout); + } on Exception catch (dtdError) { + logger( + LoggingLevel.warning, + 'DTD unavailable at $wsUri: $dtdError', + logger: 'ConnectionContext', + ); + _dartToolingDaemon = null; + } _vmChannel = WebSocketChannel.connect(wsUri); From 40b4b1198c2e491fce497afb8a62df9c653a693c Mon Sep 17 00:00:00 2001 From: arenukvern Date: Tue, 8 Sep 2026 02:09:17 +0300 Subject: [PATCH 2/3] fix: await websocket upgrade and guard sink close in VM connect Await WebSocketChannel.ready so connection failures surface as normal errors from _connectToEndpoint instead of unhandled async errors, and bound sink.close() in disconnect() since it never completes when the websocket upgrade failed. This keeps DTD graceful-degradation correct when the VM endpoint is unreachable or not a websocket endpoint. --- .../vm_connections/connection_context.dart | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/mcp_server_dart/lib/src/shared_core/vm_connections/connection_context.dart b/mcp_server_dart/lib/src/shared_core/vm_connections/connection_context.dart index 9dbf66fa..79048f18 100644 --- a/mcp_server_dart/lib/src/shared_core/vm_connections/connection_context.dart +++ b/mcp_server_dart/lib/src/shared_core/vm_connections/connection_context.dart @@ -631,7 +631,13 @@ final class ConnectionContext { await _vmService?.dispose(); } if (_vmChannel != null) { - await _vmChannel?.sink.close(); + // sink.close() never completes when the websocket upgrade failed, + // so bound the wait and swallow late errors to keep disconnect() + // best-effort and non-blocking. + await _vmChannel?.sink.close().catchError((final _) {}).timeout( + const Duration(seconds: 1), + onTimeout: () => null, + ); } } catch (e) { logger( @@ -679,6 +685,19 @@ final class ConnectionContext { _vmChannel = WebSocketChannel.connect(wsUri); + // Await the websocket upgrade so connection failures (refused, + // non-websocket endpoint) surface here as a normal error instead of + // escaping as an unhandled async error from the channel. + final vmReady = _vmChannel!.ready; + // Swallow a late error if the timeout below fires first so it does + // not escape as an unhandled async error. + unawaited(vmReady.catchError((final _) {})); + if (timeout == Duration.zero) { + await vmReady; + } else { + await vmReady.timeout(timeout); + } + _vmService = VmService( _vmChannel!.stream.cast(), (final message) => _vmChannel!.sink.add(message), From 39d187ed2797c6d90bad0ef678c373abfaf8fb3e Mon Sep 17 00:00:00 2001 From: arenukvern Date: Tue, 8 Sep 2026 02:26:56 +0300 Subject: [PATCH 3/3] refactor: manage DTD websocket channel explicitly and redact auth token from logs - Own the DTD WebSocketChannel so a failed/timeout handshake closes the sink (bounded) instead of leaking the socket, then build the daemon via DartToolingDaemon.fromStreamChannel (equivalent to DTD.connect). - Redact the VM-service URI from the DTD-unavailable warning: log only host and port plus the sanitized error type, since the URI path carries an auth token (CWE-532). - Explicitly type the new future variables (Future). --- .../vm_connections/connection_context.dart | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/mcp_server_dart/lib/src/shared_core/vm_connections/connection_context.dart b/mcp_server_dart/lib/src/shared_core/vm_connections/connection_context.dart index 79048f18..de92ce82 100644 --- a/mcp_server_dart/lib/src/shared_core/vm_connections/connection_context.dart +++ b/mcp_server_dart/lib/src/shared_core/vm_connections/connection_context.dart @@ -669,15 +669,35 @@ final class ConnectionContext { // DDS endpoints do not speak the DTD protocol, so a failed DTD // handshake must not block VM-service connections. DTD-dependent // features degrade gracefully when this remains null. + final dtdChannel = WebSocketChannel.connect(wsUri); try { - final dtdFuture = DartToolingDaemon.connect(wsUri); - _dartToolingDaemon = timeout == Duration.zero - ? await dtdFuture - : await dtdFuture.timeout(timeout); + final Future dtdReady = dtdChannel.ready; + // Swallow a late error if the timeout below fires first so it does + // not escape as an unhandled async error. + unawaited(dtdReady.catchError((final _) {})); + if (timeout == Duration.zero) { + await dtdReady; + } else { + await dtdReady.timeout(timeout); + } + _dartToolingDaemon = DartToolingDaemon.fromStreamChannel( + dtdChannel.cast(), + ); } on Exception catch (dtdError) { + // Bound the close: sink.close() never completes when the upgrade + // failed, and abandoning the channel would leak the socket. + unawaited( + dtdChannel.sink.close().catchError((final _) {}).timeout( + const Duration(seconds: 1), + onTimeout: () => null, + ), + ); logger( LoggingLevel.warning, - 'DTD unavailable at $wsUri: $dtdError', + // The VM-service URI path carries an auth token, so only the + // host, port, and sanitized error type are logged. + 'DTD unavailable at ${wsUri.host}:${wsUri.port} ' + '(${dtdError.runtimeType})', logger: 'ConnectionContext', ); _dartToolingDaemon = null; @@ -688,7 +708,7 @@ final class ConnectionContext { // Await the websocket upgrade so connection failures (refused, // non-websocket endpoint) surface here as a normal error instead of // escaping as an unhandled async error from the channel. - final vmReady = _vmChannel!.ready; + final Future vmReady = _vmChannel!.ready; // Swallow a late error if the timeout below fires first so it does // not escape as an unhandled async error. unawaited(vmReady.catchError((final _) {}));