Skip to content

Commit 87d0746

Browse files
Classify dispatched messages with a shared JsonRpc2Object
1 parent ad2ae3b commit 87d0746

5 files changed

Lines changed: 131 additions & 88 deletions

File tree

pkgs/dart_mcp/lib/src/server/request_scoped.dart

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -71,26 +71,27 @@ Future<Map<String, Object?>?> handleRequestScopedMessage(
7171
MCPServerFactory serverFactory, {
7272
void Function(Map<String, Object?> notification)? onNotification,
7373
}) async {
74-
final method = message[Keys.method];
75-
if (method is! String) {
74+
final object = JsonRpc2Object.fromMap(message);
75+
if (object.kind == JsonRpc2Kind.response) {
7676
throw ArgumentError.value(
7777
message,
7878
'message',
79-
'A dispatched message must have a string method',
79+
'A request or notification must not carry a result or error',
8080
);
8181
}
82-
if (message.containsKey(Keys.id) && message[Keys.id] == null) {
82+
final method = object.method;
83+
if (method is! String) {
8384
throw ArgumentError.value(
8485
message,
8586
'message',
86-
'A request id must not be null',
87+
'A dispatched message must have a string method',
8788
);
8889
}
89-
if (message.containsKey(Keys.result) || message.containsKey(Keys.error)) {
90+
if (object.kind == JsonRpc2Kind.request && object.id == null) {
9091
throw ArgumentError.value(
9192
message,
9293
'message',
93-
'A request or notification must not carry a result or error',
94+
'A request id must not be null',
9495
);
9596
}
9697
if (method == InitializeRequest.methodName ||
@@ -112,14 +113,14 @@ Future<Map<String, Object?>?> handleRequestScopedMessage(
112113
StreamChannel.withCloseGuarantee(inbound.stream, outbound.sink),
113114
);
114115

115-
final isRequest = message.containsKey(Keys.id);
116+
final isRequest = object.kind == JsonRpc2Kind.request;
116117
final response = Completer<Map<String, Object?>?>();
117118
final subscription = outbound.stream.listen(
118119
(encoded) {
119120
try {
120121
final decoded = (jsonDecode(encoded) as Map).cast<String, Object?>();
121-
if (decoded.containsKey(Keys.method)) {
122-
if (decoded.containsKey(Keys.id)) {
122+
switch (JsonRpc2Object.fromMap(decoded).kind) {
123+
case JsonRpc2Kind.request:
123124
// A request from the server to the client. Nothing can answer it
124125
// in a single-message exchange, so fail it back to the server
125126
// instead of leaving its handler waiting forever. Late requests
@@ -129,24 +130,27 @@ Future<Map<String, Object?>?> handleRequestScopedMessage(
129130
inbound.add(
130131
jsonEncode(
131132
_errorResponse(
132-
decoded[Keys.id],
133+
JsonRpc2Request.fromMap(decoded).id,
133134
'Server to client requests are not supported on a '
134135
'request-scoped transport',
135136
),
136137
),
137138
);
138139
}
139-
} else {
140+
case JsonRpc2Kind.notification:
140141
try {
141142
onNotification?.call(decoded);
142143
} catch (error, stackTrace) {
143144
// A misbehaving callback must not fail the request being
144145
// handled, but it should still be visible.
145146
Zone.current.handleUncaughtError(error, stackTrace);
146147
}
147-
}
148-
} else if (!response.isCompleted) {
149-
response.complete(_withServerInfo(decoded, server.implementation));
148+
case JsonRpc2Kind.response:
149+
if (!response.isCompleted) {
150+
response.complete(
151+
_withServerInfo(decoded, server.implementation),
152+
);
153+
}
150154
}
151155
} catch (error, stackTrace) {
152156
// The server sent a frame we could not process. Never let it wedge or
@@ -205,11 +209,11 @@ Map<String, Object?> _withServerInfo(
205209
Map<String, Object?> response,
206210
Implementation implementation,
207211
) {
208-
final result = response[Keys.result];
212+
final result = JsonRpc2Response.fromMap(response).result;
209213
if (result is! Map) return response;
210-
final existingMeta = result[Keys.meta];
211-
if (existingMeta is! Map?) return response;
212214
final resultMap = result.cast<String, Object?>();
215+
final existingMeta = resultMap[Keys.meta];
216+
if (existingMeta is! Map?) return response;
213217
final meta = existingMeta?.cast<String, Object?>() ?? <String, Object?>{};
214218
// Copy so the returned response does not alias the shared server info map.
215219
meta.putIfAbsent(

pkgs/dart_mcp/lib/src/server/server.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'package:stream_transform/stream_transform.dart';
1414
import '../api/api.dart';
1515
import '../shared.dart';
1616
import '../utils/constants.dart';
17+
import '../utils/json_rpc_2_object.dart';
1718

1819
part 'completions_support.dart';
1920
part 'elicitation_request_support.dart';

pkgs/dart_mcp/lib/src/utils/constants.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ extension Keys on Never {
8484
static const oneOf = 'oneOf';
8585
static const openWorldHint = 'openWorldHint';
8686
static const outputSchema = 'outputSchema';
87+
static const params = 'params';
8788
static const path = 'path';
8889
static const pattern = 'pattern';
8990
static const patternProperties = 'patternProperties';
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'constants.dart';
6+
7+
/// The kind of a decoded JSON-RPC 2.0 message.
8+
enum JsonRpc2Kind { request, notification, response }
9+
10+
/// A decoded JSON-RPC 2.0 message.
11+
extension type JsonRpc2Object.fromMap(Map<String, Object?> _value) {
12+
/// The kind of this message.
13+
///
14+
/// A message with a `result` or `error` member is a response, any other
15+
/// message with an `id` member is a request, and the rest are
16+
/// notifications.
17+
JsonRpc2Kind get kind =>
18+
_value.containsKey(Keys.result) || _value.containsKey(Keys.error)
19+
? JsonRpc2Kind.response
20+
: _value.containsKey(Keys.id)
21+
? JsonRpc2Kind.request
22+
: JsonRpc2Kind.notification;
23+
24+
/// The method of this message, if it is a request or notification.
25+
Object? get method => _value[Keys.method];
26+
27+
/// The id of this message, if it is a request or response.
28+
Object? get id => _value[Keys.id];
29+
}
30+
31+
/// A decoded JSON-RPC 2.0 request.
32+
extension type JsonRpc2Request.fromMap(Map<String, Object?> _value)
33+
implements JsonRpc2Object {}
34+
35+
/// A decoded JSON-RPC 2.0 response.
36+
extension type JsonRpc2Response.fromMap(Map<String, Object?> _value)
37+
implements JsonRpc2Object {
38+
/// The result of this response, if it is a success response.
39+
Object? get result => _value[Keys.result];
40+
}

0 commit comments

Comments
 (0)