diff --git a/mcp_examples/bin/workflow_client.dart b/mcp_examples/bin/workflow_client.dart index 537392a3..a8dace90 100644 --- a/mcp_examples/bin/workflow_client.dart +++ b/mcp_examples/bin/workflow_client.dart @@ -512,11 +512,21 @@ final class WorkflowClient extends MCPClient with RootsSupport { properties: properties ?? {}, nullable: nullable, ); - case JsonType.string: + case JsonType.string + when (inputSchema as StringSchema).enumValues == null: return gemini.Schema.string( description: inputSchema.description, nullable: nullable, ); + case JsonType.string + when (inputSchema as StringSchema).enumValues != null: + case JsonType.enumeration: // ignore: deprecated_member_use + final schema = inputSchema as StringSchema; + return gemini.Schema.enumString( + enumValues: schema.enumValues!.toList(), + description: description, + nullable: nullable, + ); case JsonType.list: final listSchema = inputSchema as ListSchema; final itemSchema = @@ -546,13 +556,6 @@ final class WorkflowClient extends MCPClient with RootsSupport { description: description, nullable: nullable, ); - case JsonType.enumeration: - final schema = inputSchema as EnumSchema; - return gemini.Schema.enumString( - enumValues: schema.values.toList(), - description: description, - nullable: nullable, - ); default: throw UnimplementedError( 'Unimplemented schema type ${inputSchema.type}', diff --git a/pkgs/dart_mcp/CHANGELOG.md b/pkgs/dart_mcp/CHANGELOG.md index 20625f0c..f0596cda 100644 --- a/pkgs/dart_mcp/CHANGELOG.md +++ b/pkgs/dart_mcp/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.3.2-wip + +- Deprecate the `EnumSchema` type in favor of the `StringSchema` with an + `enumValues` parameter. The `EnumSchema` type was not MCP spec compatible. + - Also deprecated the associated JsonType.enumeration which doesn't exist + in the JSON schema spec. + ## 0.3.1 - Fixes communication problem when a `MCPServer` is instantiated without diff --git a/pkgs/dart_mcp/example/elicitations_client.dart b/pkgs/dart_mcp/example/elicitations_client.dart index 19780831..05d1c1d3 100644 --- a/pkgs/dart_mcp/example/elicitations_client.dart +++ b/pkgs/dart_mcp/example/elicitations_client.dart @@ -88,8 +88,10 @@ Do you want to accept (a), reject (r), or cancel (c) the elicitation? final name = property.key; final type = property.value.type; final allowedValues = - type == JsonType.enumeration - ? ' (${(property.value as EnumSchema).values.join(', ')})' + // ignore: deprecated_member_use_from_same_package + (type == JsonType.enumeration || type == JsonType.string) && + (property.value as StringSchema).enumValues != null + ? ' (${(property.value as StringSchema).enumValues!.join(', ')})' : ''; // Ask the user in a loop until the value provided matches the schema, // at which point we will `break` from the loop. @@ -99,6 +101,7 @@ Do you want to accept (a), reject (r), or cancel (c) the elicitation? try { // Convert the value to the correct type. final convertedValue = switch (type) { + // ignore: deprecated_member_use_from_same_package JsonType.string || JsonType.enumeration => userValue, JsonType.num => num.parse(userValue), JsonType.int => int.parse(userValue), diff --git a/pkgs/dart_mcp/example/elicitations_server.dart b/pkgs/dart_mcp/example/elicitations_server.dart index 477a0790..0fa7cac6 100644 --- a/pkgs/dart_mcp/example/elicitations_server.dart +++ b/pkgs/dart_mcp/example/elicitations_server.dart @@ -42,7 +42,7 @@ base class MCPServerWithElicitation extends MCPServer properties: { 'name': Schema.string(), 'age': Schema.int(), - 'gender': Schema.enumeration(values: ['male', 'female', 'other']), + 'gender': Schema.string(enumValues: ['male', 'female', 'other']), }, ), ), diff --git a/pkgs/dart_mcp/lib/src/api/elicitation.dart b/pkgs/dart_mcp/lib/src/api/elicitation.dart index 38abc759..4d5c1cf4 100644 --- a/pkgs/dart_mcp/lib/src/api/elicitation.dart +++ b/pkgs/dart_mcp/lib/src/api/elicitation.dart @@ -77,7 +77,8 @@ extension type ElicitRequest._fromMap(Map _value) case JsonType.num: case JsonType.int: case JsonType.bool: - case JsonType.enumeration: + case JsonType + .enumeration: // ignore: deprecated_member_use_from_same_package break; case JsonType.object: case JsonType.list: diff --git a/pkgs/dart_mcp/lib/src/api/tools.dart b/pkgs/dart_mcp/lib/src/api/tools.dart index 85a2c093..4e151e2e 100644 --- a/pkgs/dart_mcp/lib/src/api/tools.dart +++ b/pkgs/dart_mcp/lib/src/api/tools.dart @@ -234,6 +234,7 @@ enum JsonType { num('number'), int('integer'), bool('boolean'), + @Deprecated('Use JsonType.string') enumeration('enum'), nil('null'); @@ -405,6 +406,7 @@ extension type Schema.fromMap(Map _value) { static const object = ObjectSchema.new; /// Alias for [EnumSchema.new]. + @Deprecated('Use Schema.string instead') static const enumeration = EnumSchema.new; /// Alias for [NullSchema.new]. @@ -480,6 +482,8 @@ extension SchemaValidation on Schema { accumulatedFailures, ); case JsonType.string: + case JsonType + .enumeration: // ignore: deprecated_member_use_from_same_package isValid = (this as StringSchema)._validateString( data, currentPath, @@ -497,12 +501,6 @@ 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; @@ -1077,6 +1075,7 @@ extension type const StringSchema.fromMap(Map _value) int? minLength, int? maxLength, String? pattern, + Iterable? enumValues, }) => StringSchema.fromMap({ 'type': JsonType.string.typeName, if (title != null) 'title': title, @@ -1084,6 +1083,7 @@ extension type const StringSchema.fromMap(Map _value) if (minLength != null) 'minLength': minLength, if (maxLength != null) 'maxLength': maxLength, if (pattern != null) 'pattern': pattern, + if (enumValues != null) 'enum': enumValues, }); /// The minimum allowed length of this String. @@ -1095,6 +1095,16 @@ extension type const StringSchema.fromMap(Map _value) /// A regular expression pattern that the String must match. String? get pattern => _value['pattern'] as String?; + /// The allowed values for this String, corresponds to the `enum` key. + Iterable? get enumValues { + final values = (_value['enum'] as Iterable?)?.cast(); + assert( + values?.toSet().length == values?.length, + "The 'enum' property has duplicate entries.", + ); + return values; + } + bool _validateString( Object? data, List currentPath, @@ -1142,13 +1152,27 @@ extension type const StringSchema.fromMap(Map _value) ), ); } + if (enumValues case final enumValues? when !enumValues.contains(data)) { + accumulatedFailures.add( + ValidationError( + ValidationErrorType.enumValueNotAllowed, + path: currentPath, + details: + 'String "$data" is not one of the allowed values: ' + '${enumValues.join(', ')}', + ), + ); + return false; + } return isValid; } } /// A JSON Schema definition for a set of allowed string values. +@Deprecated('Use StringSchema instead') extension type EnumSchema.fromMap(Map _value) implements Schema { + @Deprecated('Use StringSchema instead') factory EnumSchema({ String? title, String? description, @@ -1178,36 +1202,6 @@ extension type EnumSchema.fromMap(Map _value) ); return values; } - - bool _validateEnum( - Object? data, - List currentPath, - HashSet accumulatedFailures, - ) { - if (data is! String) { - accumulatedFailures.add( - ValidationError.typeMismatch( - path: currentPath, - expectedType: String, - actualValue: data, - ), - ); - 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]. diff --git a/pkgs/dart_mcp/pubspec.yaml b/pkgs/dart_mcp/pubspec.yaml index 588d2378..d94b6cd6 100644 --- a/pkgs/dart_mcp/pubspec.yaml +++ b/pkgs/dart_mcp/pubspec.yaml @@ -1,5 +1,5 @@ name: dart_mcp -version: 0.3.1 +version: 0.3.2-wip description: A package for making MCP servers and clients. repository: https://github.com/dart-lang/ai/tree/main/pkgs/dart_mcp issue_tracker: https://github.com/dart-lang/ai/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Adart_mcp diff --git a/pkgs/dart_mcp/test/api/tools_test.dart b/pkgs/dart_mcp/test/api/tools_test.dart index d3ba8130..e40f2d94 100644 --- a/pkgs/dart_mcp/test/api/tools_test.dart +++ b/pkgs/dart_mcp/test/api/tools_test.dart @@ -205,6 +205,7 @@ void main() { }); test('EnumSchema', () { + // ignore: deprecated_member_use_from_same_package final schema = EnumSchema( title: 'Foo', description: 'Bar', @@ -860,30 +861,6 @@ void main() { }); }); - group('Enum Specific', () { - test('enumValueNotAllowed', () { - final schema = EnumSchema(values: {'a', 'b'}); - expectFailuresMatch(schema, 'c', [ - ValidationError( - ValidationErrorType.enumValueNotAllowed, - path: const [], - ), - ]); - }); - - 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, path: const []), - ]); - }); - }); - group('Schema Combinators', () { test('allOfNotMet - one sub-schema fails', () { final schema = Schema.combined( @@ -1340,6 +1317,28 @@ void main() { ValidationError(ValidationErrorType.patternMismatch, path: const []), ]); }); + + test('enumValueNotAllowed', () { + final schema = StringSchema(enumValues: {'a', 'b'}); + expectFailuresMatch(schema, 'c', [ + ValidationError( + ValidationErrorType.enumValueNotAllowed, + path: const [], + ), + ]); + }); + + test('valid enum value', () { + final schema = StringSchema(enumValues: {'a', 'b'}); + expectFailuresMatch(schema, 'a', []); + }); + + test('enum with non-string data', () { + final schema = StringSchema(enumValues: {'a', 'b'}); + expectFailuresMatch(schema, 1, [ + ValidationError(ValidationErrorType.typeMismatch, path: const []), + ]); + }); }); group('Number Specific', () { diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart index 58cf6b35..7f84b305 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart @@ -653,10 +653,10 @@ base mixin DartToolingDaemonSupport 'figure out how to find the widget instead of just guessing tooltip ' 'text or other things.', properties: { - 'command': Schema.enumeration( + 'command': Schema.string( // Commented out values are flutter_driver commands that are not // supported, but may be in the future. - values: [ + enumValues: [ 'get_health', 'get_layer_tree', 'get_render_tree', @@ -707,13 +707,13 @@ base mixin DartToolingDaemonSupport 'The frequency in Hz of the generated move events. ' 'Required for the scroll command', ), - 'finderType': Schema.enumeration( + 'finderType': Schema.string( description: 'The kind of finder to use, if required for the command. ' 'Required for get_text, scroll, scroll_into_view, tap, waitFor, ' 'waitForAbsent, waitForTappable, get_offset, and ' 'get_diagnostics_tree', - values: [ + enumValues: [ 'ByType', 'ByValueKey', 'ByTooltipMessage', @@ -728,8 +728,8 @@ base mixin DartToolingDaemonSupport description: 'Required for the ByValueKey finder, the String value of the key', ), - 'keyValueType': Schema.enumeration( - values: ['int', 'String'], + 'keyValueType': Schema.string( + enumValues: ['int', 'String'], description: 'Required for the ByValueKey finder, the type of the key', ), @@ -767,25 +767,25 @@ base mixin DartToolingDaemonSupport additionalProperties: true, ), // This is a boolean but uses the `true` and `false` strings. - 'matchRoot': Schema.enumeration( + 'matchRoot': Schema.string( description: 'Required by the Descendent and Ancestor finders. ' 'Whether the widget matching `of` will be considered for a ' 'match', - values: ['true', 'false'], + enumValues: ['true', 'false'], ), // This is a boolean but uses the `true` and `false` strings. - 'firstMatchOnly': Schema.enumeration( + 'firstMatchOnly': Schema.string( description: 'Required by the Descendent and Ancestor finders. ' 'If true then only the first ancestor or descendent matching ' '`matching` will be returned.', - values: ['true', 'false'], + enumValues: ['true', 'false'], ), - 'action': Schema.enumeration( + 'action': Schema.string( description: 'Required for send_text_input_action, the input action to send', - values: [ + enumValues: [ 'none', 'unspecified', 'done', @@ -806,11 +806,11 @@ base mixin DartToolingDaemonSupport 'Maximum time in milliseconds to wait for the command to ' 'complete. Defaults to $_defaultTimeoutMs.', ), - 'offsetType': Schema.enumeration( + 'offsetType': Schema.string( description: 'Offset types that can be requested by get_offset. ' 'Required for get_offset.', - values: [ + enumValues: [ 'topLeft', 'topRight', 'bottomLeft', @@ -818,11 +818,11 @@ base mixin DartToolingDaemonSupport 'center', ], ), - 'diagnosticsType': Schema.enumeration( + 'diagnosticsType': Schema.string( description: 'The type of diagnostics tree to request. ' 'Required for get_diagnostics_tree', - values: ['renderObject', 'widget'], + enumValues: ['renderObject', 'widget'], ), 'subtreeDepth': Schema.int( description: diff --git a/pkgs/dart_mcp_server/pubspec.yaml b/pkgs/dart_mcp_server/pubspec.yaml index 561d4fd0..40f054d7 100644 --- a/pkgs/dart_mcp_server/pubspec.yaml +++ b/pkgs/dart_mcp_server/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: args: ^2.7.0 async: ^2.13.0 collection: ^1.19.1 - dart_mcp: ^0.3.1 + dart_mcp: ^0.3.2 dds_service_extensions: ^2.0.1 devtools_shared: ^12.0.0 dtd: ^4.0.0