Skip to content
Draft
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pubspec.lock
.vscode
.idea
*.iml
.agents

# Gemini specific ignores
**/.gemini/*
Expand Down
5 changes: 5 additions & 0 deletions pkgs/dart_mcp/lib/api.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

export 'src/api/api.dart';
2 changes: 1 addition & 1 deletion pkgs/dart_mcp/lib/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

export 'src/api/api.dart';
export 'api.dart';
export 'src/client/client.dart';
2 changes: 1 addition & 1 deletion pkgs/dart_mcp/lib/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

export 'src/api/api.dart';
export 'api.dart';
export 'src/server/server.dart';
29 changes: 17 additions & 12 deletions pkgs/dart_mcp/lib/src/api/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ enum ProtocolVersion {

/// A progress token, used to associate progress notifications with the original
/// request.
extension type ProgressToken(/*String|int*/ Object _) {}
extension type ProgressToken(/*String|int*/ Object _) implements Object {}

/// An opaque token used to represent a cursor for pagination.
extension type Cursor(String _) {}
extension type Cursor(String _) implements Object {}

/// Generic metadata passed with most requests.
///
Expand Down Expand Up @@ -92,15 +92,16 @@ extension type Cursor(String _) {}
/// - Name: Unless empty, MUST begin and end with an alphanumeric character
/// (`[a-z0-9A-Z]`). MAY contain hyphens (`-`), underscores (`_`), dots
/// (`.`), and alphanumerics in between.
extension type Meta.fromMap(Map<String, Object?> _value) {
extension type Meta.fromMap(Map<String, Object?> _value) implements Object {
Object? operator [](String key) => _value[key];
}

/// Basic metadata required by multiple types.
///
/// Not to be confused with the `_meta` property in the spec, which has a
/// different purpose.
extension type BaseMetadata.fromMap(Map<String, Object?> _value) {
extension type BaseMetadata.fromMap(Map<String, Object?> _value)
implements Object {
factory BaseMetadata({required String name, String? title}) =>
BaseMetadata.fromMap({Keys.name: name, Keys.title: title});

Expand Down Expand Up @@ -130,7 +131,8 @@ extension type BaseMetadata.fromMap(Map<String, Object?> _value) {
/// [ProgressToken] at the key "progressToken".
///
/// Should be "mixed in" by implementing this type from other extension types.
extension type WithProgressToken.fromMap(Map<String, Object?> _value) {
extension type WithProgressToken.fromMap(Map<String, Object?> _value)
implements Object {
ProgressToken? get progressToken =>
_value[Keys.progressToken] as ProgressToken?;
}
Expand All @@ -147,7 +149,8 @@ extension type MetaWithProgressToken.fromMap(Map<String, Object?> _value)
/// Base interface for all types that can have arbitrary metadata attached.
///
/// Should not be constructed directly, and has no public constructor.
extension type WithMetadata._fromMap(Map<String, Object?> _value) {
extension type WithMetadata._fromMap(Map<String, Object?> _value)
implements Object {
/// The `_meta` property/parameter is reserved by MCP to allow clients and
/// servers to attach additional metadata to their interactions.
///
Expand All @@ -171,14 +174,14 @@ extension type Request._fromMap(Map<String, Object?> _value)
}

/// Base interface for all notifications.
extension type Notification(Map<String, Object?> _value) {
extension type Notification(Map<String, Object?> _value) implements Object {
/// This parameter name is reserved by MCP to allow clients and servers to
/// attach additional metadata to their notifications.
Meta? get meta => _value[Keys.meta] as Meta?;
}

/// Base interface for all responses to requests.
extension type Result._(Map<String, Object?> _value) {
extension type Result._(Map<String, Object?> _value) implements Object {
Meta? get meta => _value[Keys.meta] as Meta?;
}

Expand Down Expand Up @@ -226,7 +229,7 @@ extension type CancelledNotification.fromMap(Map<String, Object?> _value)
}

/// An opaque request ID.
extension type RequestId(/*String|int*/ Parameter _) {}
extension type RequestId(/*String|int*/ Parameter _) implements Object {}

/// A ping, issued by either the server or the client, to check that the other
/// party is still alive.
Expand Down Expand Up @@ -315,7 +318,7 @@ extension type PaginatedResult._fromMap(Map<String, Object?> _value)
///
/// Doing `is` checks does not work because these are just extension types, they
/// all have the same runtime type (`Map<String, Object?>`).
extension type Content._(Map<String, Object?> _value) {
extension type Content._(Map<String, Object?> _value) implements Object {
factory Content.fromMap(Map<String, Object?> value) {
assert(value.containsKey(Keys.type));
return Content._(value);
Expand Down Expand Up @@ -538,13 +541,15 @@ extension type ResourceLink.fromMap(Map<String, Object?> _value)
/// Base type for objects that include optional annotations for the client.
///
/// The client can use annotations to inform how objects are used or displayed.
extension type Annotated._fromMap(Map<String, Object?> _value) {
extension type Annotated._fromMap(Map<String, Object?> _value)
implements Object {
/// Annotations for this object.
Annotations? get annotations => _value[Keys.annotations] as Annotations?;
}

/// The annotations for an [Annotated] object.
extension type Annotations.fromMap(Map<String, Object?> _value) {
extension type Annotations.fromMap(Map<String, Object?> _value)
implements Object {
factory Annotations({
List<Role>? audience,
DateTime? lastModified,
Expand Down
3 changes: 2 additions & 1 deletion pkgs/dart_mcp/lib/src/api/sampling.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ extension type CreateMessageResult.fromMap(Map<String, Object?> _value)
}

/// Describes a message issued to or received from an LLM API.
extension type SamplingMessage.fromMap(Map<String, Object?> _value) {
extension type SamplingMessage.fromMap(Map<String, Object?> _value)
implements Object {
factory SamplingMessage({required Role role, required Content content}) =>
SamplingMessage.fromMap({Keys.role: role.name, Keys.content: content});

Expand Down
1 change: 1 addition & 0 deletions pkgs/dart_mcp/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ dependencies:
stream_transform: ^2.1.1

dev_dependencies:
checks: ^0.3.1
dart_flutter_team_lints: ^3.2.1
test: ^1.25.15
115 changes: 48 additions & 67 deletions pkgs/dart_mcp/test/api/api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,126 +2,107 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:checks/checks.dart';
import 'package:dart_mcp/client.dart';
import 'package:test/test.dart';

void main() {
test('protocol versions can be compared', () {
expect(
check(
ProtocolVersion.latestSupported > ProtocolVersion.oldestSupported,
true,
);
expect(
).isTrue();
check(
ProtocolVersion.latestSupported >= ProtocolVersion.oldestSupported,
true,
);
expect(
).isTrue();
check(
ProtocolVersion.latestSupported < ProtocolVersion.oldestSupported,
false,
);
expect(
).isFalse();
check(
ProtocolVersion.latestSupported <= ProtocolVersion.oldestSupported,
false,
);
).isFalse();

expect(
check(
ProtocolVersion.oldestSupported > ProtocolVersion.latestSupported,
false,
);
expect(
).isFalse();
check(
ProtocolVersion.oldestSupported >= ProtocolVersion.latestSupported,
false,
);
expect(
).isFalse();
check(
ProtocolVersion.oldestSupported < ProtocolVersion.latestSupported,
true,
);
expect(
).isTrue();
check(
ProtocolVersion.oldestSupported <= ProtocolVersion.latestSupported,
true,
);
).isTrue();

expect(
check(
ProtocolVersion.latestSupported <= ProtocolVersion.latestSupported,
true,
);
expect(
).isTrue();
check(
ProtocolVersion.latestSupported >= ProtocolVersion.latestSupported,
true,
);
expect(
).isTrue();
check(
ProtocolVersion.latestSupported < ProtocolVersion.latestSupported,
false,
);
expect(
).isFalse();
check(
ProtocolVersion.latestSupported > ProtocolVersion.latestSupported,
false,
);
).isFalse();
});

group('API object validation', () {
test('throws when required fields are missing', () {
expect(() => Root.fromMap({}).uri, throwsA(isA<ArgumentError>()));
expect(
check(() => Root.fromMap({}).uri).throws<ArgumentError>();
check(
() => Implementation.fromMap({'name': 'test'}).version,
throwsA(isA<ArgumentError>()),
);
expect(
() => BaseMetadata.fromMap({}).name,
throwsA(isA<ArgumentError>()),
);
).throws<ArgumentError>();
check(() => BaseMetadata.fromMap({}).name).throws<ArgumentError>();

final empty = <String, Object?>{};

// Initialization
expect(
check(
() => (empty as InitializeRequest).capabilities,
throwsArgumentError,
);
expect(
).throws<ArgumentError>();
check(
() => (empty as InitializeRequest).clientInfo,
throwsArgumentError,
);
).throws<ArgumentError>();

// Tools
expect(() => (empty as CallToolRequest).name, throwsArgumentError);
check(() => (empty as CallToolRequest).name).throws<ArgumentError>();

// Resources
expect(() => (empty as ReadResourceRequest).uri, throwsArgumentError);
expect(() => (empty as SubscribeRequest).uri, throwsArgumentError);
expect(() => (empty as UnsubscribeRequest).uri, throwsArgumentError);
check(() => (empty as ReadResourceRequest).uri).throws<ArgumentError>();
check(() => (empty as SubscribeRequest).uri).throws<ArgumentError>();
check(() => (empty as UnsubscribeRequest).uri).throws<ArgumentError>();

// Roots
expect(() => (empty as ListRootsResult).roots, throwsArgumentError);
check(() => (empty as ListRootsResult).roots).throws<ArgumentError>();

// Prompts
expect(() => (empty as GetPromptRequest).name, throwsArgumentError);
check(() => (empty as GetPromptRequest).name).throws<ArgumentError>();

// Completions
expect(() => (empty as CompleteRequest).ref, throwsArgumentError);
expect(() => (empty as CompleteRequest).argument, throwsArgumentError);
check(() => (empty as CompleteRequest).ref).throws<ArgumentError>();
check(() => (empty as CompleteRequest).argument).throws<ArgumentError>();

// Logging
expect(() => (empty as SetLevelRequest).level, throwsArgumentError);
check(() => (empty as SetLevelRequest).level).throws<ArgumentError>();

// Sampling
expect(
check(
() => (empty as CreateMessageRequest).messages,
throwsArgumentError,
);
expect(
).throws<ArgumentError>();
check(
() => (empty as CreateMessageRequest).maxTokens,
throwsArgumentError,
);
).throws<ArgumentError>();
});
test('meta field is parsed correctly', () {
final root = Root.fromMap({
'uri': 'file:///foo/bar',
'_meta': {'foo': 'bar'},
});
expect(root.meta, isNotNull);
check(root.meta).isNotNull();
final metaMap = root.meta as Map;
expect(metaMap['foo'], 'bar');
check(metaMap['foo']).equals('bar');
});
});
}
Loading
Loading