Skip to content

Add enum support to Schema #190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 25, 2025
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
1 change: 1 addition & 0 deletions pkgs/dart_mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- Added error checking to required fields of all `Request` subclasses so that
they will throw helpful errors when accessed and not set.
- Added enum support to Schema.

## 0.2.2

Expand Down
105 changes: 101 additions & 4 deletions pkgs/dart_mcp/lib/src/api/tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ extension type ListToolsResult.fromMap(Map<String, Object?> _value)
if (meta != null) '_meta': meta,
});

List<Tool> get tools => (_value['tools'] as List).cast<Tool>();
List<Tool> get tools {
final tools = (_value['tools'] as List?)?.cast<Tool>();
if (tools == null) {
throw ArgumentError('Missing tools field in $ListToolsResult');
}
return tools;
}
}

/// The server's response to a tool call.
Expand All @@ -56,7 +62,13 @@ extension type CallToolResult.fromMap(Map<String, Object?> _value)

/// The type of content, either [TextContent], [ImageContent],
/// or [EmbeddedResource],
List<Content> get content => (_value['content'] as List).cast<Content>();
List<Content> get content {
final content = (_value['content'] as List?)?.cast<Content>();
if (content == null) {
throw ArgumentError('Missing content field in $CallToolResult');
}
return content;
}

/// Whether the tool call ended in an error.
///
Expand Down Expand Up @@ -129,14 +141,26 @@ extension type Tool.fromMap(Map<String, Object?> _value) {
as ToolAnnotations?;

/// The name of the tool.
String get name => _value['name'] as String;
String get name {
final name = _value['name'] as String?;
if (name == null) {
throw ArgumentError('Missing name field in $Tool');
}
return name;
}

/// A human-readable description of the tool.
String? get description => _value['description'] as String?;

/// A JSON [ObjectSchema] object defining the expected parameters for the
/// tool.
ObjectSchema get inputSchema => _value['inputSchema'] as ObjectSchema;
ObjectSchema get inputSchema {
final inputSchema = _value['inputSchema'] as ObjectSchema?;
if (inputSchema == null) {
throw ArgumentError('Missing inputSchema field in $Tool');
}
return inputSchema;
}
}

/// Additional properties describing a Tool to clients.
Expand Down Expand Up @@ -196,6 +220,7 @@ enum JsonType {
num('number'),
int('integer'),
bool('boolean'),
enumeration('enum'),
nil('null');

const JsonType(this.typeName);
Expand Down Expand Up @@ -238,6 +263,9 @@ enum ValidationErrorType {
maxLengthExceeded,
patternMismatch,

// Enum specific
enumValueNotAllowed,

// Number/Integer specific
minimumNotMet,
maximumExceeded,
Expand Down Expand Up @@ -334,6 +362,9 @@ extension type Schema.fromMap(Map<String, Object?> _value) {
/// Alias for [ObjectSchema.new].
static const object = ObjectSchema.new;

/// Alias for [EnumSchema.new].
static const enumeration = EnumSchema.new;

/// Alias for [NullSchema.new].
static const nil = NullSchema.new;

Expand Down Expand Up @@ -424,6 +455,12 @@ extension SchemaValidation on Schema {
currentPath,
accumulatedFailures,
);
case JsonType.enumeration:
isValid = (this as EnumSchema)._validateEnum(
data,
currentPath,
accumulatedFailures,
);
case JsonType.bool:
if (data is! bool) {
isValid = false;
Expand Down Expand Up @@ -1081,6 +1118,66 @@ extension type const StringSchema.fromMap(Map<String, Object?> _value)
}
}

/// A JSON Schema definition for a set of allowed string values.
extension type EnumSchema.fromMap(Map<String, Object?> _value)
implements Schema {
factory EnumSchema({
String? title,
String? description,
required Iterable<String> values,
}) => EnumSchema.fromMap({
'type': JsonType.enumeration.typeName,
if (title != null) 'title': title,
if (description != null) 'description': description,
'enum': values,
});

/// A title for this schema, should be short.
String? get title => _value['title'] as String?;

/// A description of this schema.
String? get description => _value['description'] as String?;

/// The allowed enum values.
Iterable<String> get values {
final values = (_value['enum'] as Iterable?)?.cast<String>();
if (values == null) {
throw ArgumentError('Missing required property "values"');
}
assert(
values.toSet().length == values.length,
"The 'values' property has duplicate entries.",
);
return values;
}

bool _validateEnum(
Object? data,
List<String> currentPath,
HashSet<ValidationError> accumulatedFailures,
) {
if (data is! String) {
accumulatedFailures.add(
ValidationError(ValidationErrorType.typeMismatch, path: currentPath),
);
return false;
}
if (!values.contains(data)) {
accumulatedFailures.add(
ValidationError(
ValidationErrorType.enumValueNotAllowed,
path: currentPath,
details:
'String "$data" is not one of the allowed values: '
'${values.join(', ')}',
),
);
return false;
}
return true;
}
}

/// A JSON Schema definition for a [num].
extension type NumberSchema.fromMap(Map<String, Object?> _value)
implements Schema {
Expand Down
35 changes: 35 additions & 0 deletions pkgs/dart_mcp/test/api/tools_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,20 @@ void main() {
});
});

test('EnumSchema', () {
final schema = EnumSchema(
title: 'Foo',
description: 'Bar',
values: {'a', 'b', 'c'},
);
expect(schema, {
'type': 'enum',
'title': 'Foo',
'description': 'Bar',
'enum': ['a', 'b', 'c'],
});
});

test('Schema', () {
final schema = Schema.combined(
type: JsonType.bool,
Expand Down Expand Up @@ -830,6 +844,27 @@ void main() {
});
});

group('Enum Specific', () {
test('enumValueNotAllowed', () {
final schema = EnumSchema(values: {'a', 'b'});
expectFailuresMatch(schema, 'c', [
ValidationError(ValidationErrorType.enumValueNotAllowed),
]);
});

test('valid enum value', () {
final schema = EnumSchema(values: {'a', 'b'});
expectFailuresMatch(schema, 'a', []);
});

test('enum with non-string data', () {
final schema = EnumSchema(values: {'a', 'b'});
expectFailuresMatch(schema, 1, [
ValidationError(ValidationErrorType.typeMismatch),
]);
});
});

group('Schema Combinators', () {
test('allOfNotMet - one sub-schema fails', () {
final schema = Schema.combined(
Expand Down