From eff3be4f7647a38ff431be1e788c825e182fbe41 Mon Sep 17 00:00:00 2001 From: Jhin Lee Date: Mon, 1 Jun 2026 04:42:22 -0400 Subject: [PATCH] Validate icon source URIs --- lib/src/types/content.dart | 36 +++++++++++++++++++++++++++++------- test/types_test.dart | 18 ++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/lib/src/types/content.dart b/lib/src/types/content.dart index ce19f8da..e3bd9966 100644 --- a/lib/src/types/content.dart +++ b/lib/src/types/content.dart @@ -28,6 +28,24 @@ String _readRequiredString(Object? value, String field) { throw FormatException('$field must be a string'); } +bool _isAbsoluteUri(String value) { + return Uri.tryParse(value)?.hasScheme ?? false; +} + +String _readRequiredAbsoluteUriString(Object? value, String field) { + final result = _readRequiredString(value, field); + if (!_isAbsoluteUri(result)) { + throw FormatException('$field must be an absolute URI'); + } + return result; +} + +void _validateAbsoluteUriString(String value, String field) { + if (!_isAbsoluteUri(value)) { + throw ArgumentError.value(value, field, 'must be an absolute URI'); + } +} + String? _readOptionalPresentString( Map json, String key, @@ -266,7 +284,7 @@ class McpIcon { }; return McpIcon( - src: _readRequiredString(json['src'], 'McpIcon.src'), + src: _readRequiredAbsoluteUriString(json['src'], 'McpIcon.src'), mimeType: _readOptionalPresentString( json, 'mimeType', @@ -277,12 +295,16 @@ class McpIcon { ); } - Map toJson() => { - 'src': src, - if (mimeType != null) 'mimeType': mimeType, - if (sizes != null) 'sizes': sizes, - if (theme != null) 'theme': theme!.name, - }; + Map toJson() { + _validateAbsoluteUriString(src, 'McpIcon.src'); + + return { + 'src': src, + if (mimeType != null) 'mimeType': mimeType, + if (sizes != null) 'sizes': sizes, + if (theme != null) 'theme': theme!.name, + }; + } } /// Base class for content parts within prompts or tool results. diff --git a/test/types_test.dart b/test/types_test.dart index 798851d0..9a634387 100644 --- a/test/types_test.dart +++ b/test/types_test.dart @@ -529,6 +529,11 @@ void main() { 'sizes': ['48x48', 'any'], 'theme': 'dark', }); + + final dataIcon = McpIcon.fromJson({ + 'src': 'data:image/png;base64,aWNvbg==', + }); + expect(dataIcon.src, equals('data:image/png;base64,aWNvbg==')); }); test('McpIcon rejects malformed stable wire fields', () { @@ -540,6 +545,8 @@ void main() { expectInvalid({}); expectInvalid({'src': 1}); + expectInvalid({'src': 'icon.png'}); + expectInvalid({'src': '://not-a-uri'}); expectInvalid({'src': 'https://example.com/icon.png', 'mimeType': null}); expectInvalid({'src': 'https://example.com/icon.png', 'mimeType': 1}); expectInvalid({'src': 'https://example.com/icon.png', 'sizes': null}); @@ -553,6 +560,17 @@ void main() { expectInvalid({'src': 'https://example.com/icon.png', 'theme': 'sepia'}); }); + test('McpIcon validates src URI during serialization', () { + expect( + () => const McpIcon(src: 'icon.png').toJson(), + throwsA(isA()), + ); + expect( + const McpIcon(src: 'data:image/png;base64,aWNvbg==').toJson()['src'], + equals('data:image/png;base64,aWNvbg=='), + ); + }); + test('Implementation icon parsing rejects invalid themes', () { expect( () => Implementation.fromJson({