Skip to content

Commit 8e3365f

Browse files
authored
Add error checks for accessing Request members (#184)
1 parent 706d224 commit 8e3365f

File tree

12 files changed

+159
-23
lines changed

12 files changed

+159
-23
lines changed

pkgs/dart_mcp/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.2.3-wip
2+
3+
- Added error checking to required fields of all `Request` subclasses so that
4+
they will throw helpful errors when accessed and not set.
5+
16
## 0.2.2
27

38
- Refactor `ClientImplementation` and `ServerImplementation` to the shared

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,22 @@ extension type CompleteRequest.fromMap(Map<String, Object?> _value)
2727
///
2828
/// In the case of a [ResourceReference], it must refer to a
2929
/// [ResourceTemplate].
30-
Reference get ref => _value['ref'] as Reference;
30+
Reference get ref {
31+
final ref = _value['ref'] as Reference?;
32+
if (ref == null) {
33+
throw ArgumentError('Missing ref field in $CompleteRequest.');
34+
}
35+
return ref;
36+
}
3137

3238
/// The argument's information.
33-
CompletionArgument get argument =>
34-
(_value['argument'] as Map).cast<String, Object?>() as CompletionArgument;
39+
CompletionArgument get argument {
40+
final argument = _value['argument'] as CompletionArgument?;
41+
if (argument == null) {
42+
throw ArgumentError('Missing argument field in $CompleteRequest.');
43+
}
44+
return argument;
45+
}
3546
}
3647

3748
/// The server's response to a completion/complete request

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,21 @@ extension type InitializeRequest._fromMap(Map<String, Object?> _value)
3030
ProtocolVersion? get protocolVersion =>
3131
ProtocolVersion.tryParse(_value['protocolVersion'] as String);
3232

33-
ClientCapabilities get capabilities =>
34-
_value['capabilities'] as ClientCapabilities;
33+
ClientCapabilities get capabilities {
34+
final capabilities = _value['capabilities'] as ClientCapabilities?;
35+
if (capabilities == null) {
36+
throw ArgumentError('Missing capabilities field in $InitializeRequest.');
37+
}
38+
return capabilities;
39+
}
3540

36-
Implementation get clientInfo => _value['clientInfo'] as Implementation;
41+
Implementation get clientInfo {
42+
final clientInfo = _value['clientInfo'] as Implementation?;
43+
if (clientInfo == null) {
44+
throw ArgumentError('Missing clientInfo field in $InitializeRequest.');
45+
}
46+
return clientInfo;
47+
}
3748
}
3849

3950
/// After receiving an initialize request from the client, the server sends

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,18 @@ extension type SetLevelRequest.fromMap(Map<String, Object?> _value)
2626
///
2727
/// The server should send all logs at this level and higher (i.e., more
2828
/// severe) to the client as notifications/message.
29-
LoggingLevel get level =>
30-
LoggingLevel.values.firstWhere((level) => level.name == _value['level']);
29+
LoggingLevel get level {
30+
final levelName = _value['level'];
31+
final foundLevel = LoggingLevel.values.firstWhereOrNull(
32+
(level) => level.name == levelName,
33+
);
34+
if (foundLevel == null) {
35+
throw ArgumentError(
36+
"Invalid level field in $SetLevelRequest: didn't find level $levelName",
37+
);
38+
}
39+
return foundLevel;
40+
}
3141
}
3242

3343
/// Notification of a log message passed from server to client.

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,13 @@ extension type GetPromptRequest.fromMap(Map<String, Object?> _value)
4949
});
5050

5151
/// The name of the prompt or prompt template.
52-
String get name => _value['name'] as String;
52+
String get name {
53+
final name = _value['name'] as String?;
54+
if (name == null) {
55+
throw ArgumentError('Missing name field in $GetPromptRequest.');
56+
}
57+
return name;
58+
}
5359

5460
/// Arguments to use for templating the prompt.
5561
Map<String, Object?>? get arguments =>

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,13 @@ extension type ReadResourceRequest.fromMap(Map<String, Object?> _value)
8080

8181
/// The URI of the resource to read. The URI can use any protocol; it is
8282
/// up to the server how to interpret it.
83-
String get uri => _value['uri'] as String;
83+
String get uri {
84+
final uri = _value['uri'] as String?;
85+
if (uri == null) {
86+
throw ArgumentError('Missing uri field in $ReadResourceRequest.');
87+
}
88+
return uri;
89+
}
8490
}
8591

8692
/// The server's response to a resources/read request from the client.
@@ -128,7 +134,13 @@ extension type SubscribeRequest.fromMap(Map<String, Object?> _value)
128134

129135
/// The URI of the resource to subscribe to. The URI can use any protocol;
130136
/// it is up to the server how to interpret it.
131-
String get uri => _value['uri'] as String;
137+
String get uri {
138+
final uri = _value['uri'] as String?;
139+
if (uri == null) {
140+
throw ArgumentError('Missing uri field in $SubscribeRequest.');
141+
}
142+
return uri;
143+
}
132144
}
133145

134146
/// Sent from the client to request cancellation of resources/updated
@@ -146,7 +158,13 @@ extension type UnsubscribeRequest.fromMap(Map<String, Object?> _value)
146158
UnsubscribeRequest.fromMap({'uri': uri, if (meta != null) '_meta': meta});
147159

148160
/// The URI of the resource to unsubscribe from.
149-
String get uri => _value['uri'] as String;
161+
String get uri {
162+
final uri = _value['uri'] as String?;
163+
if (uri == null) {
164+
throw ArgumentError('Missing uri field in $UnsubscribeRequest.');
165+
}
166+
return uri;
167+
}
150168
}
151169

152170
/// A notification from the server to the client, informing it that a resource

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ extension type ListRootsResult.fromMap(Map<String, Object?> _value)
3333
if (meta != null) '_meta': meta,
3434
});
3535

36-
List<Root> get roots => (_value['roots'] as List).cast<Root>();
36+
List<Root> get roots {
37+
final roots = _value['roots'] as List?;
38+
if (roots == null) {
39+
throw ArgumentError('Missing roots field in $ListRootsResult.');
40+
}
41+
return roots.cast<Root>();
42+
}
3743
}
3844

3945
/// Represents a root directory or file that the server can operate on.

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,13 @@ extension type CreateMessageRequest.fromMap(Map<String, Object?> _value)
3939
});
4040

4141
/// The messages to send to the LLM.
42-
List<SamplingMessage> get messages =>
43-
(_value['messages'] as List).cast<SamplingMessage>();
42+
List<SamplingMessage> get messages {
43+
final messages = _value['messages'] as List?;
44+
if (messages == null) {
45+
throw ArgumentError('Missing messages field in $CreateMessageRequest.');
46+
}
47+
return messages.cast<SamplingMessage>();
48+
}
4449

4550
/// The server's preferences for which model to select.
4651
///
@@ -69,7 +74,13 @@ extension type CreateMessageRequest.fromMap(Map<String, Object?> _value)
6974
/// The maximum number of tokens to sample, as requested by the server.
7075
///
7176
/// The client MAY choose to sample fewer tokens than requested.
72-
int get maxTokens => _value['maxTokens'] as int;
77+
int get maxTokens {
78+
final maxTokens = _value['maxTokens'] as int?;
79+
if (maxTokens == null) {
80+
throw ArgumentError('Missing maxTokens field in $CreateMessageRequest.');
81+
}
82+
return maxTokens;
83+
}
7384

7485
/// Note: This has no documentation in the specification or schema.
7586
List<String>? get stopSequences =>

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,13 @@ extension type CallToolRequest._fromMap(Map<String, Object?> _value)
8080
});
8181

8282
/// The name of the method to invoke.
83-
String get name => _value['name'] as String;
83+
String get name {
84+
final name = _value['name'] as String?;
85+
if (name == null) {
86+
throw ArgumentError('Missing name field in $CallToolRequest');
87+
}
88+
return name;
89+
}
8490

8591
/// The arguments to pass to the method.
8692
Map<String, Object?>? get arguments =>

pkgs/dart_mcp/lib/src/shared.dart

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,15 @@ base class MCPBase {
7979
void registerRequestHandler<T extends Request?, R extends Result?>(
8080
String name,
8181
FutureOr<R> Function(T) impl,
82-
) => _peer.registerMethod(
83-
name,
84-
(Parameters p) => impl((p.value as Map?)?.cast<String, Object?>() as T),
85-
);
82+
) => _peer.registerMethod(name, (Parameters p) {
83+
if (p.value != null && p.value is! Map) {
84+
throw ArgumentError(
85+
'Request to $name must be a Map or null. Instead, got '
86+
'${p.value.runtimeType}',
87+
);
88+
}
89+
return impl((p.value as Map?)?.cast<String, Object?>() as T);
90+
});
8691

8792
/// Registers a notification handler named [name] on this server.
8893
void registerNotificationHandler<T extends Notification?>(

0 commit comments

Comments
 (0)