Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions mcp_examples/bin/workflow_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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}',
Expand Down
7 changes: 7 additions & 0 deletions pkgs/dart_mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 5 additions & 2 deletions pkgs/dart_mcp/example/elicitations_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion pkgs/dart_mcp/example/elicitations_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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']),
},
),
),
Expand Down
3 changes: 2 additions & 1 deletion pkgs/dart_mcp/lib/src/api/elicitation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ extension type ElicitRequest._fromMap(Map<String, Object?> _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:
Expand Down
66 changes: 30 additions & 36 deletions pkgs/dart_mcp/lib/src/api/tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ enum JsonType {
num('number'),
int('integer'),
bool('boolean'),
@Deprecated('Use JsonType.string')
enumeration('enum'),
nil('null');

Expand Down Expand Up @@ -405,6 +406,7 @@ extension type Schema.fromMap(Map<String, Object?> _value) {
static const object = ObjectSchema.new;

/// Alias for [EnumSchema.new].
@Deprecated('Use Schema.string instead')
static const enumeration = EnumSchema.new;

/// Alias for [NullSchema.new].
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -1077,13 +1075,15 @@ extension type const StringSchema.fromMap(Map<String, Object?> _value)
int? minLength,
int? maxLength,
String? pattern,
Iterable<String>? enumValues,
}) => StringSchema.fromMap({
'type': JsonType.string.typeName,
if (title != null) 'title': title,
if (description != null) 'description': description,
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.
Expand All @@ -1095,6 +1095,16 @@ extension type const StringSchema.fromMap(Map<String, Object?> _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<String>? get enumValues {
final values = (_value['enum'] as Iterable?)?.cast<String>();
assert(
values?.toSet().length == values?.length,
"The 'enum' property has duplicate entries.",
);
return values;
}

bool _validateString(
Object? data,
List<String> currentPath,
Expand Down Expand Up @@ -1142,13 +1152,27 @@ extension type const StringSchema.fromMap(Map<String, Object?> _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<String, Object?> _value)
implements Schema {
@Deprecated('Use StringSchema instead')
factory EnumSchema({
String? title,
String? description,
Expand Down Expand Up @@ -1178,36 +1202,6 @@ extension type EnumSchema.fromMap(Map<String, Object?> _value)
);
return values;
}

bool _validateEnum(
Object? data,
List<String> currentPath,
HashSet<ValidationError> 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].
Expand Down
2 changes: 1 addition & 1 deletion pkgs/dart_mcp/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
47 changes: 23 additions & 24 deletions pkgs/dart_mcp/test/api/tools_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ void main() {
});

test('EnumSchema', () {
// ignore: deprecated_member_use_from_same_package
final schema = EnumSchema(
title: 'Foo',
description: 'Bar',
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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', () {
Expand Down
32 changes: 16 additions & 16 deletions pkgs/dart_mcp_server/lib/src/mixins/dtd.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
),
Expand Down Expand Up @@ -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',
Expand All @@ -806,23 +806,23 @@ 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',
'bottomRight',
'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:
Expand Down
Loading