From e4c6e6a522689cc5a439b749f12f1eeca007b84f Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Mon, 8 Dec 2025 11:31:25 -0800 Subject: [PATCH 1/4] Register with a unique client ID --- pkgs/dart_mcp_server/lib/src/mixins/dtd.dart | 22 ++++++------ pkgs/dart_mcp_server/lib/src/utils/uuid.dart | 23 +++++++++++++ pkgs/dart_mcp_server/test/tools/dtd_test.dart | 34 ++++++++++++++----- 3 files changed, 60 insertions(+), 19 deletions(-) create mode 100644 pkgs/dart_mcp_server/lib/src/utils/uuid.dart diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart index 24916fcc..c633cf47 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart @@ -19,6 +19,7 @@ import '../arg_parser.dart'; import '../utils/analytics.dart'; import '../utils/constants.dart'; import '../utils/tools_configuration.dart'; +import '../utils/uuid.dart'; /// Constants used by the MCP server to register services on DTD. /// @@ -290,20 +291,19 @@ base mixin DartToolingDaemonSupport final dtd = _dtd!; if (clientCapabilities.sampling != null) { - try { - await dtd.registerService( - McpServiceConstants.serviceName, - McpServiceConstants.samplingRequest, - _handleSamplingRequest, - ); - } on RpcException catch (e) { - // It is expected for there to be an exception if the sampling service - // was already registered by another Dart MCP Server. - if (e.code != RpcErrorCodes.kServiceAlreadyRegistered) rethrow; - } + await dtd.registerService( + '${McpServiceConstants.serviceName}-${_generateUniqueClientId()}', + McpServiceConstants.samplingRequest, + _handleSamplingRequest, + ); } } + String _generateUniqueClientId() { + final sanitizedClientName = clientInfo.name.replaceAll(RegExp(r'\s+'), '-'); + return '$sanitizedClientName-${generateShortUUID()}'; + } + Future> _handleSamplingRequest(Parameters params) async { final result = await createMessage( CreateMessageRequest.fromMap(params.asMap.cast()), diff --git a/pkgs/dart_mcp_server/lib/src/utils/uuid.dart b/pkgs/dart_mcp_server/lib/src/utils/uuid.dart new file mode 100644 index 00000000..5f04febd --- /dev/null +++ b/pkgs/dart_mcp_server/lib/src/utils/uuid.dart @@ -0,0 +1,23 @@ +// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:math' show Random; + +/// Generates a short, 8-character hex string from 32 bits of random data. +/// +/// This is not a standard UUID but is sufficient for use cases where a short, +/// unique-enough identifier is needed. +String generateShortUUID() => _bitsDigits(16, 4) + _bitsDigits(16, 4); + +// Note: The following private helpers were copied over from: +// https://github.com/dart-lang/webdev/blob/e2d14f1050fa07e9a60455cf9d2b8e6f4e9c332c/frontend_server_common/lib/src/uuid.dart +final Random _random = Random(); + +String _bitsDigits(int bitCount, int digitCount) => + _printDigits(_generateBits(bitCount), digitCount); + +int _generateBits(int bitCount) => _random.nextInt(1 << bitCount); + +String _printDigits(int value, int count) => + value.toRadixString(16).padLeft(count, '0'); diff --git a/pkgs/dart_mcp_server/test/tools/dtd_test.dart b/pkgs/dart_mcp_server/test/tools/dtd_test.dart index 3e842265..9e1ff664 100644 --- a/pkgs/dart_mcp_server/test/tools/dtd_test.dart +++ b/pkgs/dart_mcp_server/test/tools/dtd_test.dart @@ -107,10 +107,21 @@ void main() { return (responseContent['text'] as String).split('\n'); } + Future getSamplingServiceName( + DartToolingDaemon dtdClient, + ) async { + final services = await dtdClient.getRegisteredServices(); + final samplingService = services.clientServices.firstWhere( + (s) => s.name.startsWith(McpServiceConstants.serviceName), + ); + return samplingService.name; + } + test('can make a sampling request with text', () async { final dtdClient = testHarness.fakeEditorExtension.dtd; + final samplingServiceName = await getSamplingServiceName(dtdClient); final response = await dtdClient.call( - McpServiceConstants.serviceName, + samplingServiceName, McpServiceConstants.samplingRequest, params: { 'messages': [ @@ -130,8 +141,9 @@ void main() { test('can make a sampling request with an image', () async { final dtdClient = testHarness.fakeEditorExtension.dtd; + final samplingServiceName = await getSamplingServiceName(dtdClient); final response = await dtdClient.call( - McpServiceConstants.serviceName, + samplingServiceName, McpServiceConstants.samplingRequest, params: { 'messages': [ @@ -155,8 +167,9 @@ void main() { test('can make a sampling request with audio', () async { final dtdClient = testHarness.fakeEditorExtension.dtd; + final samplingServiceName = await getSamplingServiceName(dtdClient); final response = await dtdClient.call( - McpServiceConstants.serviceName, + samplingServiceName, McpServiceConstants.samplingRequest, params: { 'messages': [ @@ -177,8 +190,9 @@ void main() { test('can make a sampling request with an embedded resource', () async { final dtdClient = testHarness.fakeEditorExtension.dtd; + final samplingServiceName = await getSamplingServiceName(dtdClient); final response = await dtdClient.call( - McpServiceConstants.serviceName, + samplingServiceName, McpServiceConstants.samplingRequest, params: { 'messages': [ @@ -201,8 +215,9 @@ void main() { test('can make a sampling request with mixed content', () async { final dtdClient = testHarness.fakeEditorExtension.dtd; + final samplingServiceName = await getSamplingServiceName(dtdClient); final response = await dtdClient.call( - McpServiceConstants.serviceName, + samplingServiceName, McpServiceConstants.samplingRequest, params: { 'messages': [ @@ -231,8 +246,9 @@ void main() { test('can handle user and assistant messages', () async { final dtdClient = testHarness.fakeEditorExtension.dtd; + final samplingServiceName = await getSamplingServiceName(dtdClient); final response = await dtdClient.call( - McpServiceConstants.serviceName, + samplingServiceName, McpServiceConstants.samplingRequest, params: { 'messages': [ @@ -262,8 +278,9 @@ void main() { test('forwards all messages, even those with unknown types', () async { final dtdClient = testHarness.fakeEditorExtension.dtd; + final samplingServiceName = await getSamplingServiceName(dtdClient); final response = await dtdClient.call( - McpServiceConstants.serviceName, + samplingServiceName, McpServiceConstants.samplingRequest, params: { 'messages': [ @@ -285,9 +302,10 @@ void main() { test('throws for invalid requests', () async { final dtdClient = testHarness.fakeEditorExtension.dtd; + final samplingServiceName = await getSamplingServiceName(dtdClient); try { await dtdClient.call( - McpServiceConstants.serviceName, + samplingServiceName, McpServiceConstants.samplingRequest, params: { 'messages': [ From 29a2a50c36a778beaed621c576e1b91a64ed82ba Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:12:37 -0800 Subject: [PATCH 2/4] Test and clean up --- pkgs/dart_mcp_server/lib/src/mixins/dtd.dart | 22 ++++++++++++++----- pkgs/dart_mcp_server/test/tools/dtd_test.dart | 20 +++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart index c633cf47..c4b68531 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart @@ -79,6 +79,21 @@ base mixin DartToolingDaemonSupport /// Whether or not to enable the screenshot tool. bool get enableScreenshots; + /// A unique identifier for this server instance. + /// + /// This is generated on first access and then cached. It is used to create + /// a unique service name when registering services on DTD. + /// + /// Can only be accessed after `initialize` has been called. + String get clientId { + if (_clientId != null) return _clientId!; + final sanitizedClientName = clientInfo.name.replaceAll(RegExp(r'\s+'), '-'); + _clientId = '$sanitizedClientName-${generateShortUUID()}'; + return _clientId!; + } + + String? _clientId; + /// Called when the DTD connection is lost, resets all associated state. Future _resetDtd() async { _dtd = null; @@ -292,18 +307,13 @@ base mixin DartToolingDaemonSupport if (clientCapabilities.sampling != null) { await dtd.registerService( - '${McpServiceConstants.serviceName}-${_generateUniqueClientId()}', + '${McpServiceConstants.serviceName}-$clientId', McpServiceConstants.samplingRequest, _handleSamplingRequest, ); } } - String _generateUniqueClientId() { - final sanitizedClientName = clientInfo.name.replaceAll(RegExp(r'\s+'), '-'); - return '$sanitizedClientName-${generateShortUUID()}'; - } - Future> _handleSamplingRequest(Parameters params) async { final result = await createMessage( CreateMessageRequest.fromMap(params.asMap.cast()), diff --git a/pkgs/dart_mcp_server/test/tools/dtd_test.dart b/pkgs/dart_mcp_server/test/tools/dtd_test.dart index 9e1ff664..0dd55d67 100644 --- a/pkgs/dart_mcp_server/test/tools/dtd_test.dart +++ b/pkgs/dart_mcp_server/test/tools/dtd_test.dart @@ -117,6 +117,26 @@ void main() { return samplingService.name; } + test('is registered with correct name format', () async { + final dtdClient = testHarness.fakeEditorExtension.dtd; + final services = await dtdClient.getRegisteredServices(); + final samplingService = services.clientServices.first; + final sanitizedClientName = + 'test-client-for-the-dart-tooling-mcp-server'; + expect( + samplingService.name, + startsWith( + '${McpServiceConstants.serviceName}-$sanitizedClientName-', + ), + ); + // Check that the service name ends with an 8-character ID. + expect(samplingService.name, matches(RegExp(r'[a-f0-9]{8}$'))); + expect( + samplingService.methods, + contains(McpServiceConstants.samplingRequest), + ); + }); + test('can make a sampling request with text', () async { final dtdClient = testHarness.fakeEditorExtension.dtd; final samplingServiceName = await getSamplingServiceName(dtdClient); From f55d220124518836d956bfe7131d45401e995dc2 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:15:54 -0800 Subject: [PATCH 3/4] Small fix --- pkgs/dart_mcp_server/lib/src/mixins/dtd.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart index c4b68531..1b3de38b 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart @@ -87,7 +87,8 @@ base mixin DartToolingDaemonSupport /// Can only be accessed after `initialize` has been called. String get clientId { if (_clientId != null) return _clientId!; - final sanitizedClientName = clientInfo.name.replaceAll(RegExp(r'\s+'), '-'); + final clientName = clientInfo.title ?? clientInfo.name; + final sanitizedClientName = clientName.replaceAll(RegExp(r'\s+'), '-'); _clientId = '$sanitizedClientName-${generateShortUUID()}'; return _clientId!; } From 2d6660e7fb8664394a1c238cebc35863749043f6 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:01:02 -0800 Subject: [PATCH 4/4] Sanitize client ID --- pkgs/dart_mcp_server/lib/src/mixins/dtd.dart | 18 +++++- pkgs/dart_mcp_server/test/tools/dtd_test.dart | 63 ++++++++++++++++++- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart index 1b3de38b..55eb1523 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart @@ -88,13 +88,25 @@ base mixin DartToolingDaemonSupport String get clientId { if (_clientId != null) return _clientId!; final clientName = clientInfo.title ?? clientInfo.name; - final sanitizedClientName = clientName.replaceAll(RegExp(r'\s+'), '-'); - _clientId = '$sanitizedClientName-${generateShortUUID()}'; + _clientId = generateClientId(clientName); return _clientId!; } String? _clientId; + @visibleForTesting + String generateClientId(String clientName) { + // Sanitizes the client name by: + // 1. replacing whitespace, '-', and '.' with '_' + // 2. removing all non-alphanumeric characters except '_' + final sanitizedClientName = clientName + .trim() + .toLowerCase() + .replaceAll(RegExp(r'[\s\.\-]+'), '_') + .replaceAll(RegExp(r'[^a-zA-Z0-9_]'), ''); + return '${sanitizedClientName}_${generateShortUUID()}'; + } + /// Called when the DTD connection is lost, resets all associated state. Future _resetDtd() async { _dtd = null; @@ -308,7 +320,7 @@ base mixin DartToolingDaemonSupport if (clientCapabilities.sampling != null) { await dtd.registerService( - '${McpServiceConstants.serviceName}-$clientId', + '${McpServiceConstants.serviceName}_$clientId', McpServiceConstants.samplingRequest, _handleSamplingRequest, ); diff --git a/pkgs/dart_mcp_server/test/tools/dtd_test.dart b/pkgs/dart_mcp_server/test/tools/dtd_test.dart index 0dd55d67..febcfe04 100644 --- a/pkgs/dart_mcp_server/test/tools/dtd_test.dart +++ b/pkgs/dart_mcp_server/test/tools/dtd_test.dart @@ -122,11 +122,11 @@ void main() { final services = await dtdClient.getRegisteredServices(); final samplingService = services.clientServices.first; final sanitizedClientName = - 'test-client-for-the-dart-tooling-mcp-server'; + 'test_client_for_the_dart_tooling_mcp_server'; expect( samplingService.name, startsWith( - '${McpServiceConstants.serviceName}-$sanitizedClientName-', + '${McpServiceConstants.serviceName}_${sanitizedClientName}_', ), ); // Check that the service name ends with an 8-character ID. @@ -416,6 +416,65 @@ void main() { await testHarness.connectToDtd(); }); + group('generateClientId creates ID from client name', () { + test('removes whitespaces', () { + // Single whitespace character. + expect( + server.generateClientId('Example Name'), + startsWith('example_name_'), + ); + // Multiple whitespace characters. + expect( + server.generateClientId('Example Name'), + startsWith('example_name_'), + ); + // Newline and other whitespace. + expect( + server.generateClientId('Example\n\tName'), + startsWith('example_name_'), + ); + // Whitespace at the end. + expect( + server.generateClientId('Example Name\n'), + startsWith('example_name_'), + ); + }); + + test('replaces periods and dashes with underscores', () { + // Replaces periods. + expect( + server.generateClientId('Example.Client.Name'), + startsWith('example_client_name_'), + ); + // Replaces dashes. + expect( + server.generateClientId('example-client-name'), + startsWith('example_client_name_'), + ); + }); + + test('removes special characters', () { + expect( + server.generateClientId('Example!@#Client\$%^Name'), + startsWith('exampleclientname_'), + ); + }); + + test('handles a mix of sanitization rules', () { + expect( + server.generateClientId(' Example Client.Name!@# '), + startsWith('example_client_name_'), + ); + }); + + test('ends with an 8-character uuid', () { + expect( + server.generateClientId('Example name'), + matches(RegExp(r'[a-f0-9]{8}$')), + ); + }); + }); + group('$VmService management', () { late Directory appDir; final appPath = 'bin/main.dart';