From 5378a69f83f36490c137ec4b42ae62751fc714bc Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Thu, 26 Jun 2025 21:53:01 +0000 Subject: [PATCH 1/3] auto validate tool arguments, improve validation messages --- pkgs/dart_mcp/CHANGELOG.md | 12 + pkgs/dart_mcp/lib/src/api/tools.dart | 335 ++++++------ .../lib/src/server/tools_support.dart | 27 +- pkgs/dart_mcp/test/api/tools_test.dart | 505 ++++++++++-------- .../test/server/tools_support_test.dart | 61 ++- .../lib/src/mixins/dash_cli.dart | 6 +- pkgs/dart_mcp_server/pubspec.yaml | 2 +- 7 files changed, 564 insertions(+), 384 deletions(-) diff --git a/pkgs/dart_mcp/CHANGELOG.md b/pkgs/dart_mcp/CHANGELOG.md index 08eb159c..e0b7f61e 100644 --- a/pkgs/dart_mcp/CHANGELOG.md +++ b/pkgs/dart_mcp/CHANGELOG.md @@ -3,6 +3,18 @@ - 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. +- Add more detail to type validation errors. +- Remove some duplicate validation errors, errors are only reported for the + leaf nodes and not all the way up the tree. + - Deprecated a few validation error types as a part of this, including + `propertyNamesInvalid`, `propertyValueInvalid`, `itemInvalid` and + `prefixItemInvalid`. +- Added a `custom` validation error type. +- Auto-validate schemas for all tools by default. This can be disabled by + passing `validateArguments: false` to `registerTool`. + - While this could be breaking, we are not treating it as such as it should + not be breaking and will generally result in better errors for invalid + arguments. ## 0.2.2 diff --git a/pkgs/dart_mcp/lib/src/api/tools.dart b/pkgs/dart_mcp/lib/src/api/tools.dart index 407566d3..47be5b28 100644 --- a/pkgs/dart_mcp/lib/src/api/tools.dart +++ b/pkgs/dart_mcp/lib/src/api/tools.dart @@ -231,6 +231,9 @@ enum JsonType { /// Enum representing the types of validation failures when checking data /// against a schema. enum ValidationErrorType { + // For custom validation. + custom, + // General typeMismatch, @@ -245,7 +248,15 @@ enum ValidationErrorType { additionalPropertyNotAllowed, minPropertiesNotMet, maxPropertiesExceeded, + @Deprecated( + 'These events are no longer emitted, just emit a single error for the ' + 'key itself', + ) propertyNamesInvalid, + @Deprecated( + 'These events are no longer emitted, just emit a single error for the ' + 'property itself', + ) propertyValueInvalid, patternPropertyValueInvalid, unevaluatedPropertyNotAllowed, @@ -254,7 +265,15 @@ enum ValidationErrorType { minItemsNotMet, maxItemsExceeded, uniqueItemsViolated, + @Deprecated( + 'These events are no longer emitted, just emit a single error for the ' + 'item itself', + ) itemInvalid, + @Deprecated( + 'These events are no longer emitted, just emit a single error for the ' + 'prefix item itself', + ) prefixItemInvalid, unevaluatedItemNotAllowed, @@ -279,29 +298,38 @@ enum ValidationErrorType { extension type ValidationError.fromMap(Map _value) { factory ValidationError( ValidationErrorType error, { - List? path, + required List path, String? details, }) => ValidationError.fromMap({ 'error': error.name, - if (path != null) 'path': path.toList(), + 'path': path.toList(), if (details != null) 'details': details, }); - /// The type of validation error that occurred. - ValidationErrorType? get error => ValidationErrorType.values.firstWhereOrNull( - (t) => t.name == _value['error'], + factory ValidationError.typeMismatch({ + required List path, + required Type expectedType, + required Object? actualValue, + }) => ValidationError( + ValidationErrorType.typeMismatch, + path: path, + details: 'Value `$actualValue` is not of type `$expectedType`', ); + /// The type of validation error that occurred. + ValidationErrorType get error => + ValidationErrorType.values.firstWhere((t) => t.name == _value['error']); + /// The path to the object that had the error. - List? get path => (_value['path'] as List?)?.cast(); + List get path => (_value['path'] as List).cast(); /// Additional details about the error (optional). String? get details => _value['details'] as String?; String toErrorString() { - return '${error!.name} in object at ' - '${path!.map((p) => '[$p]').join('')}' - '${details != null ? ' - $details' : ''}'; + return '${details != null ? '$details' : error.name} at path ' + '#root${path.map((p) => '["$p"]').join('')}' + ''; } } @@ -465,9 +493,10 @@ extension SchemaValidation on Schema { if (data is! bool) { isValid = false; accumulatedFailures.add( - ValidationError( - ValidationErrorType.typeMismatch, + ValidationError.typeMismatch( path: currentPath, + expectedType: bool, + actualValue: data, ), ); } @@ -475,9 +504,10 @@ extension SchemaValidation on Schema { if (data != null) { isValid = false; accumulatedFailures.add( - ValidationError( - ValidationErrorType.typeMismatch, + ValidationError.typeMismatch( path: currentPath, + expectedType: Null, + actualValue: data, ), ); } @@ -498,9 +528,9 @@ extension SchemaValidation on Schema { // Validate data against the non-combinator keywords of the current schema // ('this'). - if (!_performDirectValidation(data, currentPath, accumulatedFailures)) { - isValid = false; - } + isValid = + _performDirectValidation(data, currentPath, accumulatedFailures) && + isValid; // Handle combinator keywords. Create the "base schema" from 'this' schema, // excluding combinator keywords. This base schema's constraints are @@ -530,19 +560,16 @@ extension SchemaValidation on Schema { if (allOf case final allOfList?) { var allSubSchemasAreValid = true; - final allOfDetailedSubFailures = []; for (final subSchemaMember in allOfList) { final effectiveSubSchema = mergeWithBase(subSchemaMember); - final currentSubSchemaFailures = _createHashSet(); - if (!effectiveSubSchema._validateSchema( - data, - currentPath, - currentSubSchemaFailures, - )) { - allSubSchemasAreValid = false; - allOfDetailedSubFailures.addAll(currentSubSchemaFailures); - } + allSubSchemasAreValid = + effectiveSubSchema._validateSchema( + data, + currentPath, + accumulatedFailures, + ) && + allSubSchemasAreValid; } // `allOf` fails if any effective sub-schema (Base AND SubMember) failed. if (!allSubSchemasAreValid) { @@ -550,7 +577,6 @@ extension SchemaValidation on Schema { accumulatedFailures.add( ValidationError(ValidationErrorType.allOfNotMet, path: currentPath), ); - accumulatedFailures.addAll(allOfDetailedSubFailures); } } if (anyOf case final anyOfList?) { @@ -570,7 +596,11 @@ extension SchemaValidation on Schema { if (!oneSubSchemaPassed) { isValid = false; accumulatedFailures.add( - ValidationError(ValidationErrorType.anyOfNotMet, path: currentPath), + ValidationError( + ValidationErrorType.anyOfNotMet, + path: currentPath, + details: 'No sub-schema passed validation for $data', + ), ); } } @@ -589,30 +619,35 @@ extension SchemaValidation on Schema { if (matchingSubSchemaCount != 1) { isValid = false; accumulatedFailures.add( - ValidationError(ValidationErrorType.oneOfNotMet, path: currentPath), + ValidationError( + ValidationErrorType.oneOfNotMet, + path: currentPath, + details: + 'Exactly one sub-schema must match $data but ' + '$matchingSubSchemaCount did', + ), ); } } if (not case final notList?) { - final notConditionViolatedBySubSchema = notList.any((subSchemaInNot) { + for (final subSchemaInNot in notList) { final effectiveSubSchemaForNot = mergeWithBase(subSchemaInNot); // 'not' is violated if data *validates* against the (Base AND // NotSubSchema). - return effectiveSubSchemaForNot._validateSchema( + if (effectiveSubSchemaForNot._validateSchema( data, currentPath, _createHashSet(), - ); - }); - - if (notConditionViolatedBySubSchema) { - isValid = false; - accumulatedFailures.add( - ValidationError( - ValidationErrorType.notConditionViolated, - path: currentPath, - ), - ); + )) { + isValid = false; + accumulatedFailures.add( + ValidationError( + ValidationErrorType.notConditionViolated, + path: currentPath, + details: '$data matched the schema $subSchemaInNot', + ), + ); + } } } @@ -858,7 +893,11 @@ extension type ObjectSchema.fromMap(Map _value) ) { if (data is! Map) { accumulatedFailures.add( - ValidationError(ValidationErrorType.typeMismatch, path: currentPath), + ValidationError.typeMismatch( + path: currentPath, + expectedType: Map, + actualValue: data, + ), ); return false; } @@ -873,7 +912,7 @@ extension type ObjectSchema.fromMap(Map _value) path: currentPath, details: 'There should be at least $minProperties ' - 'properties. Only ${data.keys.length} were found.', + 'properties. Only ${data.keys.length} were found', ), ); } @@ -886,7 +925,7 @@ extension type ObjectSchema.fromMap(Map _value) path: currentPath, details: 'Exceeded maxProperties limit of $maxProperties ' - '(${data.keys.length}).', + '(${data.keys.length})', ), ); } @@ -898,7 +937,7 @@ extension type ObjectSchema.fromMap(Map _value) ValidationError( ValidationErrorType.requiredPropertyMissing, path: currentPath, - details: 'Required property "$reqProp" is missing.', + details: 'Required property "$reqProp" is missing', ), ); } @@ -914,21 +953,13 @@ extension type ObjectSchema.fromMap(Map _value) if (data.containsKey(entry.key)) { currentPath.add(entry.key); evaluatedKeys.add(entry.key); - final propertySpecificFailures = _createHashSet(); - if (!entry.value._validateSchema( - data[entry.key], - currentPath, - propertySpecificFailures, - )) { - isValid = false; - accumulatedFailures.add( - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: currentPath, - ), - ); - accumulatedFailures.addAll(propertySpecificFailures); - } + isValid = + entry.value._validateSchema( + data[entry.key], + currentPath, + accumulatedFailures, + ) && + isValid; currentPath.removeLast(); } } @@ -945,21 +976,13 @@ extension type ObjectSchema.fromMap(Map _value) if (pattern.hasMatch(dataKey)) { currentPath.add(dataKey); evaluatedKeys.add(dataKey); - final patternPropertySpecificFailures = _createHashSet(); - if (!entry.value._validateSchema( - data[dataKey], - currentPath, - patternPropertySpecificFailures, - )) { - isValid = false; - accumulatedFailures.add( - ValidationError( - ValidationErrorType.patternPropertyValueInvalid, - path: currentPath, - ), - ); - accumulatedFailures.addAll(patternPropertySpecificFailures); - } + isValid = + entry.value._validateSchema( + data[dataKey], + currentPath, + accumulatedFailures, + ) && + isValid; currentPath.removeLast(); } } @@ -973,21 +996,13 @@ extension type ObjectSchema.fromMap(Map _value) if (propertyNames case final propNamesSchema?) { for (final key in data.keys) { currentPath.add(key); - final propertyNameSpecificFailures = _createHashSet(); - if (!propNamesSchema._validateSchema( - key, - currentPath, - propertyNameSpecificFailures, - )) { - isValid = false; - accumulatedFailures.addAll(propertyNameSpecificFailures); - accumulatedFailures.add( - ValidationError( - ValidationErrorType.propertyNamesInvalid, - path: currentPath, - ), - ); - } + isValid = + propNamesSchema._validateSchema( + key, + currentPath, + accumulatedFailures, + ) && + isValid; currentPath.removeLast(); } } @@ -999,32 +1014,26 @@ extension type ObjectSchema.fromMap(Map _value) for (final dataKey in data.keys) { if (evaluatedKeys.contains(dataKey)) continue; - var isAdditionalPropertyAllowed = true; if (additionalProperties != null) { final ap = additionalProperties; currentPath.add(dataKey); if (ap is bool && !ap) { - isAdditionalPropertyAllowed = false; - } else if (ap is Schema) { - final additionalPropSchemaFailures = _createHashSet(); - if (!ap._validateSchema( - data[dataKey], - currentPath, - additionalPropSchemaFailures, - )) { - isAdditionalPropertyAllowed = false; - // Add details why it failed - accumulatedFailures.addAll(additionalPropSchemaFailures); - } - } - if (!isAdditionalPropertyAllowed) { isValid = false; accumulatedFailures.add( ValidationError( ValidationErrorType.additionalPropertyNotAllowed, path: currentPath, + details: 'Additional property "$dataKey" is not allowed', ), ); + } else if (ap is Schema) { + isValid = + ap._validateSchema( + data[dataKey], + currentPath, + accumulatedFailures, + ) && + isValid; } currentPath.removeLast(); } else if (unevaluatedProperties == false) { @@ -1035,6 +1044,7 @@ extension type ObjectSchema.fromMap(Map _value) ValidationError( ValidationErrorType.unevaluatedPropertyNotAllowed, path: currentPath, + details: 'Unevaluated property "$dataKey" is not allowed', ), ); currentPath.removeLast(); @@ -1078,7 +1088,11 @@ extension type const StringSchema.fromMap(Map _value) ) { if (data is! String) { accumulatedFailures.add( - ValidationError(ValidationErrorType.typeMismatch, path: currentPath), + ValidationError.typeMismatch( + path: currentPath, + expectedType: String, + actualValue: data, + ), ); return false; } @@ -1089,7 +1103,7 @@ extension type const StringSchema.fromMap(Map _value) ValidationError( ValidationErrorType.minLengthNotMet, path: currentPath, - details: 'String "$data" is not at least $minLen characters long.', + details: 'String "$data" is not at least $minLen characters long', ), ); } @@ -1099,7 +1113,7 @@ extension type const StringSchema.fromMap(Map _value) ValidationError( ValidationErrorType.maxLengthExceeded, path: currentPath, - details: 'String "$data" is more than $maxLen characters long.', + details: 'String "$data" is more than $maxLen characters long', ), ); } @@ -1110,7 +1124,7 @@ extension type const StringSchema.fromMap(Map _value) ValidationError( ValidationErrorType.patternMismatch, path: currentPath, - details: 'String "$data" doesn\'t match the pattern "$dataPattern".', + details: 'String "$data" doesn\'t match the pattern "$dataPattern"', ), ); } @@ -1158,7 +1172,11 @@ extension type EnumSchema.fromMap(Map _value) ) { if (data is! String) { accumulatedFailures.add( - ValidationError(ValidationErrorType.typeMismatch, path: currentPath), + ValidationError.typeMismatch( + path: currentPath, + expectedType: String, + actualValue: data, + ), ); return false; } @@ -1222,7 +1240,11 @@ extension type NumberSchema.fromMap(Map _value) ) { if (data is! num) { accumulatedFailures.add( - ValidationError(ValidationErrorType.typeMismatch, path: currentPath), + ValidationError.typeMismatch( + path: currentPath, + expectedType: num, + actualValue: data, + ), ); return false; } @@ -1234,7 +1256,7 @@ extension type NumberSchema.fromMap(Map _value) ValidationError( ValidationErrorType.minimumNotMet, path: currentPath, - details: 'Value $data is not at least $min.', + details: 'Value $data is not at least $min', ), ); } @@ -1244,7 +1266,7 @@ extension type NumberSchema.fromMap(Map _value) ValidationError( ValidationErrorType.maximumExceeded, path: currentPath, - details: 'Value $data is larger than $max.', + details: 'Value $data is larger than $max', ), ); } @@ -1255,7 +1277,7 @@ extension type NumberSchema.fromMap(Map _value) ValidationErrorType.exclusiveMinimumNotMet, path: currentPath, - details: 'Value $data is not greater than $exclusiveMin.', + details: 'Value $data is not greater than $exclusiveMin', ), ); } @@ -1265,7 +1287,7 @@ extension type NumberSchema.fromMap(Map _value) ValidationError( ValidationErrorType.exclusiveMaximumExceeded, path: currentPath, - details: 'Value $data is not less than $exclusiveMax.', + details: 'Value $data is not less than $exclusiveMax', ), ); } @@ -1277,7 +1299,7 @@ extension type NumberSchema.fromMap(Map _value) ValidationError( ValidationErrorType.multipleOfInvalid, path: currentPath, - details: 'Value $data is not a multiple of $multipleOf.', + details: 'Value $data is not a multiple of $multipleOf', ), ); } @@ -1330,12 +1352,10 @@ extension type IntegerSchema.fromMap(Map _value) ) { if (data == null || (data is! int && data is! num)) { accumulatedFailures.add( - ValidationError( - ValidationErrorType.typeMismatch, + ValidationError.typeMismatch( path: currentPath, - details: - 'Value $data has the type ${data.runtimeType}, which is not ' - 'an integer.', + expectedType: int, + actualValue: data, ), ); return false; @@ -1347,7 +1367,7 @@ extension type IntegerSchema.fromMap(Map _value) ValidationError( ValidationErrorType.typeMismatch, path: currentPath, - details: 'Value $data is a number, but is not an integer.', + details: 'Value $data is a number, but is not an integer', ), ); return false; @@ -1363,7 +1383,7 @@ extension type IntegerSchema.fromMap(Map _value) ValidationError( ValidationErrorType.minimumNotMet, path: currentPath, - details: 'Value $data is less than the minimum of $min.', + details: 'Value $data is less than the minimum of $min', ), ); } @@ -1373,7 +1393,7 @@ extension type IntegerSchema.fromMap(Map _value) ValidationError( ValidationErrorType.maximumExceeded, path: currentPath, - details: 'Value $data is more than the maximum of $max.', + details: 'Value $data is more than the maximum of $max', ), ); } @@ -1383,7 +1403,7 @@ extension type IntegerSchema.fromMap(Map _value) ValidationError( ValidationErrorType.exclusiveMinimumNotMet, path: currentPath, - details: 'Value $data is not greater than $exclusiveMin.', + details: 'Value $data is not greater than $exclusiveMin', ), ); } @@ -1393,7 +1413,7 @@ extension type IntegerSchema.fromMap(Map _value) ValidationError( ValidationErrorType.exclusiveMaximumExceeded, path: currentPath, - details: 'Value $data is not less than $exclusiveMax.', + details: 'Value $data is not less than $exclusiveMax', ), ); } @@ -1404,7 +1424,7 @@ extension type IntegerSchema.fromMap(Map _value) ValidationError( ValidationErrorType.multipleOfInvalid, path: currentPath, - details: 'Value $data is not a multiple of $multOf.', + details: 'Value $data is not a multiple of $multOf', ), ); } @@ -1570,7 +1590,11 @@ extension type ListSchema.fromMap(Map _value) ) { if (data is! List) { accumulatedFailures.add( - ValidationError(ValidationErrorType.typeMismatch, path: currentPath), + ValidationError.typeMismatch( + path: currentPath, + expectedType: List, + actualValue: data, + ), ); return false; } @@ -1585,7 +1609,7 @@ extension type ListSchema.fromMap(Map _value) path: currentPath, details: 'List has ${data.length} items, but must have at least ' - '$min.', + '$min', ), ); } @@ -1598,7 +1622,7 @@ extension type ListSchema.fromMap(Map _value) path: currentPath, details: 'List has ${data.length} items, but must have less than ' - '$max.', + '$max', ), ); } @@ -1618,7 +1642,7 @@ extension type ListSchema.fromMap(Map _value) ValidationError( ValidationErrorType.uniqueItemsViolated, path: currentPath, - details: 'List contains duplicate items: ${duplicates.join(', ')}.', + details: 'List contains duplicate items: ${duplicates.join(', ')}', ), ); } @@ -1628,21 +1652,13 @@ extension type ListSchema.fromMap(Map _value) for (var i = 0; i < pItems.length && i < data.length; i++) { evaluatedItems[i] = true; currentPath.add(i.toString()); - final prefixItemSpecificFailures = _createHashSet(); - if (!pItems[i]._validateSchema( - data[i], - currentPath, - prefixItemSpecificFailures, - )) { - isValid = false; - accumulatedFailures.add( - ValidationError( - ValidationErrorType.prefixItemInvalid, - path: currentPath, - ), - ); - accumulatedFailures.addAll(prefixItemSpecificFailures); - } + isValid = + pItems[i]._validateSchema( + data[i], + currentPath, + accumulatedFailures, + ) && + isValid; currentPath.removeLast(); } } @@ -1651,18 +1667,13 @@ extension type ListSchema.fromMap(Map _value) for (var i = startIndex; i < data.length; i++) { evaluatedItems[i] = true; currentPath.add(i.toString()); - final itemSpecificFailures = _createHashSet(); - if (!itemSchema._validateSchema( - data[i], - currentPath, - itemSpecificFailures, - )) { - isValid = false; - accumulatedFailures.add( - ValidationError(ValidationErrorType.itemInvalid, path: currentPath), - ); - accumulatedFailures.addAll(itemSpecificFailures); - } + isValid = + itemSchema._validateSchema( + data[i], + currentPath, + accumulatedFailures, + ) && + isValid; currentPath.removeLast(); } } @@ -1699,11 +1710,7 @@ HashSet _createHashSet() { a.error == b.error; }, hashCode: (ValidationError error) { - return Object.hashAll([ - ...error.path ?? const [], - error.details, - error.error, - ]); + return Object.hashAll([...error.path, error.details, error.error]); }, ); } diff --git a/pkgs/dart_mcp/lib/src/server/tools_support.dart b/pkgs/dart_mcp/lib/src/server/tools_support.dart index e255019a..6521b86f 100644 --- a/pkgs/dart_mcp/lib/src/server/tools_support.dart +++ b/pkgs/dart_mcp/lib/src/server/tools_support.dart @@ -43,17 +43,38 @@ base mixin ToolsSupport on MCPServer { /// /// Throws a [StateError] if there is already a [Tool] registered with the /// same name. + /// + /// If [validateArguments] is true, then request arguments are automatically + /// validated against the [tool]s input schema. void registerTool( Tool tool, - FutureOr Function(CallToolRequest) impl, - ) { + FutureOr Function(CallToolRequest) impl, { + bool validateArguments = true, + }) { if (_registeredTools.containsKey(tool.name)) { throw StateError( 'Failed to register tool ${tool.name}, it is already registered', ); } _registeredTools[tool.name] = tool; - _registeredToolImpls[tool.name] = impl; + _registeredToolImpls[tool.name] = + validateArguments + ? (request) { + final errors = tool.inputSchema.validate( + request.arguments ?? const {}, + ); + if (errors.isNotEmpty) { + return CallToolResult( + content: [ + for (final error in errors) + Content.text(text: error.toErrorString()), + ], + isError: true, + ); + } + return impl(request); + } + : impl; if (ready) { _notifyToolListChanged(); diff --git a/pkgs/dart_mcp/test/api/tools_test.dart b/pkgs/dart_mcp/test/api/tools_test.dart index 21bfb744..e6ed39fa 100644 --- a/pkgs/dart_mcp/test/api/tools_test.dart +++ b/pkgs/dart_mcp/test/api/tools_test.dart @@ -13,7 +13,8 @@ void main() { // validate(). ValidationError onlyKeepError(ValidationError e) { return ValidationError( - e.error!, // The factory requires a non-nullable error. + e.error, // The factory requires a non-nullable error. + path: const [], ); } @@ -251,49 +252,49 @@ void main() { group('Type Mismatch', () { test('object schema with non-map data', () { expectFailuresMatch(Schema.object(), 'not a map', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('list schema with non-list data', () { expectFailuresMatch(Schema.list(), 'not a list', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('string schema with non-string data', () { expectFailuresMatch(Schema.string(), 123, [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('number schema with non-num data', () { expectFailuresMatch(Schema.num(), 'not a number', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('integer schema with non-int data', () { expectFailuresMatch(Schema.int(), 'not an int', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('integer schema with non-integer num data', () { expectFailuresMatch(Schema.int(), 10.5, [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('boolean schema with non-bool data', () { expectFailuresMatch(Schema.bool(), 'not a bool', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('null schema with non-null data', () { expectFailuresMatch(Schema.nil(), 'not null', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('integer schema with integer-like num data (e.g. 10.0)', () { // This test expects minimumNotMet because 10.0 is converted to int 10, // which is less than the minimum of 11. expectFailuresMatch(IntegerSchema(minimum: 11), 10.0, [ - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError(ValidationErrorType.minimumNotMet, path: const []), ]); }); }); @@ -306,8 +307,8 @@ void main() { // 'hi' fails minLength: 3. // The allOf combinator fails, and the specific sub-failure is also reported. expectFailuresMatch(schema, 'hi', [ - ValidationError(ValidationErrorType.allOfNotMet), - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError(ValidationErrorType.allOfNotMet, path: const []), + ValidationError(ValidationErrorType.minLengthNotMet, path: const []), ]); }); @@ -320,9 +321,9 @@ void main() { ); // 'Short123' fails minLength and pattern. expectFailuresMatch(schema, 'Short123', [ - ValidationError(ValidationErrorType.allOfNotMet), - ValidationError(ValidationErrorType.minLengthNotMet), - ValidationError(ValidationErrorType.patternMismatch), + ValidationError(ValidationErrorType.allOfNotMet, path: const []), + ValidationError(ValidationErrorType.minLengthNotMet, path: const []), + ValidationError(ValidationErrorType.patternMismatch, path: const []), ]); }); @@ -332,7 +333,7 @@ void main() { ); // `true` will cause typeMismatch for both StringSchema and NumberSchema. expectFailuresMatch(schema, true, [ - ValidationError(ValidationErrorType.anyOfNotMet), + ValidationError(ValidationErrorType.anyOfNotMet, path: const []), // The specific type mismatches from sub-schemas might also be reported // depending on how _validateSchema handles anyOf error aggregation. // Current tools.dart only adds anyOfNotMet for anyOf. @@ -349,7 +350,7 @@ void main() { // "Hi1" fails minLength: 5 and pattern: '^[a-z]+$'. // Since both sub-schemas fail, anyOfNotMet is reported. expectFailuresMatch(schema, 'Hi1', [ - ValidationError(ValidationErrorType.anyOfNotMet), + ValidationError(ValidationErrorType.anyOfNotMet, path: const []), ]); }); @@ -362,7 +363,7 @@ void main() { ); // `true` matches neither sub-schema. expectFailuresMatch(s, true, [ - ValidationError(ValidationErrorType.oneOfNotMet), + ValidationError(ValidationErrorType.oneOfNotMet, path: const []), ]); }); @@ -372,7 +373,7 @@ void main() { ); // 'test' matches both maxLength: 10 and pattern: 'test'. expectFailuresMatch(schema, 'test', [ - ValidationError(ValidationErrorType.oneOfNotMet), + ValidationError(ValidationErrorType.oneOfNotMet, path: const []), ]); }); @@ -382,7 +383,10 @@ void main() { ); // 'test' matches the second sub-schema in the "not" list. expectFailuresMatch(schema, 'test', [ - ValidationError(ValidationErrorType.notConditionViolated), + ValidationError( + ValidationErrorType.notConditionViolated, + path: const [], + ), ]); }); }); @@ -396,7 +400,8 @@ void main() { [ ValidationError( ValidationErrorType.requiredPropertyMissing, - details: 'Required property "name" is missing.', + path: const [], + details: 'Required property "name" is missing', ), ], ); @@ -411,7 +416,12 @@ void main() { expectFailuresMatch( schema, {'name': 'test', 'age': 30}, - [ValidationError(ValidationErrorType.additionalPropertyNotAllowed)], + [ + ValidationError( + ValidationErrorType.additionalPropertyNotAllowed, + path: const [], + ), + ], ); }); @@ -425,9 +435,9 @@ void main() { schema, {'name': 'test', 'extra': 'abc'}, [ - ValidationError(ValidationErrorType.additionalPropertyNotAllowed), ValidationError( ValidationErrorType.minLengthNotMet, + path: const [], ), // Sub-failure from additionalProperties schema ], ); @@ -438,7 +448,12 @@ void main() { expectFailuresMatch( schema, {'a': 1}, - [ValidationError(ValidationErrorType.minPropertiesNotMet)], + [ + ValidationError( + ValidationErrorType.minPropertiesNotMet, + path: const [], + ), + ], ); }); @@ -447,7 +462,12 @@ void main() { expectFailuresMatch( schema, {'a': 1, 'b': 2}, - [ValidationError(ValidationErrorType.maxPropertiesExceeded)], + [ + ValidationError( + ValidationErrorType.maxPropertiesExceeded, + path: const [], + ), + ], ); }); @@ -458,9 +478,9 @@ void main() { schema, {'ab': 1, 'abc': 2}, [ - ValidationError(ValidationErrorType.propertyNamesInvalid), ValidationError( ValidationErrorType.minLengthNotMet, + path: const [], ), // Sub-failure from propertyNames schema ], ); @@ -475,10 +495,7 @@ void main() { expectFailuresMatch( schema, {'age': 10}, - [ - ValidationError(ValidationErrorType.propertyValueInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); }); @@ -490,10 +507,7 @@ void main() { expectFailuresMatch( schema, {'x-custom': 5}, - [ - ValidationError(ValidationErrorType.patternPropertyValueInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); }); @@ -506,7 +520,12 @@ void main() { expectFailuresMatch( schema, {'name': 'test', 'age': 30}, - [ValidationError(ValidationErrorType.unevaluatedPropertyNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedPropertyNotAllowed, + path: const [], + ), + ], ); }); }); @@ -517,7 +536,7 @@ void main() { expectFailuresMatch( schema, [1], - [ValidationError(ValidationErrorType.minItemsNotMet)], + [ValidationError(ValidationErrorType.minItemsNotMet, path: const [])], ); }); @@ -526,7 +545,12 @@ void main() { expectFailuresMatch( schema, [1, 2], - [ValidationError(ValidationErrorType.maxItemsExceeded)], + [ + ValidationError( + ValidationErrorType.maxItemsExceeded, + path: const [], + ), + ], ); }); @@ -535,7 +559,12 @@ void main() { expectFailuresMatch( schema, [1, 2, 1], - [ValidationError(ValidationErrorType.uniqueItemsViolated)], + [ + ValidationError( + ValidationErrorType.uniqueItemsViolated, + path: const [], + ), + ], ); }); @@ -545,10 +574,7 @@ void main() { expectFailuresMatch( schema, [10, 5, 12], - [ - ValidationError(ValidationErrorType.itemInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); }); @@ -560,18 +586,17 @@ void main() { expectFailuresMatch( schema, [5], // Not enough items for all prefixItems, but first one is checked - [ - ValidationError(ValidationErrorType.prefixItemInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); // Second prefix item 'hi' fails StringSchema(minLength: 3). expectFailuresMatch( schema, [10, 'hi'], [ - ValidationError(ValidationErrorType.prefixItemInvalid), - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError( + ValidationErrorType.minLengthNotMet, + path: const [], + ), ], ); }); @@ -587,7 +612,12 @@ void main() { expectFailuresMatch( schema, [10, 'extra'], - [ValidationError(ValidationErrorType.unevaluatedItemNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedItemNotAllowed, + path: const [], + ), + ], ); }, ); @@ -597,7 +627,7 @@ void main() { test('minLengthNotMet', () { final schema = StringSchema(minLength: 3); expectFailuresMatch(schema, 'hi', [ - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError(ValidationErrorType.minLengthNotMet, path: const []), ]); }); // ... other string specific tests using expectFailuresMatch @@ -607,7 +637,7 @@ void main() { test('minimumNotMet', () { final schema = NumberSchema(minimum: 10); expectFailuresMatch(schema, 5, [ - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError(ValidationErrorType.minimumNotMet, path: const []), ]); }); // ... other number specific tests using expectFailuresMatch @@ -617,7 +647,7 @@ void main() { test('minimumNotMet', () { final schema = IntegerSchema(minimum: 10); expectFailuresMatch(schema, 5, [ - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError(ValidationErrorType.minimumNotMet, path: const []), ]); }); // ... other integer specific tests using expectFailuresMatch @@ -628,7 +658,11 @@ void main() { // Tests specifically for path validation will use expectFailuresExact test('typeMismatch at root has empty path', () { expectFailuresExact(Schema.string(), 123, [ - ValidationError(ValidationErrorType.typeMismatch, path: []), + ValidationError.typeMismatch( + path: [], + expectedType: String, + actualValue: 123, + ), ]); }); @@ -642,7 +676,7 @@ void main() { ValidationErrorType.requiredPropertyMissing, path: [], // Missing property is checked at the current object level (root in this case) - details: 'Required property "name" is missing.', + details: 'Required property "name" is missing', ), ], ); @@ -656,14 +690,10 @@ void main() { schema, {'age': 10}, [ - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: ['age'], - ), ValidationError( ValidationErrorType.minimumNotMet, path: ['age'], - details: 'Value 10 is less than the minimum of 18.', + details: 'Value 10 is less than the minimum of 18', ), // Sub-failure also has the path ], ); @@ -689,24 +719,13 @@ void main() { }, }, // 'street' is missing [ - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: [ - 'user', - 'address', - ], // Path to the object where 'street' is missing - ), - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: ['user'], // Path to the object where 'street' is missing - ), ValidationError( ValidationErrorType.requiredPropertyMissing, path: [ 'user', 'address', ], // Path to the object where 'street' is missing - details: 'Required property "street" is missing.', + details: 'Required property "street" is missing', ), ], ); @@ -718,11 +737,10 @@ void main() { schema, [101, 50, 200], // Item at index 1 (value 50) is invalid [ - ValidationError(ValidationErrorType.itemInvalid, path: ['1']), ValidationError( ValidationErrorType.minimumNotMet, path: ['1'], - details: 'Value 50 is less than the minimum of 100.', + details: 'Value 50 is less than the minimum of 100', ), ], ); @@ -736,17 +754,15 @@ void main() { schema, ['ok', 20], // Item at index 1 (value 20) fails prefixItem schema [ - ValidationError(ValidationErrorType.prefixItemInvalid, path: ['0']), ValidationError( ValidationErrorType.minLengthNotMet, path: ['0'], - details: 'String "ok" is not at least 3 characters long.', + details: 'String "ok" is not at least 3 characters long', ), - ValidationError(ValidationErrorType.prefixItemInvalid, path: ['1']), ValidationError( ValidationErrorType.maximumExceeded, path: ['1'], - details: 'Value 20 is more than the maximum of 10.', + details: 'Value 20 is more than the maximum of 10', ), ], ); @@ -762,12 +778,12 @@ void main() { ValidationError( ValidationErrorType.minLengthNotMet, path: [], - details: 'String "hi" is not at least 3 characters long.', + details: 'String "hi" is not at least 3 characters long', ), // from first sub-schema ValidationError( ValidationErrorType.maxLengthExceeded, path: [], - details: 'String "hi" is more than 1 characters long.', + details: 'String "hi" is more than 1 characters long', ), // from second sub-schema ]); }); @@ -781,14 +797,10 @@ void main() { schema, {'name': 'test', 'extra': 'abc'}, [ - ValidationError( - ValidationErrorType.additionalPropertyNotAllowed, - path: ['extra'], - ), ValidationError( ValidationErrorType.minLengthNotMet, path: ['extra'], - details: 'String "abc" is not at least 5 characters long.', + details: 'String "abc" is not at least 5 characters long', ), ], ); @@ -799,47 +811,47 @@ void main() { group('Type Mismatch', () { test('object schema with non-map data', () { expectFailuresMatch(Schema.object(), 'not a map', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('list schema with non-list data', () { expectFailuresMatch(Schema.list(), 'not a list', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('string schema with non-string data', () { expectFailuresMatch(Schema.string(), 123, [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('number schema with non-num data', () { expectFailuresMatch(Schema.num(), 'not a number', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('integer schema with non-int data', () { expectFailuresMatch(Schema.int(), 'not an int', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('integer schema with non-integer num data', () { expectFailuresMatch(Schema.int(), 10.5, [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('boolean schema with non-bool data', () { expectFailuresMatch(Schema.bool(), 'not a bool', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('null schema with non-null data', () { expectFailuresMatch(Schema.nil(), 'not null', [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); test('integer schema with integer-like num data (e.g. 10.0)', () { expectFailuresMatch(IntegerSchema(minimum: 11), 10.0, [ - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError(ValidationErrorType.minimumNotMet, path: const []), ]); }); }); @@ -848,7 +860,10 @@ void main() { test('enumValueNotAllowed', () { final schema = EnumSchema(values: {'a', 'b'}); expectFailuresMatch(schema, 'c', [ - ValidationError(ValidationErrorType.enumValueNotAllowed), + ValidationError( + ValidationErrorType.enumValueNotAllowed, + path: const [], + ), ]); }); @@ -860,7 +875,7 @@ void main() { test('enum with non-string data', () { final schema = EnumSchema(values: {'a', 'b'}); expectFailuresMatch(schema, 1, [ - ValidationError(ValidationErrorType.typeMismatch), + ValidationError(ValidationErrorType.typeMismatch, path: const []), ]); }); }); @@ -871,8 +886,8 @@ void main() { allOf: [StringSchema(minLength: 3), StringSchema(maxLength: 5)], ); expectFailuresMatch(schema, 'hi', [ - ValidationError(ValidationErrorType.allOfNotMet), - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError(ValidationErrorType.allOfNotMet, path: const []), + ValidationError(ValidationErrorType.minLengthNotMet, path: const []), ]); }); @@ -884,9 +899,9 @@ void main() { ], ); expectFailuresMatch(schema, 'Short123', [ - ValidationError(ValidationErrorType.allOfNotMet), - ValidationError(ValidationErrorType.minLengthNotMet), - ValidationError(ValidationErrorType.patternMismatch), + ValidationError(ValidationErrorType.allOfNotMet, path: const []), + ValidationError(ValidationErrorType.minLengthNotMet, path: const []), + ValidationError(ValidationErrorType.patternMismatch, path: const []), ]); }); @@ -900,7 +915,7 @@ void main() { // NumberSchema(minimum: 100).validate(true) -> [typeMismatch] // So anyOf fails. expectFailuresMatch(schema, true, [ - ValidationError(ValidationErrorType.anyOfNotMet), + ValidationError(ValidationErrorType.anyOfNotMet, path: const []), ]); }); @@ -916,7 +931,7 @@ void main() { // StringSchema(pattern: '^[a-z]+$').validate("Hi1") -> [patternMismatch] // Since both fail, anyOf fails. expectFailuresMatch(schema, 'Hi1', [ - ValidationError(ValidationErrorType.anyOfNotMet), + ValidationError(ValidationErrorType.anyOfNotMet, path: const []), ]); }); @@ -933,7 +948,7 @@ void main() { ], ); expectFailuresMatch(s, true, [ - ValidationError(ValidationErrorType.oneOfNotMet), + ValidationError(ValidationErrorType.oneOfNotMet, path: const []), ]); }); @@ -942,7 +957,7 @@ void main() { oneOf: [StringSchema(maxLength: 10), StringSchema(pattern: 'test')], ); expectFailuresMatch(schema, 'test', [ - ValidationError(ValidationErrorType.oneOfNotMet), + ValidationError(ValidationErrorType.oneOfNotMet, path: const []), ]); }); @@ -951,7 +966,10 @@ void main() { not: [StringSchema(maxLength: 2), StringSchema(pattern: 'test')], ); expectFailuresMatch(schema, 'test', [ - ValidationError(ValidationErrorType.notConditionViolated), + ValidationError( + ValidationErrorType.notConditionViolated, + path: const [], + ), ]); }); @@ -960,7 +978,14 @@ void main() { not: [StringSchema(maxLength: 10), StringSchema(pattern: 'test')], ); expectFailuresMatch(schema, 'test', [ - ValidationError(ValidationErrorType.notConditionViolated), + ValidationError( + ValidationErrorType.notConditionViolated, + path: const [], + ), + ValidationError( + ValidationErrorType.notConditionViolated, + path: const [], + ), ]); }); }); @@ -971,7 +996,12 @@ void main() { expectFailuresMatch( schema, {'foo': 1}, - [ValidationError(ValidationErrorType.requiredPropertyMissing)], + [ + ValidationError( + ValidationErrorType.requiredPropertyMissing, + path: const [], + ), + ], ); }); @@ -983,7 +1013,12 @@ void main() { expectFailuresMatch( schema, {'name': 'test', 'age': 30}, - [ValidationError(ValidationErrorType.additionalPropertyNotAllowed)], + [ + ValidationError( + ValidationErrorType.additionalPropertyNotAllowed, + path: const [], + ), + ], ); }); @@ -996,8 +1031,10 @@ void main() { schema, {'name': 'test', 'extra': 'abc'}, [ - ValidationError(ValidationErrorType.minLengthNotMet), - ValidationError(ValidationErrorType.additionalPropertyNotAllowed), + ValidationError( + ValidationErrorType.minLengthNotMet, + path: const [], + ), ], ); }); @@ -1007,7 +1044,12 @@ void main() { expectFailuresMatch( schema, {'a': 1}, - [ValidationError(ValidationErrorType.minPropertiesNotMet)], + [ + ValidationError( + ValidationErrorType.minPropertiesNotMet, + path: const [], + ), + ], ); }); @@ -1016,7 +1058,12 @@ void main() { expectFailuresMatch( schema, {'a': 1, 'b': 2}, - [ValidationError(ValidationErrorType.maxPropertiesExceeded)], + [ + ValidationError( + ValidationErrorType.maxPropertiesExceeded, + path: const [], + ), + ], ); }); @@ -1026,8 +1073,10 @@ void main() { schema, {'ab': 1, 'abc': 2}, [ - ValidationError(ValidationErrorType.minLengthNotMet), - ValidationError(ValidationErrorType.propertyNamesInvalid), + ValidationError( + ValidationErrorType.minLengthNotMet, + path: const [], + ), ], ); }); @@ -1039,10 +1088,7 @@ void main() { expectFailuresMatch( schema, {'age': 10}, - [ - ValidationError(ValidationErrorType.propertyValueInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); }); @@ -1053,10 +1099,7 @@ void main() { expectFailuresMatch( schema, {'x-custom': 5}, - [ - ValidationError(ValidationErrorType.patternPropertyValueInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); }); @@ -1069,7 +1112,12 @@ void main() { expectFailuresMatch( schema, {'name': 'test', 'age': 30}, - [ValidationError(ValidationErrorType.unevaluatedPropertyNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedPropertyNotAllowed, + path: const [], + ), + ], ); }); @@ -1103,7 +1151,7 @@ void main() { expectFailuresMatch( schema, [1], - [ValidationError(ValidationErrorType.minItemsNotMet)], + [ValidationError(ValidationErrorType.minItemsNotMet, path: const [])], ); }); @@ -1112,7 +1160,12 @@ void main() { expectFailuresMatch( schema, [1, 2], - [ValidationError(ValidationErrorType.maxItemsExceeded)], + [ + ValidationError( + ValidationErrorType.maxItemsExceeded, + path: const [], + ), + ], ); }); @@ -1121,7 +1174,12 @@ void main() { expectFailuresMatch( schema, [1, 2, 1], - [ValidationError(ValidationErrorType.uniqueItemsViolated)], + [ + ValidationError( + ValidationErrorType.uniqueItemsViolated, + path: const [], + ), + ], ); }); @@ -1132,10 +1190,7 @@ void main() { expectFailuresMatch( schema, [10, 5, 12], - [ - ValidationError(ValidationErrorType.itemInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); }); @@ -1146,17 +1201,16 @@ void main() { expectFailuresMatch( schema, [5], - [ - ValidationError(ValidationErrorType.prefixItemInvalid), - ValidationError(ValidationErrorType.minimumNotMet), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); expectFailuresMatch( schema, [10, 'hi'], [ - ValidationError(ValidationErrorType.prefixItemInvalid), - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError( + ValidationErrorType.minLengthNotMet, + path: const [], + ), ], ); }); @@ -1171,7 +1225,12 @@ void main() { expectFailuresMatch( schema, [10, 'extra'], - [ValidationError(ValidationErrorType.unevaluatedItemNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedItemNotAllowed, + path: const [], + ), + ], ); }, ); @@ -1181,7 +1240,12 @@ void main() { expectFailuresMatch( schema, ['extra'], - [ValidationError(ValidationErrorType.unevaluatedItemNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedItemNotAllowed, + path: const [], + ), + ], ); }); @@ -1204,8 +1268,10 @@ void main() { schemaWithItems, [10, 'a'], [ - ValidationError(ValidationErrorType.itemInvalid), - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError( + ValidationErrorType.minLengthNotMet, + path: const [], + ), ], ); @@ -1218,7 +1284,12 @@ void main() { expectFailuresMatch( schemaNoItems, [10, 'extra string'], - [ValidationError(ValidationErrorType.unevaluatedItemNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedItemNotAllowed, + path: const [], + ), + ], ); }); @@ -1232,10 +1303,7 @@ void main() { expectFailuresMatch( schema, [10, 'hello', true], // `true` is unevaluated but allowed - [ - ValidationError(ValidationErrorType.itemInvalid), - ValidationError(ValidationErrorType.typeMismatch), - ], + [ValidationError(ValidationErrorType.typeMismatch, path: const [])], reason: 'Item `true` at index 2 is evaluated by `items: StringSchema()` ' 'and fails. `unevaluatedItems` (defaulting to true) does not apply ' @@ -1248,21 +1316,24 @@ void main() { test('minLengthNotMet', () { final schema = StringSchema(minLength: 3); expectFailuresMatch(schema, 'hi', [ - ValidationError(ValidationErrorType.minLengthNotMet), + ValidationError(ValidationErrorType.minLengthNotMet, path: const []), ]); }); test('maxLengthExceeded', () { final schema = StringSchema(maxLength: 3); expectFailuresMatch(schema, 'hello', [ - ValidationError(ValidationErrorType.maxLengthExceeded), + ValidationError( + ValidationErrorType.maxLengthExceeded, + path: const [], + ), ]); }); test('patternMismatch', () { final schema = StringSchema(pattern: r'^\d+$'); expectFailuresMatch(schema, 'abc', [ - ValidationError(ValidationErrorType.patternMismatch), + ValidationError(ValidationErrorType.patternMismatch, path: const []), ]); }); }); @@ -1271,53 +1342,71 @@ void main() { test('minimumNotMet', () { final schema = NumberSchema(minimum: 10); expectFailuresMatch(schema, 5, [ - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError(ValidationErrorType.minimumNotMet, path: const []), ]); }); test('maximumExceeded', () { final schema = NumberSchema(maximum: 10); expectFailuresMatch(schema, 15, [ - ValidationError(ValidationErrorType.maximumExceeded), + ValidationError(ValidationErrorType.maximumExceeded, path: const []), ]); }); test('exclusiveMinimumNotMet - equal value', () { final schema = NumberSchema(exclusiveMinimum: 10); expectFailuresMatch(schema, 10, [ - ValidationError(ValidationErrorType.exclusiveMinimumNotMet), + ValidationError( + ValidationErrorType.exclusiveMinimumNotMet, + path: const [], + ), ]); }); test('exclusiveMinimumNotMet - smaller value', () { final schema = NumberSchema(exclusiveMinimum: 10); expectFailuresMatch(schema, 9, [ - ValidationError(ValidationErrorType.exclusiveMinimumNotMet), + ValidationError( + ValidationErrorType.exclusiveMinimumNotMet, + path: const [], + ), ]); }); test('exclusiveMaximumExceeded - equal value', () { final schema = NumberSchema(exclusiveMaximum: 10); expectFailuresMatch(schema, 10, [ - ValidationError(ValidationErrorType.exclusiveMaximumExceeded), + ValidationError( + ValidationErrorType.exclusiveMaximumExceeded, + path: const [], + ), ]); }); test('exclusiveMaximumExceeded - larger value', () { final schema = NumberSchema(exclusiveMaximum: 10); expectFailuresMatch(schema, 11, [ - ValidationError(ValidationErrorType.exclusiveMaximumExceeded), + ValidationError( + ValidationErrorType.exclusiveMaximumExceeded, + path: const [], + ), ]); }); test('multipleOfInvalid', () { final schema = NumberSchema(multipleOf: 3); expectFailuresMatch(schema, 10, [ - ValidationError(ValidationErrorType.multipleOfInvalid), + ValidationError( + ValidationErrorType.multipleOfInvalid, + path: const [], + ), ]); }); test('multipleOfInvalid - floating point', () { final schema = NumberSchema(multipleOf: 0.1); expectFailuresMatch(schema, 0.25, [ - ValidationError(ValidationErrorType.multipleOfInvalid), + ValidationError( + ValidationErrorType.multipleOfInvalid, + path: const [], + ), ]); }); test('multipleOfInvalid - valid floating point', () { @@ -1343,47 +1432,62 @@ void main() { test('minimumNotMet', () { final schema = IntegerSchema(minimum: 10); expectFailuresMatch(schema, 5, [ - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError(ValidationErrorType.minimumNotMet, path: const []), ]); }); test('maximumExceeded', () { final schema = IntegerSchema(maximum: 10); expectFailuresMatch(schema, 15, [ - ValidationError(ValidationErrorType.maximumExceeded), + ValidationError(ValidationErrorType.maximumExceeded, path: const []), ]); }); test('exclusiveMinimumNotMet - equal value', () { final schema = IntegerSchema(exclusiveMinimum: 10); expectFailuresMatch(schema, 10, [ - ValidationError(ValidationErrorType.exclusiveMinimumNotMet), + ValidationError( + ValidationErrorType.exclusiveMinimumNotMet, + path: const [], + ), ]); }); test('exclusiveMinimumNotMet - smaller value', () { final schema = IntegerSchema(exclusiveMinimum: 10); expectFailuresMatch(schema, 9, [ - ValidationError(ValidationErrorType.exclusiveMinimumNotMet), + ValidationError( + ValidationErrorType.exclusiveMinimumNotMet, + path: const [], + ), ]); }); test('exclusiveMaximumExceeded - equal value', () { final schema = IntegerSchema(exclusiveMaximum: 10); expectFailuresMatch(schema, 10, [ - ValidationError(ValidationErrorType.exclusiveMaximumExceeded), + ValidationError( + ValidationErrorType.exclusiveMaximumExceeded, + path: const [], + ), ]); }); test('exclusiveMaximumExceeded - larger value', () { final schema = IntegerSchema(exclusiveMaximum: 10); expectFailuresMatch(schema, 11, [ - ValidationError(ValidationErrorType.exclusiveMaximumExceeded), + ValidationError( + ValidationErrorType.exclusiveMaximumExceeded, + path: const [], + ), ]); }); test('multipleOfInvalid', () { final schema = IntegerSchema(multipleOf: 3); expectFailuresMatch(schema, 10, [ - ValidationError(ValidationErrorType.multipleOfInvalid), + ValidationError( + ValidationErrorType.multipleOfInvalid, + path: const [], + ), ]); }); @@ -1417,12 +1521,12 @@ void main() { final schemaAnyOfEmpty = Schema.combined(anyOf: []); expectFailuresMatch(schemaAnyOfEmpty, 'any data', [ - ValidationError(ValidationErrorType.anyOfNotMet), + ValidationError(ValidationErrorType.anyOfNotMet, path: const []), ]); final schemaOneOfEmpty = Schema.combined(oneOf: []); expectFailuresMatch(schemaOneOfEmpty, 'any data', [ - ValidationError(ValidationErrorType.oneOfNotMet), + ValidationError(ValidationErrorType.oneOfNotMet, path: const []), ]); // If 'not' is a list of schemas, and the list is empty, @@ -1454,16 +1558,16 @@ void main() { // Fails minLength (from parent StringSchema) and pattern (from allOf) expectFailuresExact(schema, 'A', [ + ValidationError(ValidationErrorType.allOfNotMet, path: []), ValidationError( ValidationErrorType.minLengthNotMet, path: [], - details: 'String "A" is not at least 2 characters long.', + details: 'String "A" is not at least 2 characters long', ), - ValidationError(ValidationErrorType.allOfNotMet, path: []), ValidationError( ValidationErrorType.patternMismatch, path: [], - details: 'String "A" doesn\'t match the pattern "^[a-z]+\$".', + details: 'String "A" doesn\'t match the pattern "^[a-z]+\$"', ), ]); @@ -1473,7 +1577,7 @@ void main() { ValidationError( ValidationErrorType.maxLengthExceeded, path: [], - details: 'String "abcdef" is more than 5 characters long.', + details: 'String "abcdef" is more than 5 characters long', ), ]); }); @@ -1496,18 +1600,10 @@ void main() { 'user': {'name': 'hi'}, }, [ - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: ['user'], - ), - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: ['user', 'name'], - ), ValidationError( ValidationErrorType.minLengthNotMet, path: ['user', 'name'], - details: 'String "hi" is not at least 5 characters long.', + details: 'String "hi" is not at least 5 characters long', ), ], ); @@ -1527,15 +1623,10 @@ void main() { {'id': 10}, // This item is invalid ], [ - ValidationError(ValidationErrorType.itemInvalid, path: ['1']), - ValidationError( - ValidationErrorType.propertyValueInvalid, - path: ['1', 'id'], - ), ValidationError( ValidationErrorType.minimumNotMet, path: ['1', 'id'], - details: 'Value 10 is less than the minimum of 100.', + details: 'Value 10 is less than the minimum of 100', ), ], ); @@ -1552,19 +1643,13 @@ void main() { expectFailuresMatch( schema, {'known': 'yes', 'extraNum': -5}, - [ - ValidationError(ValidationErrorType.minimumNotMet), - ValidationError(ValidationErrorType.additionalPropertyNotAllowed), - ], + [ValidationError(ValidationErrorType.minimumNotMet, path: const [])], ); // Invalid: additional property is wrong type for its schema expectFailuresMatch( schema, {'known': 'yes', 'extraStr': 'text'}, - [ - ValidationError(ValidationErrorType.typeMismatch), - ValidationError(ValidationErrorType.additionalPropertyNotAllowed), - ], + [ValidationError(ValidationErrorType.typeMismatch, path: const [])], ); }); @@ -1579,7 +1664,12 @@ void main() { expectFailuresMatch( schema, {'y-foo': 'bar'}, - [ValidationError(ValidationErrorType.unevaluatedPropertyNotAllowed)], + [ + ValidationError( + ValidationErrorType.unevaluatedPropertyNotAllowed, + path: const [], + ), + ], ); }); @@ -1627,8 +1717,10 @@ void main() { schema, {'name': 'test', 'age': 30}, [ - ValidationError(ValidationErrorType.additionalPropertyNotAllowed), - ValidationError(ValidationErrorType.minimumNotMet), + ValidationError( + ValidationErrorType.minimumNotMet, + path: const [], + ), ], ); }, @@ -1646,10 +1738,7 @@ void main() { expectFailuresMatch( schema, [1, 'b', 3], - [ - ValidationError(ValidationErrorType.itemInvalid), - ValidationError(ValidationErrorType.typeMismatch), - ], + [ValidationError(ValidationErrorType.typeMismatch, path: const [])], ); }); @@ -1667,19 +1756,13 @@ void main() { expectFailuresMatch( schema, ['a', 1, 'c'], - [ - ValidationError(ValidationErrorType.itemInvalid), - ValidationError(ValidationErrorType.typeMismatch), - ], + [ValidationError(ValidationErrorType.typeMismatch, path: const [])], ); // Invalid: prefixItem fails StringSchema expectFailuresMatch( schema, [10, 1, 2], - [ - ValidationError(ValidationErrorType.prefixItemInvalid), - ValidationError(ValidationErrorType.typeMismatch), - ], + [ValidationError(ValidationErrorType.typeMismatch, path: const [])], ); }, ); @@ -1716,13 +1799,13 @@ void main() { ValidationError( ValidationErrorType.minLengthNotMet, path: [], - details: 'String "Hi" is not at least 5 characters long.', + details: 'String "Hi" is not at least 5 characters long', ), ValidationError(ValidationErrorType.allOfNotMet, path: []), ValidationError( ValidationErrorType.patternMismatch, path: [], - details: 'String "Hi" doesn\'t match the pattern "^[a-z]+\$".', + details: 'String "Hi" doesn\'t match the pattern "^[a-z]+\$"', ), ]); @@ -1734,7 +1817,7 @@ void main() { ValidationError( ValidationErrorType.patternMismatch, path: [], - details: 'String "Hiall" doesn\'t match the pattern "^[a-z]+\$".', + details: 'String "Hiall" doesn\'t match the pattern "^[a-z]+\$"', ), ]); @@ -1756,7 +1839,7 @@ void main() { ValidationErrorType.patternMismatch, path: [], details: - 'String "LongEnoughButCAPS" doesn\'t match the pattern "^[a-z]+\$".', + 'String "LongEnoughButCAPS" doesn\'t match the pattern "^[a-z]+\$"', ), ]); }, diff --git a/pkgs/dart_mcp/test/server/tools_support_test.dart b/pkgs/dart_mcp/test/server/tools_support_test.dart index 75721eb2..337082a7 100644 --- a/pkgs/dart_mcp/test/server/tools_support_test.dart +++ b/pkgs/dart_mcp/test/server/tools_support_test.dart @@ -24,9 +24,11 @@ void main() { final serverConnection = environment.serverConnection; final toolsResult = await serverConnection.listTools(); - expect(toolsResult.tools.length, 1); + expect(toolsResult.tools.length, 2); - final tool = toolsResult.tools.single; + final tool = toolsResult.tools.firstWhere( + (tool) => tool.name == TestMCPServerWithTools.helloWorld.name, + ); final result = await serverConnection.callTool( CallToolRequest(name: tool.name), @@ -72,6 +74,46 @@ void main() { // Need to manually close so the stream matchers can complete. await environment.shutdown(); }); + + test('schema validation failure returns an error', () async { + final environment = TestEnvironment( + TestMCPClient(), + TestMCPServerWithTools.new, + ); + await environment.initializeServer(); + + final serverConnection = environment.serverConnection; + + // Call with no arguments, should fail because 'message' is required. + var result = await serverConnection.callTool( + CallToolRequest( + name: TestMCPServerWithTools.echo.name, + arguments: const {}, + ), + ); + expect(result.isError, isTrue); + expect(result.content.single, isA()); + final textContent = result.content.single as TextContent; + expect( + textContent.text, + contains('Required property "message" is missing at path #root'), + ); + + // Call with wrong type for 'message'. + result = await serverConnection.callTool( + CallToolRequest( + name: TestMCPServerWithTools.echo.name, + arguments: {'message': 123}, + ), + ); + expect(result.isError, isTrue); + expect(result.content.single, isA()); + final textContent2 = result.content.single as TextContent; + expect( + textContent2.text, + contains('Value `123` is not of type `String` at path #root["message"]'), + ); + }); } final class TestMCPServerWithTools extends TestMCPServer with ToolsSupport { @@ -83,9 +125,24 @@ final class TestMCPServerWithTools extends TestMCPServer with ToolsSupport { helloWorld, (_) => CallToolResult(content: [helloWorldContent]), ); + registerTool(TestMCPServerWithTools.echo, TestMCPServerWithTools.echoImpl); return super.initialize(request); } + static final echo = Tool( + name: 'echo', + description: 'Echoes the input', + inputSchema: ObjectSchema( + properties: {'message': StringSchema(description: 'The message to echo')}, + required: ['message'], + ), + ); + + static CallToolResult echoImpl(CallToolRequest request) { + final message = request.arguments!['message'] as String; + return CallToolResult(content: [TextContent(text: message)]); + } + static final helloWorld = Tool( name: 'hello_world', description: 'Says hello world!', diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart b/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart index 70fc5a6b..ef4bb554 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart @@ -96,7 +96,7 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport if (projectType != 'dart' && projectType != 'flutter') { errors.add( ValidationError( - ValidationErrorType.itemInvalid, + ValidationErrorType.custom, path: [ParameterNames.projectType], details: 'Only `dart` and `flutter` are allowed values.', ), @@ -106,7 +106,7 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport if (p.isAbsolute(directory)) { errors.add( ValidationError( - ValidationErrorType.itemInvalid, + ValidationErrorType.custom, path: [ParameterNames.directory], details: 'Directory must be a relative path.', ), @@ -125,7 +125,7 @@ base mixin DashCliSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport : 'is not a valid platform'; errors.add( ValidationError( - ValidationErrorType.itemInvalid, + ValidationErrorType.custom, path: [ParameterNames.platform], details: '${invalidPlatforms.join(',')} $plural. Platforms ' diff --git a/pkgs/dart_mcp_server/pubspec.yaml b/pkgs/dart_mcp_server/pubspec.yaml index b78cbf73..60f4ccf7 100644 --- a/pkgs/dart_mcp_server/pubspec.yaml +++ b/pkgs/dart_mcp_server/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: args: ^2.7.0 async: ^2.13.0 collection: ^1.19.1 - dart_mcp: ^0.2.2 + dart_mcp: ^0.2.3 dds_service_extensions: ^2.0.1 devtools_shared: ^11.2.0 dtd: ^2.4.0 From 1f134d5c2f391d4a7a01e446af971e05156bbc5c Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Thu, 26 Jun 2025 21:59:39 +0000 Subject: [PATCH 2/3] make it a breaking change since the Dart MCP server overrides registerTool for analytics --- pkgs/dart_mcp/CHANGELOG.md | 6 ++---- pkgs/dart_mcp/pubspec.yaml | 2 +- pkgs/dart_mcp_server/lib/src/server.dart | 6 ++++-- pkgs/dart_mcp_server/pubspec.yaml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkgs/dart_mcp/CHANGELOG.md b/pkgs/dart_mcp/CHANGELOG.md index e0b7f61e..af4d70d7 100644 --- a/pkgs/dart_mcp/CHANGELOG.md +++ b/pkgs/dart_mcp/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.2.3-wip +## 0.3.0-wip - Added error checking to required fields of all `Request` subclasses so that they will throw helpful errors when accessed and not set. @@ -12,9 +12,7 @@ - Added a `custom` validation error type. - Auto-validate schemas for all tools by default. This can be disabled by passing `validateArguments: false` to `registerTool`. - - While this could be breaking, we are not treating it as such as it should - not be breaking and will generally result in better errors for invalid - arguments. + - This is breaking since this method is overridden by the Dart MCP server. ## 0.2.2 diff --git a/pkgs/dart_mcp/pubspec.yaml b/pkgs/dart_mcp/pubspec.yaml index 3a98683f..0e48b2a9 100644 --- a/pkgs/dart_mcp/pubspec.yaml +++ b/pkgs/dart_mcp/pubspec.yaml @@ -1,5 +1,5 @@ name: dart_mcp -version: 0.2.3-wip +version: 0.3.0-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_server/lib/src/server.dart b/pkgs/dart_mcp_server/lib/src/server.dart index f90a80f2..04af79d4 100644 --- a/pkgs/dart_mcp_server/lib/src/server.dart +++ b/pkgs/dart_mcp_server/lib/src/server.dart @@ -161,8 +161,9 @@ final class DartMCPServer extends MCPServer /// if [analytics] is not `null`. void registerTool( Tool tool, - FutureOr Function(CallToolRequest) impl, - ) { + FutureOr Function(CallToolRequest) impl, { + bool validateArguments = true, + }) { // For type promotion. final analytics = this.analytics; @@ -196,6 +197,7 @@ final class DartMCPServer extends MCPServer } } }, + validateArguments: validateArguments, ); } diff --git a/pkgs/dart_mcp_server/pubspec.yaml b/pkgs/dart_mcp_server/pubspec.yaml index 60f4ccf7..84c1f07f 100644 --- a/pkgs/dart_mcp_server/pubspec.yaml +++ b/pkgs/dart_mcp_server/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: args: ^2.7.0 async: ^2.13.0 collection: ^1.19.1 - dart_mcp: ^0.2.3 + dart_mcp: ^0.3.0 dds_service_extensions: ^2.0.1 devtools_shared: ^11.2.0 dtd: ^2.4.0 From 28a47f90eb4e38f57ca7dcd2b5ac558b41e68bf3 Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Fri, 27 Jun 2025 01:38:50 +0000 Subject: [PATCH 3/3] remove custom validation in dart mcp server, update tests --- pkgs/dart_mcp_server/lib/src/mixins/dtd.dart | 9 --------- pkgs/dart_mcp_server/lib/src/mixins/pub.dart | 8 +------- pkgs/dart_mcp_server/test/tools/dtd_test.dart | 2 +- pkgs/dart_mcp_server/test/tools/pub_test.dart | 2 +- .../plugins/GeneratedPluginRegistrant.java | 19 +++++++++++++++++++ .../counter_app/android/local.properties | 1 + 6 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 pkgs/dart_mcp_server/test_fixtures/counter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java create mode 100644 pkgs/dart_mcp_server/test_fixtures/counter_app/android/local.properties diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart index c390004c..8fe2f8e0 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart @@ -187,15 +187,6 @@ base mixin DartToolingDaemonSupport return _dtdAlreadyConnected; } - if (request.arguments?[ParameterNames.uri] == null) { - return CallToolResult( - isError: true, - content: [ - TextContent(text: 'Required parameter "uri" was not provided.'), - ], - ); - } - try { _dtd = await DartToolingDaemon.connect( Uri.parse(request.arguments![ParameterNames.uri] as String), diff --git a/pkgs/dart_mcp_server/lib/src/mixins/pub.dart b/pkgs/dart_mcp_server/lib/src/mixins/pub.dart index ab485001..9059a108 100644 --- a/pkgs/dart_mcp_server/lib/src/mixins/pub.dart +++ b/pkgs/dart_mcp_server/lib/src/mixins/pub.dart @@ -34,13 +34,7 @@ base mixin PubSupport on ToolsSupport, LoggingSupport, RootsTrackingSupport /// Implementation of the [pubTool]. Future _runDartPubTool(CallToolRequest request) async { - final command = request.arguments?[ParameterNames.command] as String?; - if (command == null) { - return CallToolResult( - content: [TextContent(text: 'Missing required argument `command`.')], - isError: true, - ); - } + final command = request.arguments![ParameterNames.command] as String; final matchingCommand = SupportedPubCommand.fromName(command); if (matchingCommand == null) { return CallToolResult( diff --git a/pkgs/dart_mcp_server/test/tools/dtd_test.dart b/pkgs/dart_mcp_server/test/tools/dtd_test.dart index 713fd1a2..bf92e23f 100644 --- a/pkgs/dart_mcp_server/test/tools/dtd_test.dart +++ b/pkgs/dart_mcp_server/test/tools/dtd_test.dart @@ -607,7 +607,7 @@ void main() { expect(missingArgResult.isError, isTrue); expect( (missingArgResult.content.first as TextContent).text, - 'Required parameter "enabled" was not provided or is not a boolean.', + 'Required property "enabled" is missing at path #root', ); // Clean up diff --git a/pkgs/dart_mcp_server/test/tools/pub_test.dart b/pkgs/dart_mcp_server/test/tools/pub_test.dart index 841a60d6..e349db1d 100644 --- a/pkgs/dart_mcp_server/test/tools/pub_test.dart +++ b/pkgs/dart_mcp_server/test/tools/pub_test.dart @@ -192,7 +192,7 @@ void main() { expect( (result.content.single as TextContent).text, - 'Missing required argument `command`.', + 'Required property "command" is missing at path #root', ); expect(testProcessManager.commandsRan, isEmpty); }); diff --git a/pkgs/dart_mcp_server/test_fixtures/counter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/pkgs/dart_mcp_server/test_fixtures/counter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 00000000..539ab022 --- /dev/null +++ b/pkgs/dart_mcp_server/test_fixtures/counter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,19 @@ +package io.flutter.plugins; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import io.flutter.Log; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +@Keep +public final class GeneratedPluginRegistrant { + private static final String TAG = "GeneratedPluginRegistrant"; + public static void registerWith(@NonNull FlutterEngine flutterEngine) { + } +} diff --git a/pkgs/dart_mcp_server/test_fixtures/counter_app/android/local.properties b/pkgs/dart_mcp_server/test_fixtures/counter_app/android/local.properties new file mode 100644 index 00000000..be68df01 --- /dev/null +++ b/pkgs/dart_mcp_server/test_fixtures/counter_app/android/local.properties @@ -0,0 +1 @@ +flutter.sdk=/usr/local/google/home/jakemac/flutter \ No newline at end of file