From 846a48004ed30bfc9baf45a0a743a5593cd94303 Mon Sep 17 00:00:00 2001 From: Jhin Lee Date: Mon, 1 Jun 2026 04:50:03 -0400 Subject: [PATCH] Validate implementation wire fields --- lib/src/types/initialization.dart | 112 +++++++++++++++++++++++++----- test/types_test.dart | 95 +++++++++++++++++++++++++ 2 files changed, 191 insertions(+), 16 deletions(-) diff --git a/lib/src/types/initialization.dart b/lib/src/types/initialization.dart index 8ba21bf3..e743be17 100644 --- a/lib/src/types/initialization.dart +++ b/lib/src/types/initialization.dart @@ -18,6 +18,69 @@ Map? _asJsonObject(dynamic value) { throw FormatException('Expected object capability, got ${value.runtimeType}'); } +String _readRequiredString(Object? value, String field) { + if (value is String) { + return value; + } + throw FormatException('$field must be a string'); +} + +String? _readOptionalPresentString( + Map json, + String key, + String field, +) { + if (!json.containsKey(key)) { + return null; + } + return _readRequiredString(json[key], field); +} + +bool _isAbsoluteUri(String value) { + return Uri.tryParse(value)?.hasScheme ?? false; +} + +String? _readOptionalPresentUriString( + Map json, + String key, + String field, +) { + final value = _readOptionalPresentString(json, key, field); + if (value == null) { + return null; + } + if (!_isAbsoluteUri(value)) { + throw FormatException('$field must be an absolute URI'); + } + return value; +} + +void _validateAbsoluteUriString(String value, String field) { + if (!_isAbsoluteUri(value)) { + throw ArgumentError.value(value, field, 'must be an absolute URI'); + } +} + +List? _readOptionalIconList( + Map json, + String key, + String field, +) { + if (!json.containsKey(key)) { + return null; + } + + final value = json[key]; + if (value is! List) { + throw FormatException('$field must be a list of objects'); + } + + return [ + for (var i = 0; i < value.length; i++) + McpIcon.fromJson(readJsonObject(value[i], '$field[$i]')), + ]; +} + Map? _asStrictJsonObject(Object? value, String field) { if (value == null) { return null; @@ -150,25 +213,42 @@ class Implementation { factory Implementation.fromJson(Map json) { return Implementation( - name: json['name'] as String, - title: json['title'] as String?, - version: json['version'] as String, - description: json['description'] as String?, - icons: (json['icons'] as List?) - ?.map((e) => McpIcon.fromJson(e as Map)) - .toList(), - websiteUrl: json['websiteUrl'] as String?, + name: _readRequiredString(json['name'], 'Implementation.name'), + title: _readOptionalPresentString( + json, + 'title', + 'Implementation.title', + ), + version: _readRequiredString(json['version'], 'Implementation.version'), + description: _readOptionalPresentString( + json, + 'description', + 'Implementation.description', + ), + icons: _readOptionalIconList(json, 'icons', 'Implementation.icons'), + websiteUrl: _readOptionalPresentUriString( + json, + 'websiteUrl', + 'Implementation.websiteUrl', + ), ); } - Map toJson() => { - 'name': name, - if (title != null) 'title': title, - 'version': version, - if (description != null) 'description': description, - if (icons != null) 'icons': icons?.map((e) => e.toJson()).toList(), - if (websiteUrl != null) 'websiteUrl': websiteUrl, - }; + Map toJson() { + final websiteUrl = this.websiteUrl; + if (websiteUrl != null) { + _validateAbsoluteUriString(websiteUrl, 'Implementation.websiteUrl'); + } + + return { + 'name': name, + if (title != null) 'title': title, + 'version': version, + if (description != null) 'description': description, + if (icons != null) 'icons': icons?.map((e) => e.toJson()).toList(), + if (websiteUrl != null) 'websiteUrl': websiteUrl, + }; + } } /// Describes capabilities related to root resources (e.g., workspace folders). diff --git a/test/types_test.dart b/test/types_test.dart index 9a634387..644ae92e 100644 --- a/test/types_test.dart +++ b/test/types_test.dart @@ -587,6 +587,101 @@ void main() { ); }); + test('Implementation parses stable wire fields', () { + final implementation = Implementation.fromJson({ + 'name': 'test-client', + 'title': 'Test Client', + 'version': '1.0.0', + 'description': 'A test MCP client', + 'icons': [ + { + 'src': 'https://example.com/icon.png', + 'theme': 'light', + }, + ], + 'websiteUrl': 'https://example.com', + }); + + expect(implementation.name, equals('test-client')); + expect(implementation.title, equals('Test Client')); + expect(implementation.version, equals('1.0.0')); + expect(implementation.description, equals('A test MCP client')); + expect(implementation.icons!.single.theme, equals(IconTheme.light)); + expect(implementation.websiteUrl, equals('https://example.com')); + expect(implementation.toJson(), { + 'name': 'test-client', + 'title': 'Test Client', + 'version': '1.0.0', + 'description': 'A test MCP client', + 'icons': [ + { + 'src': 'https://example.com/icon.png', + 'theme': 'light', + }, + ], + 'websiteUrl': 'https://example.com', + }); + }); + + test('Implementation rejects malformed stable wire fields', () { + void expectInvalid(Map json) { + expect( + () => Implementation.fromJson(json), + throwsA(isA()), + ); + } + + expectInvalid({}); + expectInvalid({'name': 'test-client'}); + expectInvalid({'name': 1, 'version': '1.0.0'}); + expectInvalid({'name': 'test-client', 'version': 1}); + expectInvalid({'name': 'test-client', 'version': '1.0.0', 'title': null}); + expectInvalid({'name': 'test-client', 'version': '1.0.0', 'title': 1}); + expectInvalid({ + 'name': 'test-client', + 'version': '1.0.0', + 'description': null, + }); + expectInvalid({ + 'name': 'test-client', + 'version': '1.0.0', + 'description': 1, + }); + expectInvalid({'name': 'test-client', 'version': '1.0.0', 'icons': null}); + expectInvalid({'name': 'test-client', 'version': '1.0.0', 'icons': {}}); + expectInvalid({ + 'name': 'test-client', + 'version': '1.0.0', + 'icons': [null], + }); + expectInvalid({ + 'name': 'test-client', + 'version': '1.0.0', + 'websiteUrl': null, + }); + expectInvalid({ + 'name': 'test-client', + 'version': '1.0.0', + 'websiteUrl': 1, + }); + expectInvalid({ + 'name': 'test-client', + 'version': '1.0.0', + 'websiteUrl': 'example.com', + }); + }); + + test('Implementation validates website URL during serialization', () { + expect( + () => const Implementation( + name: 'test-client', + version: '1.0.0', + websiteUrl: 'example.com', + ).toJson(), + throwsA(isA()), + ); + }); + test('ImageContent supports annotations and meta', () { final content = const ImageContent( data: 'base64data',