Skip to content

Commit d0b89cf

Browse files
authored
Add enum support to Schema (#190)
1 parent f219861 commit d0b89cf

File tree

3 files changed

+137
-4
lines changed

3 files changed

+137
-4
lines changed

pkgs/dart_mcp/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

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

67
## 0.2.2
78

pkgs/dart_mcp/lib/src/api/tools.dart

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ extension type ListToolsResult.fromMap(Map<String, Object?> _value)
2929
if (meta != null) '_meta': meta,
3030
});
3131

32-
List<Tool> get tools => (_value['tools'] as List).cast<Tool>();
32+
List<Tool> get tools {
33+
final tools = (_value['tools'] as List?)?.cast<Tool>();
34+
if (tools == null) {
35+
throw ArgumentError('Missing tools field in $ListToolsResult');
36+
}
37+
return tools;
38+
}
3339
}
3440

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

5763
/// The type of content, either [TextContent], [ImageContent],
5864
/// or [EmbeddedResource],
59-
List<Content> get content => (_value['content'] as List).cast<Content>();
65+
List<Content> get content {
66+
final content = (_value['content'] as List?)?.cast<Content>();
67+
if (content == null) {
68+
throw ArgumentError('Missing content field in $CallToolResult');
69+
}
70+
return content;
71+
}
6072

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

131143
/// The name of the tool.
132-
String get name => _value['name'] as String;
144+
String get name {
145+
final name = _value['name'] as String?;
146+
if (name == null) {
147+
throw ArgumentError('Missing name field in $Tool');
148+
}
149+
return name;
150+
}
133151

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

137155
/// A JSON [ObjectSchema] object defining the expected parameters for the
138156
/// tool.
139-
ObjectSchema get inputSchema => _value['inputSchema'] as ObjectSchema;
157+
ObjectSchema get inputSchema {
158+
final inputSchema = _value['inputSchema'] as ObjectSchema?;
159+
if (inputSchema == null) {
160+
throw ArgumentError('Missing inputSchema field in $Tool');
161+
}
162+
return inputSchema;
163+
}
140164
}
141165

142166
/// Additional properties describing a Tool to clients.
@@ -196,6 +220,7 @@ enum JsonType {
196220
num('number'),
197221
int('integer'),
198222
bool('boolean'),
223+
enumeration('enum'),
199224
nil('null');
200225

201226
const JsonType(this.typeName);
@@ -238,6 +263,9 @@ enum ValidationErrorType {
238263
maxLengthExceeded,
239264
patternMismatch,
240265

266+
// Enum specific
267+
enumValueNotAllowed,
268+
241269
// Number/Integer specific
242270
minimumNotMet,
243271
maximumExceeded,
@@ -334,6 +362,9 @@ extension type Schema.fromMap(Map<String, Object?> _value) {
334362
/// Alias for [ObjectSchema.new].
335363
static const object = ObjectSchema.new;
336364

365+
/// Alias for [EnumSchema.new].
366+
static const enumeration = EnumSchema.new;
367+
337368
/// Alias for [NullSchema.new].
338369
static const nil = NullSchema.new;
339370

@@ -424,6 +455,12 @@ extension SchemaValidation on Schema {
424455
currentPath,
425456
accumulatedFailures,
426457
);
458+
case JsonType.enumeration:
459+
isValid = (this as EnumSchema)._validateEnum(
460+
data,
461+
currentPath,
462+
accumulatedFailures,
463+
);
427464
case JsonType.bool:
428465
if (data is! bool) {
429466
isValid = false;
@@ -1081,6 +1118,66 @@ extension type const StringSchema.fromMap(Map<String, Object?> _value)
10811118
}
10821119
}
10831120

1121+
/// A JSON Schema definition for a set of allowed string values.
1122+
extension type EnumSchema.fromMap(Map<String, Object?> _value)
1123+
implements Schema {
1124+
factory EnumSchema({
1125+
String? title,
1126+
String? description,
1127+
required Iterable<String> values,
1128+
}) => EnumSchema.fromMap({
1129+
'type': JsonType.enumeration.typeName,
1130+
if (title != null) 'title': title,
1131+
if (description != null) 'description': description,
1132+
'enum': values,
1133+
});
1134+
1135+
/// A title for this schema, should be short.
1136+
String? get title => _value['title'] as String?;
1137+
1138+
/// A description of this schema.
1139+
String? get description => _value['description'] as String?;
1140+
1141+
/// The allowed enum values.
1142+
Iterable<String> get values {
1143+
final values = (_value['enum'] as Iterable?)?.cast<String>();
1144+
if (values == null) {
1145+
throw ArgumentError('Missing required property "values"');
1146+
}
1147+
assert(
1148+
values.toSet().length == values.length,
1149+
"The 'values' property has duplicate entries.",
1150+
);
1151+
return values;
1152+
}
1153+
1154+
bool _validateEnum(
1155+
Object? data,
1156+
List<String> currentPath,
1157+
HashSet<ValidationError> accumulatedFailures,
1158+
) {
1159+
if (data is! String) {
1160+
accumulatedFailures.add(
1161+
ValidationError(ValidationErrorType.typeMismatch, path: currentPath),
1162+
);
1163+
return false;
1164+
}
1165+
if (!values.contains(data)) {
1166+
accumulatedFailures.add(
1167+
ValidationError(
1168+
ValidationErrorType.enumValueNotAllowed,
1169+
path: currentPath,
1170+
details:
1171+
'String "$data" is not one of the allowed values: '
1172+
'${values.join(', ')}',
1173+
),
1174+
);
1175+
return false;
1176+
}
1177+
return true;
1178+
}
1179+
}
1180+
10841181
/// A JSON Schema definition for a [num].
10851182
extension type NumberSchema.fromMap(Map<String, Object?> _value)
10861183
implements Schema {

pkgs/dart_mcp/test/api/tools_test.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,20 @@ void main() {
199199
});
200200
});
201201

202+
test('EnumSchema', () {
203+
final schema = EnumSchema(
204+
title: 'Foo',
205+
description: 'Bar',
206+
values: {'a', 'b', 'c'},
207+
);
208+
expect(schema, {
209+
'type': 'enum',
210+
'title': 'Foo',
211+
'description': 'Bar',
212+
'enum': ['a', 'b', 'c'],
213+
});
214+
});
215+
202216
test('Schema', () {
203217
final schema = Schema.combined(
204218
type: JsonType.bool,
@@ -830,6 +844,27 @@ void main() {
830844
});
831845
});
832846

847+
group('Enum Specific', () {
848+
test('enumValueNotAllowed', () {
849+
final schema = EnumSchema(values: {'a', 'b'});
850+
expectFailuresMatch(schema, 'c', [
851+
ValidationError(ValidationErrorType.enumValueNotAllowed),
852+
]);
853+
});
854+
855+
test('valid enum value', () {
856+
final schema = EnumSchema(values: {'a', 'b'});
857+
expectFailuresMatch(schema, 'a', []);
858+
});
859+
860+
test('enum with non-string data', () {
861+
final schema = EnumSchema(values: {'a', 'b'});
862+
expectFailuresMatch(schema, 1, [
863+
ValidationError(ValidationErrorType.typeMismatch),
864+
]);
865+
});
866+
});
867+
833868
group('Schema Combinators', () {
834869
test('allOfNotMet - one sub-schema fails', () {
835870
final schema = Schema.combined(

0 commit comments

Comments
 (0)