From 395f1e3066b84c2cc4f13a4b405b4e361a597eb2 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 25 Jun 2025 10:38:14 -0700 Subject: [PATCH 1/4] Add Enum support to Schema --- pkgs/dart_mcp/lib/src/api/tools.dart | 63 ++++++++++++++++++++++++++ pkgs/dart_mcp/test/api/tools_test.dart | 35 ++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/pkgs/dart_mcp/lib/src/api/tools.dart b/pkgs/dart_mcp/lib/src/api/tools.dart index 6b90833b..7400094e 100644 --- a/pkgs/dart_mcp/lib/src/api/tools.dart +++ b/pkgs/dart_mcp/lib/src/api/tools.dart @@ -196,6 +196,7 @@ enum JsonType { num('number'), int('integer'), bool('boolean'), + enumeration('enum'), nil('null'); const JsonType(this.typeName); @@ -238,6 +239,9 @@ enum ValidationErrorType { maxLengthExceeded, patternMismatch, + // Enum specific + enumValueNotAllowed, + // Number/Integer specific minimumNotMet, maximumExceeded, @@ -334,6 +338,9 @@ extension type Schema.fromMap(Map _value) { /// Alias for [ObjectSchema.new]. static const object = ObjectSchema.new; + /// Alias for [EnumSchema.new]. + static const enumeration = EnumSchema.new; + /// Alias for [NullSchema.new]. static const nil = NullSchema.new; @@ -424,6 +431,12 @@ extension SchemaValidation on Schema { currentPath, accumulatedFailures, ); + case JsonType.enumeration: + isValid = (this as EnumSchema)._validateEnum( + data, + currentPath, + accumulatedFailures, + ); case JsonType.bool: if (data is! bool) { isValid = false; @@ -1081,6 +1094,56 @@ extension type const StringSchema.fromMap(Map _value) } } +/// A JSON Schema definition for a set of allowed string values. +extension type EnumSchema.fromMap(Map _value) + implements Schema { + factory EnumSchema({ + String? title, + String? description, + required Set values, + }) => EnumSchema.fromMap({ + 'type': JsonType.enumeration.typeName, + if (title != null) 'title': title, + if (description != null) 'description': description, + 'enum': values.toSet(), + }); + + /// A title for this schema, should be short. + String? get title => _value['title'] as String?; + + /// A description of this schema. + String? get description => _value['description'] as String?; + + /// The allowed enum values. + Set get values => (_value['enum'] as Set).cast(); + + bool _validateEnum( + Object? data, + List currentPath, + HashSet accumulatedFailures, + ) { + if (data is! String) { + accumulatedFailures.add( + ValidationError(ValidationErrorType.typeMismatch, path: currentPath), + ); + return false; + } + if (!values.contains(data)) { + accumulatedFailures.add( + ValidationError( + ValidationErrorType.enumValueNotAllowed, + path: currentPath, + details: + 'String "$data" is not one of the allowed values: ' + '${values.join(', ')}', + ), + ); + return false; + } + return true; + } +} + /// A JSON Schema definition for a [num]. extension type NumberSchema.fromMap(Map _value) implements Schema { diff --git a/pkgs/dart_mcp/test/api/tools_test.dart b/pkgs/dart_mcp/test/api/tools_test.dart index 0e9e0a96..21bfb744 100644 --- a/pkgs/dart_mcp/test/api/tools_test.dart +++ b/pkgs/dart_mcp/test/api/tools_test.dart @@ -199,6 +199,20 @@ void main() { }); }); + test('EnumSchema', () { + final schema = EnumSchema( + title: 'Foo', + description: 'Bar', + values: {'a', 'b', 'c'}, + ); + expect(schema, { + 'type': 'enum', + 'title': 'Foo', + 'description': 'Bar', + 'enum': ['a', 'b', 'c'], + }); + }); + test('Schema', () { final schema = Schema.combined( type: JsonType.bool, @@ -830,6 +844,27 @@ void main() { }); }); + group('Enum Specific', () { + test('enumValueNotAllowed', () { + final schema = EnumSchema(values: {'a', 'b'}); + expectFailuresMatch(schema, 'c', [ + ValidationError(ValidationErrorType.enumValueNotAllowed), + ]); + }); + + test('valid enum value', () { + final schema = EnumSchema(values: {'a', 'b'}); + expectFailuresMatch(schema, 'a', []); + }); + + test('enum with non-string data', () { + final schema = EnumSchema(values: {'a', 'b'}); + expectFailuresMatch(schema, 1, [ + ValidationError(ValidationErrorType.typeMismatch), + ]); + }); + }); + group('Schema Combinators', () { test('allOfNotMet - one sub-schema fails', () { final schema = Schema.combined( From 5fb652b4884d5bf603850e618bddd6c062931f08 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 25 Jun 2025 10:49:57 -0700 Subject: [PATCH 2/4] Update CHANGELOG.md --- pkgs/dart_mcp/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/pkgs/dart_mcp/CHANGELOG.md b/pkgs/dart_mcp/CHANGELOG.md index 011464e9..08eb159c 100644 --- a/pkgs/dart_mcp/CHANGELOG.md +++ b/pkgs/dart_mcp/CHANGELOG.md @@ -2,6 +2,7 @@ - Added error checking to required fields of all `Request` subclasses so that they will throw helpful errors when accessed and not set. +- Added enum support to Schema. ## 0.2.2 From 18d42e826deda923d0ccd69214bef711a31b577b Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 25 Jun 2025 13:09:21 -0700 Subject: [PATCH 3/4] Review changes --- pkgs/dart_mcp/lib/src/api/tools.dart | 55 ++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/pkgs/dart_mcp/lib/src/api/tools.dart b/pkgs/dart_mcp/lib/src/api/tools.dart index 7400094e..614828e2 100644 --- a/pkgs/dart_mcp/lib/src/api/tools.dart +++ b/pkgs/dart_mcp/lib/src/api/tools.dart @@ -29,7 +29,13 @@ extension type ListToolsResult.fromMap(Map _value) if (meta != null) '_meta': meta, }); - List get tools => (_value['tools'] as List).cast(); + List get tools { + final tools = (_value['tools'] as List?)?.cast(); + if (tools == null) { + throw ArgumentError('Missing tools field in $ListToolsResult'); + } + return tools; + } } /// The server's response to a tool call. @@ -56,7 +62,13 @@ extension type CallToolResult.fromMap(Map _value) /// The type of content, either [TextContent], [ImageContent], /// or [EmbeddedResource], - List get content => (_value['content'] as List).cast(); + List get content { + final content = (_value['content'] as List?)?.cast(); + if (content == null) { + throw ArgumentError('Missing content field in $CallToolResult'); + } + return content; + } /// Whether the tool call ended in an error. /// @@ -129,14 +141,26 @@ extension type Tool.fromMap(Map _value) { as ToolAnnotations?; /// The name of the tool. - String get name => _value['name'] as String; + String get name { + final name = _value['name'] as String?; + if (name == null) { + throw ArgumentError('Missing name field in $Tool'); + } + return name; + } /// A human-readable description of the tool. String? get description => _value['description'] as String?; /// A JSON [ObjectSchema] object defining the expected parameters for the /// tool. - ObjectSchema get inputSchema => _value['inputSchema'] as ObjectSchema; + ObjectSchema get inputSchema { + final inputSchema = _value['inputSchema'] as ObjectSchema?; + if (inputSchema == null) { + throw ArgumentError('Missing inputSchema field in $Tool'); + } + return inputSchema; + } } /// Additional properties describing a Tool to clients. @@ -300,6 +324,13 @@ extension type ValidationError.fromMap(Map _value) { /// if you need something more complex you can create your own /// `Map` and cast it to [Schema] (or [ObjectSchema]) directly. extension type Schema.fromMap(Map _value) { + factory Schema({JsonType? type, String? title, String? description}) => + Schema.fromMap({ + 'type': JsonType.enumeration.typeName, + if (title != null) 'title': title, + if (description != null) 'description': description, + }); + /// A combined schema, see /// https://json-schema.org/understanding-json-schema/reference/combining#schema-composition factory Schema.combined({ @@ -1100,12 +1131,12 @@ extension type EnumSchema.fromMap(Map _value) factory EnumSchema({ String? title, String? description, - required Set values, + required Iterable values, }) => EnumSchema.fromMap({ 'type': JsonType.enumeration.typeName, if (title != null) 'title': title, if (description != null) 'description': description, - 'enum': values.toSet(), + 'enum': values, }); /// A title for this schema, should be short. @@ -1115,7 +1146,17 @@ extension type EnumSchema.fromMap(Map _value) String? get description => _value['description'] as String?; /// The allowed enum values. - Set get values => (_value['enum'] as Set).cast(); + Iterable get values { + final values = (_value['enum'] as Iterable?)?.cast(); + if (values == null) { + throw ArgumentError('Missing required property "values"'); + } + assert( + values.toSet().length == values.length, + "The 'values' property has duplicate entries.", + ); + return values; + } bool _validateEnum( Object? data, From 1c58c204d1927256f34ca50152e41970b64be000 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 25 Jun 2025 13:27:00 -0700 Subject: [PATCH 4/4] Revert unnamed constructor --- pkgs/dart_mcp/lib/src/api/tools.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pkgs/dart_mcp/lib/src/api/tools.dart b/pkgs/dart_mcp/lib/src/api/tools.dart index 614828e2..407566d3 100644 --- a/pkgs/dart_mcp/lib/src/api/tools.dart +++ b/pkgs/dart_mcp/lib/src/api/tools.dart @@ -324,13 +324,6 @@ extension type ValidationError.fromMap(Map _value) { /// if you need something more complex you can create your own /// `Map` and cast it to [Schema] (or [ObjectSchema]) directly. extension type Schema.fromMap(Map _value) { - factory Schema({JsonType? type, String? title, String? description}) => - Schema.fromMap({ - 'type': JsonType.enumeration.typeName, - if (title != null) 'title': title, - if (description != null) 'description': description, - }); - /// A combined schema, see /// https://json-schema.org/understanding-json-schema/reference/combining#schema-composition factory Schema.combined({