Skip to content

Commit bc0bd0a

Browse files
authored
Update spec to 2025-03-26 (#70)
I had already implemented one portion of this by accident apparently (completions support). Adds version negotiation and a few other small breaking changes since I wanted to make a breaking change anyways to convert the protocol version to an enum.
1 parent 4ab0796 commit bc0bd0a

20 files changed

+442
-60
lines changed

pkgs/dart_mcp/CHANGELOG.md

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
1-
## 0.1.1-wip
1+
## 0.2.0-wip
22

3+
- Support protocol version 2025-03-26.
4+
- Adds support for `AudioContent`.
5+
- Adds support for `ToolAnnotations`.
6+
- Adds support for `ProgressNotification` messages.
37
- Save the `ServerCapabilities` object on the `ServerConnection` class to make
48
it easier to check the capabilities of the server.
9+
- Add default version negotiation logic.
10+
- Save the negotiated `ProtocolVersion` in a `protocolVersion` field for both
11+
`MCPServer` and the `ServerConnection` classes.
12+
- Automatically disconnect from servers if version negotiation fails.
513
- Added support for adding and listing `ResourceTemplate`s.
14+
- Handlers have to handle their own matching of templates.
15+
- **Breaking**: Fixed paginated result subtypes to use `nextCursor` instead of
16+
`cursor` as the key for the next cursor.
17+
- **Breaking**: Change the `ProgressNotification.progress` and
18+
`ProgressNotification.total` types to `num` instead of `int` to align with the
19+
spec.
20+
- **Breaking**: Change the `protocolVersion` string to a `ProtocolVersion` enum,
21+
which has all supported versions and whether or not they are supported.
22+
- **Breaking**: Change `InitializeRequest` and `InitializeResult` to take a
23+
`ProtocolVersion` instead of a string.
624

725
## 0.1.0
826

pkgs/dart_mcp/README.md

+23
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,29 @@ Both the `MCPServer` and `MCPClient` support these.
9494
| [Cancellation](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/utilities/cancellation/) | :x: | https://github.com/dart-lang/ai/issues/37 |
9595
| [Progress](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/utilities/progress/) | :heavy_check_mark: | |
9696

97+
## Transport Mechanisms
98+
99+
This table describes the supported
100+
[transport mechanisms](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports).
101+
102+
At its core this package is just built on streams, so any transport mechanism
103+
can be used, but some are directly supported out of the box.
104+
105+
| Transport | Support | Notes |
106+
| --- | --- | --- |
107+
| [Stdio](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#stdio) | :heavy_check_mark: | |
108+
| [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) | :x: | Unsupported at this time, may come in the future. |
109+
110+
## Batching Requests
111+
112+
Both the client and server support processing batch requests, but do not support
113+
creating batch requests at this time.
114+
115+
## Authorization
116+
117+
Authorization is not supported at this time. This package is primarily targeted
118+
at local MCP server usage for now.
119+
97120
## Server Capabilities
98121

99122
This table describes the state of implementation for the

pkgs/dart_mcp/example/client.dart

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ void main() async {
1818
print('initializing server');
1919
final initializeResult = await server.initialize(
2020
InitializeRequest(
21-
protocolVersion: protocolVersion,
21+
protocolVersion: ProtocolVersion.latestSupported,
2222
capabilities: client.capabilities,
2323
clientInfo: client.implementation,
2424
),
2525
);
2626
print('initialized: $initializeResult');
27-
if (initializeResult.protocolVersion != protocolVersion) {
27+
if (initializeResult.protocolVersion != ProtocolVersion.latestSupported) {
2828
throw StateError(
29-
'Protocol version mismatch, expected $protocolVersion, '
29+
'Protocol version mismatch, expected ${ProtocolVersion.latestSupported}, '
3030
'got ${initializeResult.protocolVersion}',
3131
);
3232
}

pkgs/dart_mcp/example/dash_client.dart

+6-5
Original file line numberDiff line numberDiff line change
@@ -224,15 +224,16 @@ final class DashClient extends MCPClient with RootsSupport {
224224
for (var connection in serverConnections) {
225225
final result = await connection.initialize(
226226
InitializeRequest(
227-
protocolVersion: protocolVersion,
227+
protocolVersion: ProtocolVersion.latestSupported,
228228
capabilities: capabilities,
229229
clientInfo: implementation,
230230
),
231231
);
232-
if (result.protocolVersion != protocolVersion) {
232+
if (result.protocolVersion != ProtocolVersion.latestSupported) {
233233
print(
234-
'Protocol version mismatch, expected $protocolVersion, '
235-
'got ${result.protocolVersion}, disconnecting from server',
234+
'Protocol version mismatch, expected '
235+
'${ProtocolVersion.latestSupported}, got ${result.protocolVersion}, '
236+
'disconnecting from server',
236237
);
237238
await connection.shutdown();
238239
serverConnections.remove(connection);
@@ -385,7 +386,7 @@ final class DashChatBotServer extends MCPServer with ToolsSupport {
385386

386387
final systemInstructions = gemini.Content.system('''
387388
You are a developer assistant for Dart and Flutter apps. Your persona is a cute
388-
blue humingbird named Dash, and you are also the mascot for the Dart and Flutter
389+
blue hummingbird named Dash, and you are also the mascot for the Dart and Flutter
389390
brands. Your personality is extremely cheery and bright, and your tone is always
390391
positive.
391392

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

+99-15
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,37 @@ part 'roots.dart';
1717
part 'sampling.dart';
1818
part 'tools.dart';
1919

20-
/// The current protocol version
21-
const protocolVersion = '2024-11-05';
20+
/// Enum of the known protocol versions.
21+
enum ProtocolVersion {
22+
v2024_11_05('2024-11-05'),
23+
v2025_03_26('2025-03-26');
24+
25+
const ProtocolVersion(this.versionString);
26+
27+
/// Returns the [ProtocolVersion] based on the [version] string, or `null` if
28+
/// it was not recognized.
29+
static ProtocolVersion? tryParse(String version) =>
30+
values.firstWhereOrNull((v) => v.versionString == version);
31+
32+
/// The oldest version supported by the current API.
33+
static const oldestSupported = ProtocolVersion.v2024_11_05;
34+
35+
/// The most recent version supported by the current API.
36+
static const latestSupported = ProtocolVersion.v2025_03_26;
37+
38+
/// The version string used over the wire to identify this version.
39+
final String versionString;
40+
41+
/// Whether or not this API is compatible with the current version.
42+
///
43+
/// **Note**: There may be extra fields included.
44+
bool get isSupported => this >= oldestSupported && this <= latestSupported;
45+
46+
bool operator <(ProtocolVersion other) => index < other.index;
47+
bool operator <=(ProtocolVersion other) => index <= other.index;
48+
bool operator >(ProtocolVersion other) => index > other.index;
49+
bool operator >=(ProtocolVersion other) => index >= other.index;
50+
}
2251

2352
/// A progress token, used to associate progress notifications with the original
2453
/// request.
@@ -136,14 +165,16 @@ extension type ProgressNotification.fromMap(Map<String, Object?> _value)
136165

137166
factory ProgressNotification({
138167
required ProgressToken progressToken,
139-
required int progress,
140-
int? total,
168+
required num progress,
169+
num? total,
141170
Meta? meta,
171+
String? message,
142172
}) => ProgressNotification.fromMap({
143173
'progressToken': progressToken,
144174
'progress': progress,
145175
if (total != null) 'total': total,
146176
if (meta != null) '_meta': meta,
177+
if (message != null) 'message': message,
147178
});
148179

149180
/// The progress token which was given in the initial request, used to
@@ -154,11 +185,14 @@ extension type ProgressNotification.fromMap(Map<String, Object?> _value)
154185
///
155186
/// This should increase every time progress is made, even if the total is
156187
/// unknown.
157-
int get progress => _value['progress'] as int;
188+
num get progress => _value['progress'] as num;
158189

159190
/// Total number of items to process (or total progress required), if
160191
/// known.
161-
int? get total => _value['total'] as int?;
192+
num? get total => _value['total'] as num?;
193+
194+
/// An optional message describing the current progress.
195+
String? get message => _value['message'] as String?;
162196
}
163197

164198
/// A "mixin"-like extension type for any request that contains a [Cursor] at
@@ -185,10 +219,11 @@ extension type PaginatedRequest._fromMap(Map<String, Object?> _value)
185219
/// constructor.
186220
extension type PaginatedResult._fromMap(Map<String, Object?> _value)
187221
implements Result {
188-
Cursor? get cursor => _value['cursor'] as Cursor?;
222+
Cursor? get nextCursor => _value['nextCursor'] as Cursor?;
189223
}
190224

191-
/// Could be either [TextContent], [ImageContent] or [EmbeddedResource].
225+
/// Could be either [TextContent], [ImageContent], [AudioContent] or
226+
/// [EmbeddedResource].
192227
///
193228
/// Use [isText], [isImage] and [isEmbeddedResource] before casting to the more
194229
/// specific types, or switch on the [type] and then cast.
@@ -201,25 +236,40 @@ extension type Content._(Map<String, Object?> _value) {
201236
return Content._(value);
202237
}
203238

239+
/// Alias for [TextContent.new].
240+
static const text = TextContent.new;
241+
242+
/// Alias for [ImageContent.new].
243+
static const image = ImageContent.new;
244+
245+
/// Alias for [AudioContent.new].
246+
static const audio = AudioContent.new;
247+
248+
/// Alias for [EmbeddedResource.new].
249+
static const embeddedResource = EmbeddedResource.new;
250+
204251
/// Whether or not this is a [TextContent].
205252
bool get isText => _value['type'] == TextContent.expectedType;
206253

207-
/// Whether or not this is a [ImageContent].
254+
/// Whether or not this is an [ImageContent].
208255
bool get isImage => _value['type'] == ImageContent.expectedType;
209256

257+
/// Whether or not this is an [AudioContent].
258+
bool get isAudio => _value['type'] == AudioContent.expectedType;
259+
210260
/// Whether or not this is an [EmbeddedResource].
211261
bool get isEmbeddedResource =>
212262
_value['type'] == EmbeddedResource.expectedType;
213263

214264
/// The type of content.
215265
///
216266
/// You can use this in a switch to handle the various types (see the static
217-
/// `expectedType` getters), or you can use [isText], [isImage], and
267+
/// `expectedType` getters), or you can use [isText], [isImage], [isAudio] and
218268
/// [isEmbeddedResource] to determine the type and then do the cast.
219269
String get type => _value['type'] as String;
220270
}
221271

222-
/// Text provided to an LLM.
272+
/// Text provided to or from an LLM.
223273
extension type TextContent.fromMap(Map<String, Object?> _value)
224274
implements Content, Annotated {
225275
static const expectedType = 'text';
@@ -241,7 +291,7 @@ extension type TextContent.fromMap(Map<String, Object?> _value)
241291
String get text => _value['text'] as String;
242292
}
243293

244-
/// An image provided to an LLM.
294+
/// An image provided to or from an LLM.
245295
extension type ImageContent.fromMap(Map<String, Object?> _value)
246296
implements Content, Annotated {
247297
static const expectedType = 'image';
@@ -263,11 +313,45 @@ extension type ImageContent.fromMap(Map<String, Object?> _value)
263313
return type;
264314
}
265315

266-
/// If the [type] is `image`, this is the base64 encoded image data.
316+
/// The base64 encoded image data.
267317
String get data => _value['data'] as String;
268318

269-
/// If the [type] is `image`, the MIME type of the image. Different providers
270-
/// may support different image types.
319+
/// The MIME type of the image.
320+
///
321+
/// Different providers may support different image types.
322+
String get mimeType => _value['mimeType'] as String;
323+
}
324+
325+
/// Audio provided to or from an LLM.
326+
///
327+
/// Only supported since version [ProtocolVersion.v2025_03_26].
328+
extension type AudioContent.fromMap(Map<String, Object?> _value)
329+
implements Content, Annotated {
330+
static const expectedType = 'audio';
331+
332+
factory AudioContent({
333+
required String data,
334+
required String mimeType,
335+
Annotations? annotations,
336+
}) => AudioContent.fromMap({
337+
'data': data,
338+
'mimeType': mimeType,
339+
'type': expectedType,
340+
if (annotations != null) 'annotations': annotations,
341+
});
342+
343+
String get type {
344+
final type = _value['type'] as String;
345+
assert(type == expectedType);
346+
return type;
347+
}
348+
349+
/// The base64 encoded audio data.
350+
String get data => _value['data'] as String;
351+
352+
/// The MIME type of the audio.
353+
///
354+
/// Different providers may support different audio types.
271355
String get mimeType => _value['mimeType'] as String;
272356
}
273357

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

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
/// Note that all features here are only supported since version
6+
/// [ProtocolVersion.v2025_03_26].
57
part of 'api.dart';
68

79
/// A request from the client to the server, to ask for completion options.

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

+23-6
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ extension type InitializeRequest._fromMap(Map<String, Object?> _value)
1111
static const methodName = 'initialize';
1212

1313
factory InitializeRequest({
14-
required String protocolVersion,
14+
required ProtocolVersion protocolVersion,
1515
required ClientCapabilities capabilities,
1616
required ClientImplementation clientInfo,
1717
MetaWithProgressToken? meta,
1818
}) => InitializeRequest._fromMap({
19-
'protocolVersion': protocolVersion,
19+
'protocolVersion': protocolVersion.versionString,
2020
'capabilities': capabilities,
2121
'clientInfo': clientInfo,
2222
if (meta != null) '_meta': meta,
@@ -25,9 +25,14 @@ extension type InitializeRequest._fromMap(Map<String, Object?> _value)
2525
/// The latest version of the Model Context Protocol that the client supports.
2626
///
2727
/// The client MAY decide to support older versions as well.
28-
String get protocolVersion => _value['protocolVersion'] as String;
28+
///
29+
/// May be `null` if the version is not recognized.
30+
ProtocolVersion? get protocolVersion =>
31+
ProtocolVersion.tryParse(_value['protocolVersion'] as String);
32+
2933
ClientCapabilities get capabilities =>
3034
_value['capabilities'] as ClientCapabilities;
35+
3136
ClientImplementation get clientInfo =>
3237
_value['clientInfo'] as ClientImplementation;
3338
}
@@ -37,12 +42,12 @@ extension type InitializeRequest._fromMap(Map<String, Object?> _value)
3742
extension type InitializeResult.fromMap(Map<String, Object?> _value)
3843
implements Result {
3944
factory InitializeResult({
40-
required String protocolVersion,
45+
required ProtocolVersion protocolVersion,
4146
required ServerCapabilities serverCapabilities,
4247
required ServerImplementation serverInfo,
4348
required String instructions,
4449
}) => InitializeResult.fromMap({
45-
'protocolVersion': protocolVersion,
50+
'protocolVersion': protocolVersion.versionString,
4651
'capabilities': serverCapabilities,
4752
'serverInfo': serverInfo,
4853
'instructions': instructions,
@@ -52,7 +57,19 @@ extension type InitializeResult.fromMap(Map<String, Object?> _value)
5257
///
5358
/// This may not match the version that the client requested. If the client
5459
/// cannot support this version, it MUST disconnect.
55-
String get protocolVersion => _value['protocolVersion'] as String;
60+
///
61+
/// May be `null` if the version is not recognized.
62+
ProtocolVersion? get protocolVersion =>
63+
ProtocolVersion.tryParse(_value['protocolVersion'] as String);
64+
65+
/// Sets the protocol version, by default this is set for you, but you can
66+
/// override it to a specific version if desired.
67+
///
68+
/// While this API is typed as nullable, `null` is not an allowed value.
69+
set protocolVersion(ProtocolVersion? value) {
70+
assert(value != null);
71+
_value['protocolVersion'] = value!.versionString;
72+
}
5673

5774
ServerCapabilities get capabilities =>
5875
_value['capabilities'] as ServerCapabilities;

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ extension type ListPromptsResult.fromMap(Map<String, Object?> _value)
2222
implements PaginatedResult {
2323
factory ListPromptsResult({
2424
required List<Prompt> prompts,
25-
Cursor? cursor,
25+
Cursor? nextCursor,
2626
Meta? meta,
2727
}) => ListPromptsResult.fromMap({
2828
'prompts': prompts,
29-
if (cursor != null) 'cursor': cursor,
29+
if (nextCursor != null) 'nextCursor': nextCursor,
3030
if (meta != null) '_meta': meta,
3131
});
3232

0 commit comments

Comments
 (0)