Update to new MCP spec version (#193)
diff --git a/pkgs/dart_mcp/CHANGELOG.md b/pkgs/dart_mcp/CHANGELOG.md
index 08eb159..700381a 100644
--- a/pkgs/dart_mcp/CHANGELOG.md
+++ b/pkgs/dart_mcp/CHANGELOG.md
@@ -3,6 +3,10 @@
- Added error checking to required fields of all `Request` subclasses so that
they will throw helpful errors when accessed and not set.
- Added enum support to Schema.
+- Updates to the latest MCP spec, [2025-06-08](https://modelcontextprotocol.io/specification/2025-06-18/changelog)
+ - Adds support for Elicitations to allow the server to ask the user questions.
+ - Adds `ResourceLink` as a tool return content type.
+ - Adds support for structured tool output.
## 0.2.2
diff --git a/pkgs/dart_mcp/lib/src/api/api.dart b/pkgs/dart_mcp/lib/src/api/api.dart
index 73fb647..2ead41d 100644
--- a/pkgs/dart_mcp/lib/src/api/api.dart
+++ b/pkgs/dart_mcp/lib/src/api/api.dart
@@ -2,7 +2,8 @@
// 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.
-/// Interfaces are based on https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json
+/// Interfaces are based on
+/// https://github.com/modelcontextprotocol/specification/blob/main/schema/2025-06-18/schema.ts
library;
import 'dart:collection';
@@ -11,6 +12,7 @@
import 'package:json_rpc_2/json_rpc_2.dart';
part 'completions.dart';
+part 'elicitation.dart';
part 'initialization.dart';
part 'logging.dart';
part 'prompts.dart';
@@ -22,7 +24,8 @@
/// Enum of the known protocol versions.
enum ProtocolVersion {
v2024_11_05('2024-11-05'),
- v2025_03_26('2025-03-26');
+ v2025_03_26('2025-03-26'),
+ v2025_06_18('2025-06-18');
const ProtocolVersion(this.versionString);
@@ -35,7 +38,7 @@
static const oldestSupported = ProtocolVersion.v2024_11_05;
/// The most recent version supported by the current API.
- static const latestSupported = ProtocolVersion.v2025_03_26;
+ static const latestSupported = ProtocolVersion.v2025_06_18;
/// The version string used over the wire to identify this version.
final String versionString;
@@ -58,9 +61,65 @@
/// An opaque token used to represent a cursor for pagination.
extension type Cursor(String _) {}
-/// Generic metadata passed with most requests, can be anything.
+/// Generic metadata passed with most requests.
+///
+/// Metadata reserved by MCP to allow clients and servers to attach additional
+/// metadata to their interactions.
+///
+/// Certain key names are reserved by MCP for protocol-level metadata, as
+/// specified below; implementations MUST NOT make assumptions about values at
+/// these keys.
+///
+/// Additionally, definitions in the schema may reserve particular names for
+/// purpose-specific metadata, as declared in those definitions.
+///
+/// Key name format: valid `_meta` key names have two segments: an optional
+/// prefix, and a name.
+///
+/// - Prefix: If specified, MUST be a series of labels separated by dots
+/// (`.`), followed by a slash (`/`). Labels MUST start with a letter and
+/// end with a letter or digit; interior characters can be letters, digits,
+/// or hyphens (`-`). Any prefix beginning with zero or more valid labels,
+/// followed by `modelcontextprotocol` or `mcp`, followed by any valid
+/// label, is reserved for MCP use. For example: `modelcontextprotocol.io/`,
+/// `mcp.dev/`, `api.modelcontextprotocol.org/`, and `tools.mcp.com/` are
+/// all reserved.
+/// - 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) {}
+/// 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)
+ implements Meta {
+ factory BaseMetadata({required String name, String? title}) =>
+ BaseMetadata.fromMap({'name': name, 'title': title});
+
+ /// Intended for programmatic or logical use, but used as a display name in
+ /// past specs for fallback (if title isn't present).
+ String get name {
+ final name = _value['name'] as String?;
+ if (name == null) {
+ throw ArgumentError('Missing name field in $runtimeType');
+ }
+ return name;
+ }
+
+ /// A short title for this object.
+ ///
+ /// Intended for UI and end-user contexts — optimized to be human-readable and
+ /// easily understood, even by those unfamiliar with domain-specific
+ /// terminology.
+ ///
+ /// If not provided, the name should be used for display (except for Tool,
+ /// where `annotations.title` should be given precedence over using `name`, if
+ /// present).
+ String? get title => _value['title'] as String?;
+}
+
/// A "mixin"-like extension type for any extension type that might contain a
/// [ProgressToken] at the key "progressToken".
///
@@ -78,10 +137,22 @@
MetaWithProgressToken.fromMap({'progressToken': progressToken});
}
+/// 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) {
+ /// The `_meta` property/parameter is reserved by MCP to allow clients and
+ /// servers to attach additional metadata to their interactions.
+ ///
+ /// See [Meta] for more information about the format of these values.
+ Meta? get meta => _value['_meta'] as Meta?;
+}
+
/// Base interface for all request types.
///
/// Should not be constructed directly, and has no public constructor.
-extension type Request._fromMap(Map<String, Object?> _value) {
+extension type Request._fromMap(Map<String, Object?> _value)
+ implements WithMetadata {
/// If specified, the caller is requesting out-of-band progress notifications
/// for this request (as represented by notifications/progress).
///
@@ -273,15 +344,19 @@
/// Text provided to or from an LLM.
extension type TextContent.fromMap(Map<String, Object?> _value)
- implements Content, Annotated {
+ implements Content, Annotated, WithMetadata {
static const expectedType = 'text';
- factory TextContent({required String text, Annotations? annotations}) =>
- TextContent.fromMap({
- 'text': text,
- 'type': expectedType,
- if (annotations != null) 'annotations': annotations,
- });
+ factory TextContent({
+ required String text,
+ Annotations? annotations,
+ Meta? meta,
+ }) => TextContent.fromMap({
+ 'text': text,
+ 'type': expectedType,
+ if (annotations != null) 'annotations': annotations,
+ if (meta != null) '_meta': meta,
+ });
String get type {
final type = _value['type'] as String;
@@ -295,18 +370,20 @@
/// An image provided to or from an LLM.
extension type ImageContent.fromMap(Map<String, Object?> _value)
- implements Content, Annotated {
+ implements Content, Annotated, WithMetadata {
static const expectedType = 'image';
factory ImageContent({
required String data,
required String mimeType,
Annotations? annotations,
+ Meta? meta,
}) => ImageContent.fromMap({
'data': data,
'mimeType': mimeType,
'type': expectedType,
if (annotations != null) 'annotations': annotations,
+ if (meta != null) '_meta': meta,
});
String get type {
@@ -328,18 +405,20 @@
///
/// Only supported since version [ProtocolVersion.v2025_03_26].
extension type AudioContent.fromMap(Map<String, Object?> _value)
- implements Content, Annotated {
+ implements Content, Annotated, WithMetadata {
static const expectedType = 'audio';
factory AudioContent({
required String data,
required String mimeType,
Annotations? annotations,
+ Meta? meta,
}) => AudioContent.fromMap({
'data': data,
'mimeType': mimeType,
'type': expectedType,
if (annotations != null) 'annotations': annotations,
+ if (meta != null) '_meta': meta,
});
String get type {
@@ -362,16 +441,18 @@
/// It is up to the client how best to render embedded resources for the benefit
/// of the LLM and/or the user.
extension type EmbeddedResource.fromMap(Map<String, Object?> _value)
- implements Content, Annotated {
+ implements Content, Annotated, WithMetadata {
static const expectedType = 'resource';
factory EmbeddedResource({
required Content resource,
Annotations? annotations,
+ Meta? meta,
}) => EmbeddedResource.fromMap({
'resource': resource,
'type': expectedType,
if (annotations != null) 'annotations': annotations,
+ if (meta != null) '_meta': meta,
});
String get type {
@@ -386,6 +467,76 @@
String? get mimeType => _value['mimeType'] as String?;
}
+/// A resource link returned from a tool.
+///
+/// Resource links returned by tools are not guaranteed to appear in the results
+/// of a `resources/list` request.
+extension type ResourceLink.fromMap(Map<String, Object?> _value)
+ implements Content, Annotated, WithMetadata, BaseMetadata {
+ static const expectedType = 'resource_link';
+
+ factory ResourceLink({
+ required String name,
+ String? title,
+ required String description,
+ required String uri,
+ required String mimeType,
+ Annotations? annotations,
+ Meta? meta,
+ }) => ResourceLink.fromMap({
+ 'name': name,
+ if (title != null) 'title': title,
+ 'description': description,
+ 'uri': uri,
+ 'mimeType': mimeType,
+ 'type': expectedType,
+ if (annotations != null) 'annotations': annotations,
+ if (meta != null) '_meta': meta,
+ });
+
+ String get type {
+ final type = _value['type'] as String;
+ assert(type == expectedType);
+ return type;
+ }
+
+ /// The name of the resource.
+ String get name {
+ final name = _value['name'] as String?;
+ if (name == null) {
+ throw ArgumentError('Missing name field in $ResourceLink.');
+ }
+ return name;
+ }
+
+ /// The description of the resource.
+ String get description {
+ final description = _value['description'] as String?;
+ if (description == null) {
+ throw ArgumentError('Missing description field in $ResourceLink.');
+ }
+ return description;
+ }
+
+ /// The URI of the resource.
+ String get uri {
+ final uri = _value['uri'] as String?;
+ if (uri == null) {
+ throw ArgumentError('Missing uri field in $ResourceLink.');
+ }
+ return uri;
+ }
+
+ /// The MIME type of the resource.
+ String get mimeType {
+ final mimeType = _value['mimeType'] as String?;
+ if (mimeType == null) {
+ throw ArgumentError('Missing mimeType field in $ResourceLink.');
+ }
+ return mimeType;
+ }
+}
+
/// Base type for objects that include optional annotations for the client.
///
/// The client can use annotations to inform how objects are used or displayed.
@@ -396,10 +547,15 @@
/// The annotations for an [Annotated] object.
extension type Annotations.fromMap(Map<String, Object?> _value) {
- factory Annotations({List<Role>? audience, double? priority}) {
+ factory Annotations({
+ List<Role>? audience,
+ DateTime? lastModified,
+ double? priority,
+ }) {
assert(priority == null || (priority >= 0 && priority <= 1));
return Annotations.fromMap({
if (audience != null) 'audience': [for (var role in audience) role.name],
+ if (lastModified != null) 'lastModified': lastModified.toIso8601String(),
if (priority != null) 'priority': priority,
});
}
@@ -416,6 +572,18 @@
];
}
+ /// Describes when this data was last modified.
+ ///
+ /// The moment the resource was last modified.
+ ///
+ /// Examples: last activity timestamp in an open file, timestamp when the
+ /// resource was attached, etc.
+ DateTime? get lastModified {
+ final lastModified = _value['lastModified'] as String?;
+ if (lastModified == null) return null;
+ return DateTime.parse(lastModified);
+ }
+
/// Describes how important this data is for operating the server.
///
/// A value of 1 means "most important," and indicates that the data is
diff --git a/pkgs/dart_mcp/lib/src/api/completions.dart b/pkgs/dart_mcp/lib/src/api/completions.dart
index c5072f9..1d8b929 100644
--- a/pkgs/dart_mcp/lib/src/api/completions.dart
+++ b/pkgs/dart_mcp/lib/src/api/completions.dart
@@ -14,18 +14,20 @@
factory CompleteRequest({
required Reference ref,
required CompletionArgument argument,
+ CompletionContext? context,
MetaWithProgressToken? meta,
}) => CompleteRequest.fromMap({
'ref': ref,
'argument': argument,
+ if (context != null) 'context': context,
if (meta != null) '_meta': meta,
});
/// A reference to the thing to complete.
///
- /// See the [PromptReference] and [ResourceReference] types.
+ /// See the [PromptReference] and [ResourceTemplateReference] types.
///
- /// In the case of a [ResourceReference], it must refer to a
+ /// In the case of a [ResourceTemplateReference], it must refer to a
/// [ResourceTemplate].
Reference get ref {
final ref = _value['ref'] as Reference?;
@@ -43,6 +45,9 @@
}
return argument;
}
+
+ /// Additional, optional context for completions.
+ CompletionContext? get context => _value['context'] as CompletionContext?;
}
/// The server's response to a completion/complete request
@@ -105,7 +110,18 @@
String get value => _value['value'] as String;
}
-/// Union type for references, see [PromptReference] and [ResourceReference].
+/// A context passed to a [CompleteRequest].
+extension type CompletionContext.fromMap(Map<String, Object?> _value) {
+ factory CompletionContext({Map<String, String>? arguments}) =>
+ CompletionContext.fromMap({'arguments': arguments});
+
+ /// Previously-resolved variables in a URI template or prompt.
+ Map<String, String>? get arguments =>
+ (_value['arguments'] as Map?)?.cast<String, String>();
+}
+
+/// Union type for references, see [PromptReference] and
+/// [ResourceTemplateReference].
extension type Reference._(Map<String, Object?> _value) {
factory Reference.fromMap(Map<String, Object?> value) {
assert(value.containsKey('type'));
@@ -115,8 +131,9 @@
/// Whether or not this is a [PromptReference].
bool get isPrompt => _value['type'] == PromptReference.expectedType;
- /// Whether or not this is a [ResourceReference].
- bool get isResource => _value['type'] == ResourceReference.expectedType;
+ /// Whether or not this is a [ResourceTemplateReference].
+ bool get isResource =>
+ _value['type'] == ResourceTemplateReference.expectedType;
/// The type of reference.
///
@@ -127,12 +144,12 @@
}
/// A reference to a resource or resource template definition.
-extension type ResourceReference.fromMap(Map<String, Object?> _value)
+extension type ResourceTemplateReference.fromMap(Map<String, Object?> _value)
implements Reference {
static const expectedType = 'ref/resource';
- factory ResourceReference({required String uri}) =>
- ResourceReference.fromMap({'uri': uri, 'type': expectedType});
+ factory ResourceTemplateReference({required String uri}) =>
+ ResourceTemplateReference.fromMap({'uri': uri, 'type': expectedType});
/// This should always be [expectedType].
///
@@ -148,13 +165,20 @@
String get uri => _value['uri'] as String;
}
+@Deprecated('Use ResourceTemplateReference instead')
+typedef ResourceReference = ResourceTemplateReference;
+
/// Identifies a prompt.
extension type PromptReference.fromMap(Map<String, Object?> _value)
- implements Reference {
+ implements Reference, BaseMetadata {
static const expectedType = 'ref/prompt';
- factory PromptReference({required String name}) =>
- PromptReference.fromMap({'name': name, 'type': expectedType});
+ factory PromptReference({required String name, String? title}) =>
+ PromptReference.fromMap({
+ 'name': name,
+ 'title': title,
+ 'type': expectedType,
+ });
/// This should always be [expectedType].
///
@@ -165,7 +189,4 @@
assert(type == expectedType);
return type;
}
-
- /// The name of the prompt or prompt template
- String get name => _value['name'] as String;
}
diff --git a/pkgs/dart_mcp/lib/src/api/elicitation.dart b/pkgs/dart_mcp/lib/src/api/elicitation.dart
new file mode 100644
index 0000000..edc83ee
--- /dev/null
+++ b/pkgs/dart_mcp/lib/src/api/elicitation.dart
@@ -0,0 +1,139 @@
+// Copyright (c) 2025, 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.
+
+part of 'api.dart';
+
+/// The parameters for an `elicitation/create` request.
+extension type ElicitRequest._fromMap(Map<String, Object?> _value)
+ implements Request {
+ static const methodName = 'elicitation/create';
+
+ factory ElicitRequest({
+ required String message,
+ required Schema requestedSchema,
+ }) {
+ assert(
+ validateRequestedSchema(requestedSchema),
+ 'Invalid requestedSchema. Must be a flat object of primitive values.',
+ );
+ return ElicitRequest._fromMap({
+ 'message': message,
+ 'requestedSchema': requestedSchema,
+ });
+ }
+
+ /// A message to display to the user when collecting the response.
+ String get message {
+ final message = _value['message'] as String?;
+ if (message == null) {
+ throw ArgumentError('Missing required message field in $ElicitRequest');
+ }
+ return message;
+ }
+
+ /// A JSON schema that describes the expected response.
+ ///
+ /// The content may only consist of a flat object (no nested maps or lists)
+ /// with primitive values (`String`, `num`, `bool`, `enum`).
+ ///
+ /// You can use [validateRequestedSchema] to validate that a schema conforms
+ /// to these limitations.
+ Schema get requestedSchema {
+ final requestedSchema = _value['requestedSchema'] as Schema?;
+ if (requestedSchema == null) {
+ throw ArgumentError(
+ 'Missing required requestedSchema field in $ElicitRequest',
+ );
+ }
+ return requestedSchema;
+ }
+
+ /// Validates the [schema] to make sure that it conforms to the
+ /// limitations of the spec.
+ ///
+ /// See also: [requestedSchema] for a description of the spec limitations.
+ static bool validateRequestedSchema(Schema schema) {
+ if (schema.type != JsonType.object) {
+ return false;
+ }
+
+ final objectSchema = schema as ObjectSchema;
+ final properties = objectSchema.properties;
+
+ if (properties == null) {
+ return true; // No properties to validate.
+ }
+
+ for (final propertySchema in properties.values) {
+ // Combinators would mean it's not a simple primitive type.
+ if (propertySchema.allOf != null ||
+ propertySchema.anyOf != null ||
+ propertySchema.oneOf != null ||
+ propertySchema.not != null) {
+ return false;
+ }
+
+ switch (propertySchema.type) {
+ case JsonType.string:
+ case JsonType.num:
+ case JsonType.int:
+ case JsonType.bool:
+ case JsonType.enumeration:
+ break;
+ case JsonType.object:
+ case JsonType.list:
+ case JsonType.nil:
+ case null:
+ // Disallowed, or no type specified.
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
+
+/// The client's response to an `elicitation/create` request.
+extension type ElicitResult.fromMap(Map<String, Object?> _value)
+ implements Result {
+ factory ElicitResult({
+ required ElicitationAction action,
+ Map<String, Object?>? content,
+ }) => ElicitResult.fromMap({'action': action.name, 'content': content});
+
+ /// The action taken by the user in response to an elicitation request.
+ ///
+ /// - [ElicitationAction.accept]: The user accepted the request and provided
+ /// the requested information.
+ /// - [ElicitationAction.reject]: The user explicitly declined the action.
+ /// - [ElicitationAction.cancel]: The user dismissed without making an
+ /// explicit choice.
+ ElicitationAction get action {
+ final action = _value['action'] as String?;
+ if (action == null) {
+ throw ArgumentError('Missing required action field in $ElicitResult');
+ }
+ return ElicitationAction.values.byName(action);
+ }
+
+ /// The content of the response, if the user accepted the request.
+ ///
+ /// Must be `null` if the user didn't accept the request.
+ ///
+ /// The content must conform to the [ElicitRequest]'s `requestedSchema`.
+ Map<String, Object?>? get content =>
+ _value['content'] as Map<String, Object?>?;
+}
+
+/// The action taken by the user in response to an elicitation request.
+enum ElicitationAction {
+ /// The user accepted the request and provided the requested information.
+ accept,
+
+ /// The user explicitly declined the action.
+ reject,
+
+ /// The user dismissed without making an explicit choice.
+ cancel,
+}
diff --git a/pkgs/dart_mcp/lib/src/api/initialization.dart b/pkgs/dart_mcp/lib/src/api/initialization.dart
index 80da37c..81d7b6b 100644
--- a/pkgs/dart_mcp/lib/src/api/initialization.dart
+++ b/pkgs/dart_mcp/lib/src/api/initialization.dart
@@ -113,10 +113,12 @@
Map<String, Object?>? experimental,
RootsCapabilities? roots,
Map<String, Object?>? sampling,
+ ElicitationCapability? elicitation,
}) => ClientCapabilities.fromMap({
if (experimental != null) 'experimental': experimental,
if (roots != null) 'roots': roots,
if (sampling != null) 'sampling': sampling,
+ if (elicitation != null) 'elicitation': elicitation,
});
/// Experimental, non-standard capabilities that the client supports.
@@ -147,6 +149,16 @@
assert(sampling == null);
_value['sampling'] = value;
}
+
+ /// Present if the client supports elicitation.
+ ElicitationCapability? get elicitation =>
+ _value['elicitation'] as ElicitationCapability?;
+
+ /// Sets [elicitation], asserting it is null first.
+ set elicitation(ElicitationCapability? value) {
+ assert(elicitation == null);
+ _value['elicitation'] = value;
+ }
}
/// Whether the client supports notifications for changes to the roots list.
@@ -165,6 +177,11 @@
}
}
+/// Whether the client supports elicitation.
+extension type ElicitationCapability.fromMap(Map<String, Object?> _value) {
+ factory ElicitationCapability() => ElicitationCapability.fromMap({});
+}
+
/// Capabilities that a server may support.
///
/// Known capabilities are defined here, in this schema, but this is not a
@@ -177,12 +194,14 @@
Prompts? prompts,
Resources? resources,
Tools? tools,
+ Elicitation? elicitation,
}) => ServerCapabilities.fromMap({
if (experimental != null) 'experimental': experimental,
if (logging != null) 'logging': logging,
if (prompts != null) 'prompts': prompts,
if (resources != null) 'resources': resources,
if (tools != null) 'tools': tools,
+ if (elicitation != null) 'elicitation': elicitation,
});
/// Experimental, non-standard capabilities that the server supports.
@@ -240,6 +259,15 @@
assert(tools == null);
_value['tools'] = value;
}
+
+ /// Present if the server supports elicitation.
+ Elicitation? get elicitation => _value['elicitation'] as Elicitation?;
+
+ /// Sets [elicitation] if it is null, otherwise asserts.
+ set elicitation(Elicitation? value) {
+ assert(elicitation == null);
+ _value['elicitation'] = value;
+ }
}
/// Completions parameter for [ServerCapabilities].
@@ -304,13 +332,31 @@
}
}
-/// Describes the name and version of an MCP implementation.
-extension type Implementation.fromMap(Map<String, Object?> _value) {
- factory Implementation({required String name, required String version}) =>
- Implementation.fromMap({'name': name, 'version': version});
+/// Elicitation parameter for [ServerCapabilities].
+extension type Elicitation.fromMap(Map<String, Object?> _value) {
+ factory Elicitation() => Elicitation.fromMap({});
+}
- String get name => _value['name'] as String;
- String get version => _value['version'] as String;
+/// Describes the name and version of an MCP implementation.
+extension type Implementation.fromMap(Map<String, Object?> _value)
+ implements BaseMetadata {
+ factory Implementation({
+ required String name,
+ required String version,
+ String? title,
+ }) => Implementation.fromMap({
+ 'name': name,
+ 'version': version,
+ if (title != null) 'title': title,
+ });
+
+ String get version {
+ final version = _value['version'] as String?;
+ if (version == null) {
+ throw ArgumentError('Missing version field in $Implementation.');
+ }
+ return version;
+ }
}
@Deprecated('Use Implementation instead.')
diff --git a/pkgs/dart_mcp/lib/src/api/prompts.dart b/pkgs/dart_mcp/lib/src/api/prompts.dart
index a658725..86fa48e 100644
--- a/pkgs/dart_mcp/lib/src/api/prompts.dart
+++ b/pkgs/dart_mcp/lib/src/api/prompts.dart
@@ -86,7 +86,8 @@
}
/// A prompt or prompt template that the server offers.
-extension type Prompt.fromMap(Map<String, Object?> _value) {
+extension type Prompt.fromMap(Map<String, Object?> _value)
+ implements BaseMetadata {
factory Prompt({
required String name,
String? description,
@@ -97,9 +98,6 @@
if (arguments != null) 'arguments': arguments,
});
- /// The name of the prompt or prompt template.
- String get name => _value['name'] as String;
-
/// An optional description of what this prompt provides.
String? get description => _value['description'] as String?;
@@ -108,20 +106,20 @@
}
/// Describes an argument that a prompt can accept.
-extension type PromptArgument.fromMap(Map<String, Object?> _value) {
+extension type PromptArgument.fromMap(Map<String, Object?> _value)
+ implements BaseMetadata {
factory PromptArgument({
required String name,
+ String? title,
String? description,
bool? required,
}) => PromptArgument.fromMap({
'name': name,
+ if (title != null) 'title': title,
if (description != null) 'description': description,
if (required != null) 'required': required,
});
- /// The name of the argument.
- String get name => _value['name'] as String;
-
/// A human-readable description of the argument.
String? get description => _value['description'] as String?;
diff --git a/pkgs/dart_mcp/lib/src/api/resources.dart b/pkgs/dart_mcp/lib/src/api/resources.dart
index 5b6da07..799502d 100644
--- a/pkgs/dart_mcp/lib/src/api/resources.dart
+++ b/pkgs/dart_mcp/lib/src/api/resources.dart
@@ -191,7 +191,7 @@
/// A known resource that the server is capable of reading.
extension type Resource.fromMap(Map<String, Object?> _value)
- implements Annotated {
+ implements Annotated, BaseMetadata, WithMetadata {
factory Resource({
required String uri,
required String name,
@@ -199,6 +199,7 @@
String? description,
String? mimeType,
int? size,
+ Meta? meta,
}) => Resource.fromMap({
'uri': uri,
'name': name,
@@ -206,16 +207,12 @@
if (description != null) 'description': description,
if (mimeType != null) 'mimeType': mimeType,
if (size != null) 'size': size,
+ if (meta != null) '_meta': meta,
});
/// The URI of this resource.
String get uri => _value['uri'] as String;
- /// A human-readable name for this resource.
- ///
- /// This can be used by clients to populate UI elements.
- String get name => _value['name'] as String;
-
/// A description of what this resource represents.
///
/// This can be used by clients to improve the LLM's understanding of
@@ -235,30 +232,29 @@
/// A template description for resources available on the server.
extension type ResourceTemplate.fromMap(Map<String, Object?> _value)
- implements Annotated {
+ implements Annotated, BaseMetadata, WithMetadata {
factory ResourceTemplate({
required String uriTemplate,
required String name,
- Annotations? annotations,
+ String? title,
String? description,
+ Annotations? annotations,
String? mimeType,
+ Meta? meta,
}) => ResourceTemplate.fromMap({
'uriTemplate': uriTemplate,
'name': name,
- if (annotations != null) 'annotations': annotations,
+ if (title != null) 'title': title,
if (description != null) 'description': description,
+ if (annotations != null) 'annotations': annotations,
if (mimeType != null) 'mimeType': mimeType,
+ if (meta != null) '_meta': meta,
});
/// A URI template (according to RFC 6570) that can be used to construct
/// resource URIs.
String get uriTemplate => _value['uriTemplate'] as String;
- /// A human-readable name for the type of resource this template refers to.
- ///
- /// This can be used by clients to populate UI elements.
- String get name => _value['name'] as String;
-
/// A description of what this template is for.
///
/// This can be used by clients to improve the LLM's understanding of
@@ -276,7 +272,8 @@
///
/// Could be either [TextResourceContents] or [BlobResourceContents],
/// use [isText] and [isBlob] before casting to the more specific type.
-extension type ResourceContents.fromMap(Map<String, Object?> _value) {
+extension type ResourceContents.fromMap(Map<String, Object?> _value)
+ implements WithMetadata {
/// Whether or not this represents [TextResourceContents].
bool get isText => _value.containsKey('text');
@@ -297,10 +294,12 @@
required String uri,
required String text,
String? mimeType,
+ Meta? meta,
}) => TextResourceContents.fromMap({
'uri': uri,
'text': text,
if (mimeType != null) 'mimeType': mimeType,
+ if (meta != null) '_meta': meta,
});
/// The text of the item.
@@ -317,10 +316,12 @@
required String uri,
required String blob,
String? mimeType,
+ Meta? meta,
}) => BlobResourceContents.fromMap({
'uri': uri,
'blob': blob,
if (mimeType != null) 'mimeType': mimeType,
+ if (meta != null) '_meta': meta,
});
/// A base64-encoded string representing the binary data of the item.
diff --git a/pkgs/dart_mcp/lib/src/api/roots.dart b/pkgs/dart_mcp/lib/src/api/roots.dart
index f7194a3..94139e6 100644
--- a/pkgs/dart_mcp/lib/src/api/roots.dart
+++ b/pkgs/dart_mcp/lib/src/api/roots.dart
@@ -43,15 +43,26 @@
}
/// Represents a root directory or file that the server can operate on.
-extension type Root.fromMap(Map<String, Object?> _value) {
- factory Root({required String uri, String? name}) =>
- Root.fromMap({'uri': uri, if (name != null) 'name': name});
+extension type Root.fromMap(Map<String, Object?> _value)
+ implements WithMetadata {
+ factory Root({required String uri, String? name, Meta? meta}) =>
+ Root.fromMap({
+ 'uri': uri,
+ if (name != null) 'name': name,
+ if (meta != null) '_meta': meta,
+ });
/// The URI identifying the root.
///
/// This *must* start with file:// for now. This restriction may be relaxed
/// in future versions of the protocol to allow other URI schemes.
- String get uri => _value['uri'] as String;
+ String get uri {
+ final uri = _value['uri'] as String?;
+ if (uri == null) {
+ throw ArgumentError('Missing uri field in $Root.');
+ }
+ return uri;
+ }
/// An optional name for the root.
///
@@ -67,7 +78,7 @@
/// This notification should be sent whenever the client adds, removes, or
/// modifies any root.
/// The server should then request an updated list of roots using the
-/// ListRootsRequest.
+/// [ListRootsRequest].
extension type RootsListChangedNotification.fromMap(Map<String, Object?> _value)
implements Notification {
static const methodName = 'notifications/roots/list_changed';
diff --git a/pkgs/dart_mcp/lib/src/api/tools.dart b/pkgs/dart_mcp/lib/src/api/tools.dart
index 407566d..8ea339b 100644
--- a/pkgs/dart_mcp/lib/src/api/tools.dart
+++ b/pkgs/dart_mcp/lib/src/api/tools.dart
@@ -53,15 +53,17 @@
factory CallToolResult({
Meta? meta,
required List<Content> content,
+ Map<String, Object?>? structuredContent,
bool? isError,
}) => CallToolResult.fromMap({
'content': content,
+ if (structuredContent != null) 'structuredContent': structuredContent,
if (isError != null) 'isError': isError,
if (meta != null) '_meta': meta,
});
- /// The type of content, either [TextContent], [ImageContent],
- /// or [EmbeddedResource],
+ /// The returned content, either [TextContent], [ImageContent],
+ /// [AudioContent], [ResourceLink] or [EmbeddedResource].
List<Content> get content {
final content = (_value['content'] as List?)?.cast<Content>();
if (content == null) {
@@ -70,6 +72,11 @@
return content;
}
+ /// The content as structured output, if the [Tool] declared an
+ /// `outputSchema`.
+ Map<String, Object?>? get structuredContent =>
+ _value['structuredContent'] as Map<String, Object?>?;
+
/// Whether the tool call ended in an error.
///
/// If not set, this is assumed to be false (the call was successful).
@@ -119,18 +126,27 @@
}
/// Definition for a tool the client can call.
-extension type Tool.fromMap(Map<String, Object?> _value) {
+extension type Tool.fromMap(Map<String, Object?> _value)
+ implements BaseMetadata {
factory Tool({
required String name,
+ String? title,
String? description,
required ObjectSchema inputSchema,
+ // Only supported since version `ProtocolVersion.v2025_06_18`.
+ ObjectSchema? outputSchema,
// Only supported since version `ProtocolVersion.v2025_03_26`.
ToolAnnotations? annotations,
+ // Only supported since version `ProtocolVersion.v2025_03_26`.
+ Meta? meta,
}) => Tool.fromMap({
'name': name,
+ if (title != null) 'title': title,
if (description != null) 'description': description,
'inputSchema': inputSchema,
+ if (outputSchema != null) 'outputSchema': outputSchema,
if (annotations != null) 'annotations': annotations,
+ if (meta != null) '_meta': meta,
});
/// Optional additional tool information.
@@ -140,15 +156,6 @@
(_value['annotations'] as Map?)?.cast<String, Object?>()
as ToolAnnotations?;
- /// The name of the tool.
- String get name {
- final name = _value['name'] as String?;
- if (name == null) {
- throw ArgumentError('Missing name field in $Tool');
- }
- return name;
- }
-
/// A human-readable description of the tool.
String? get description => _value['description'] as String?;
@@ -161,6 +168,13 @@
}
return inputSchema;
}
+
+ /// An optional JSON [ObjectSchema] object defining the expected schema of the
+ /// tool output.
+ ///
+ /// If the `outputSchema` is specified, then the output from the tool must
+ /// conform to the schema.
+ ObjectSchema? get outputSchema => _value['outputSchema'] as ObjectSchema?;
}
/// Additional properties describing a Tool to clients.
diff --git a/pkgs/dart_mcp/lib/src/client/client.dart b/pkgs/dart_mcp/lib/src/client/client.dart
index cde9930..1b1a919 100644
--- a/pkgs/dart_mcp/lib/src/client/client.dart
+++ b/pkgs/dart_mcp/lib/src/client/client.dart
@@ -15,6 +15,7 @@
import '../api/api.dart';
import '../shared.dart';
+part 'elicitation_support.dart';
part 'roots_support.dart';
part 'sampling_support.dart';
@@ -104,6 +105,7 @@
protocolLogSink: protocolLogSink,
rootsSupport: self is RootsSupport ? self : null,
samplingSupport: self is SamplingSupport ? self : null,
+ elicitationSupport: self is ElicitationSupport ? self : null,
);
connections.add(connection);
channel.sink.done.then((_) => connections.remove(connection));
@@ -122,7 +124,7 @@
/// An active server connection.
base class ServerConnection extends MCPBase {
- /// The version of the protocol that was negotiated during intialization.
+ /// The version of the protocol that was negotiated during initialization.
///
/// Some APIs may error if you attempt to use them without first checking the
/// protocol version.
@@ -201,6 +203,7 @@
super.protocolLogSink,
RootsSupport? rootsSupport,
SamplingSupport? samplingSupport,
+ ElicitationSupport? elicitationSupport,
}) {
if (rootsSupport != null) {
registerRequestHandler(
@@ -217,6 +220,12 @@
);
}
+ if (elicitationSupport != null) {
+ registerRequestHandler(ElicitRequest.methodName, (ElicitRequest request) {
+ return elicitationSupport.handleElicitation(request);
+ });
+ }
+
registerNotificationHandler(
PromptListChangedNotification.methodName,
_promptListChangedController.sink.add,
@@ -277,8 +286,10 @@
serverInfo = response.serverInfo;
serverCapabilities = response.capabilities;
final serverVersion = response.protocolVersion;
- if (serverVersion?.isSupported != true) {
+ if (serverVersion == null || !serverVersion.isSupported) {
await shutdown();
+ } else {
+ protocolVersion = serverVersion;
}
return response;
}
diff --git a/pkgs/dart_mcp/lib/src/client/elicitation_support.dart b/pkgs/dart_mcp/lib/src/client/elicitation_support.dart
new file mode 100644
index 0000000..b847c3d
--- /dev/null
+++ b/pkgs/dart_mcp/lib/src/client/elicitation_support.dart
@@ -0,0 +1,22 @@
+// Copyright (c) 2025, 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.
+
+part of 'client.dart';
+
+/// The interface for handling elicitation requests.
+///
+/// Any client using [ElicitationSupport] must implement this interface.
+abstract interface class WithElicitationHandler {
+ FutureOr<ElicitResult> handleElicitation(ElicitRequest request);
+}
+
+/// A mixin that adds support for the `elicitation` capability to an
+/// [MCPClient].
+base mixin ElicitationSupport on MCPClient implements WithElicitationHandler {
+ @override
+ void initialize() {
+ capabilities.elicitation ??= ElicitationCapability();
+ super.initialize();
+ }
+}
diff --git a/pkgs/dart_mcp/lib/src/server/elicitation_request_support.dart b/pkgs/dart_mcp/lib/src/server/elicitation_request_support.dart
new file mode 100644
index 0000000..6bdf0e1
--- /dev/null
+++ b/pkgs/dart_mcp/lib/src/server/elicitation_request_support.dart
@@ -0,0 +1,36 @@
+part of 'server.dart';
+
+/// A mixin that adds support for making `elicitation/create` requests to a
+/// [MCPServer].
+base mixin ElicitationRequestSupport on LoggingSupport {
+ /// Whether or not the connected client supports elicitation.
+ ///
+ /// Only safe to call after calling [initialize] on `super` since this is
+ /// based on the client capabilities.
+ bool get supportsElicitation => clientCapabilities.elicitation != null;
+
+ @override
+ FutureOr<InitializeResult> initialize(InitializeRequest request) {
+ initialized.then((_) {
+ if (!supportsElicitation) {
+ log(
+ LoggingLevel.warning,
+ 'Client does not support the elicitation capability, some '
+ 'functionality may be disabled.',
+ );
+ }
+ });
+ return super.initialize(request);
+ }
+
+ /// Sends an `elicitation/create` request to the client.
+ ///
+ /// This method will only succeed if the client has advertised the
+ /// `elicitation` capability.
+ Future<ElicitResult> elicit(ElicitRequest request) async {
+ if (!supportsElicitation) {
+ throw StateError('Client does not support elicitation');
+ }
+ return sendRequest(ElicitRequest.methodName, request);
+ }
+}
diff --git a/pkgs/dart_mcp/lib/src/server/server.dart b/pkgs/dart_mcp/lib/src/server/server.dart
index a0ab0e0..43260fd 100644
--- a/pkgs/dart_mcp/lib/src/server/server.dart
+++ b/pkgs/dart_mcp/lib/src/server/server.dart
@@ -12,6 +12,7 @@
import '../shared.dart';
part 'completions_support.dart';
+part 'elicitation_request_support.dart';
part 'logging_support.dart';
part 'prompts_support.dart';
part 'resources_support.dart';
diff --git a/pkgs/dart_mcp/test/api/api_test.dart b/pkgs/dart_mcp/test/api/api_test.dart
index faddb6b..e26a237 100644
--- a/pkgs/dart_mcp/test/api/api_test.dart
+++ b/pkgs/dart_mcp/test/api/api_test.dart
@@ -61,6 +61,16 @@
group('API object validation', () {
test('throws when required fields are missing', () {
+ expect(() => Root.fromMap({}).uri, throwsA(isA<ArgumentError>()));
+ expect(
+ () => Implementation.fromMap({'name': 'test'}).version,
+ throwsA(isA<ArgumentError>()),
+ );
+ expect(
+ () => BaseMetadata.fromMap({}).name,
+ throwsA(isA<ArgumentError>()),
+ );
+
final empty = <String, Object?>{};
// Initialization
@@ -104,5 +114,14 @@
throwsArgumentError,
);
});
+ test('meta field is parsed correctly', () {
+ final root = Root.fromMap({
+ 'uri': 'file:///foo/bar',
+ '_meta': {'foo': 'bar'},
+ });
+ expect(root.meta, isNotNull);
+ final metaMap = root.meta as Map;
+ expect(metaMap['foo'], 'bar');
+ });
});
}
diff --git a/pkgs/dart_mcp/test/api/completions_test.dart b/pkgs/dart_mcp/test/api/completions_test.dart
index 04c542c..7a9730f 100644
--- a/pkgs/dart_mcp/test/api/completions_test.dart
+++ b/pkgs/dart_mcp/test/api/completions_test.dart
@@ -66,6 +66,27 @@
TestMCPServerWithCompletions.packagePaths,
);
});
+
+ test('client can request resource completions with context', () async {
+ final environment = TestEnvironment(
+ TestMCPClient(),
+ TestMCPServerWithCompletions.new,
+ );
+ final initializeResult = await environment.initializeServer();
+ expect(initializeResult.capabilities.completions, Completions());
+
+ final serverConnection = environment.serverConnection;
+ expect(
+ (await serverConnection.requestCompletions(
+ CompleteRequest(
+ ref: TestMCPServerWithCompletions.packageUriTemplateRef,
+ argument: CompletionArgument(name: 'path', value: 'a'),
+ context: CompletionContext(arguments: {'package_name': 'async'}),
+ ),
+ )).completion.values,
+ ['async.dart'],
+ );
+ });
}
final class TestMCPServerWithCompletions extends TestMCPServer
@@ -84,15 +105,23 @@
completion: Completion(values: cLanguages, hasMore: false),
);
case Reference(isResource: true)
- when (ref as ResourceReference).uri == packageUriTemplate.uriTemplate:
+ when (ref as ResourceTemplateReference).uri ==
+ packageUriTemplate.uriTemplate:
return switch (request.argument) {
CompletionArgument(name: 'package_name', value: 'a') =>
CompleteResult(
completion: Completion(values: aPackages, hasMore: false),
),
- CompletionArgument(name: 'path', value: 'a') => CompleteResult(
- completion: Completion(values: packagePaths, hasMore: false),
- ),
+ CompletionArgument(name: 'path', value: 'a') => switch (request
+ .context
+ ?.arguments?['package_name']) {
+ 'async' => CompleteResult(
+ completion: Completion(values: ['async.dart']),
+ ),
+ _ => CompleteResult(
+ completion: Completion(values: packagePaths, hasMore: false),
+ ),
+ },
_ =>
throw ArgumentError.value(
request.argument,
@@ -124,7 +153,7 @@
);
static final cLanguages = ['c', 'c++', 'c#'];
- static final packageUriTemplateRef = ResourceReference(
+ static final packageUriTemplateRef = ResourceTemplateReference(
uri: packageUriTemplate.uriTemplate,
);
static final packageUriTemplate = ResourceTemplate(
diff --git a/pkgs/dart_mcp/test/api/elicitation_test.dart b/pkgs/dart_mcp/test/api/elicitation_test.dart
new file mode 100644
index 0000000..fa53940
--- /dev/null
+++ b/pkgs/dart_mcp/test/api/elicitation_test.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2025, 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.
+
+import 'dart:async';
+
+import 'package:dart_mcp/client.dart';
+import 'package:dart_mcp/server.dart';
+import 'package:test/test.dart';
+
+import '../test_utils.dart';
+
+void main() {
+ group('elicitation', () {
+ test('server can elicit information from client', () async {
+ final elicitationCompleter = Completer<ElicitResult>();
+ final environment = TestEnvironment(
+ TestMCPClientWithElicitationSupport(
+ elicitationHandler: (request) {
+ return elicitationCompleter.future;
+ },
+ ),
+ TestMCPServerWithElicitationRequestSupport.new,
+ );
+ final server = environment.server;
+ unawaited(server.initialized);
+ await environment.serverConnection.initialize(
+ InitializeRequest(
+ protocolVersion: ProtocolVersion.latestSupported,
+ capabilities: environment.client.capabilities,
+ clientInfo: environment.client.implementation,
+ ),
+ );
+
+ final elicitationRequest = server.elicit(
+ ElicitRequest(
+ message: 'What is your name?',
+ requestedSchema: ObjectSchema(
+ properties: {'name': StringSchema(description: 'Your name')},
+ required: ['name'],
+ ),
+ ),
+ );
+
+ elicitationCompleter.complete(
+ ElicitResult(
+ action: ElicitationAction.accept,
+ content: {'name': 'John Doe'},
+ ),
+ );
+
+ final result = await elicitationRequest;
+ expect(result.action, ElicitationAction.accept);
+ expect(result.content, {'name': 'John Doe'});
+ });
+ });
+}
+
+final class TestMCPClientWithElicitationSupport extends TestMCPClient
+ with ElicitationSupport {
+ TestMCPClientWithElicitationSupport({required this.elicitationHandler});
+
+ FutureOr<ElicitResult> Function(ElicitRequest request) elicitationHandler;
+
+ @override
+ FutureOr<ElicitResult> handleElicitation(ElicitRequest request) {
+ return elicitationHandler(request);
+ }
+}
+
+base class TestMCPServerWithElicitationRequestSupport extends TestMCPServer
+ with LoggingSupport, ElicitationRequestSupport {
+ TestMCPServerWithElicitationRequestSupport(super.channel);
+}
diff --git a/pkgs/dart_mcp/test/api/tools_test.dart b/pkgs/dart_mcp/test/api/tools_test.dart
index 21bfb74..20f9a0d 100644
--- a/pkgs/dart_mcp/test/api/tools_test.dart
+++ b/pkgs/dart_mcp/test/api/tools_test.dart
@@ -4,9 +4,13 @@
// ignore_for_file: lines_longer_than_80_chars
-import 'package:dart_mcp/src/api/api.dart';
+import 'dart:async';
+
+import 'package:dart_mcp/server.dart';
import 'package:test/test.dart';
+import '../test_utils.dart';
+
void main() {
// Helper to strip path and details for comparison, keeping only the error
// field. Assumes e.error is non-null for any valid error generated by
@@ -56,12 +60,12 @@
// This relies on ValidationError's equality being based on its
// underlying map (including the path if present).
expect(
- actualErrors.toSet(),
- equals(expectedErrorsWithPaths.toSet()),
+ actualErrors.map((e) => e.toString()).toList()..sort(),
+ equals(expectedErrorsWithPaths.map((e) => e.toString()).toList()..sort()),
reason:
reason ??
- 'Data: $data. Expected (exact): $expectedErrorsWithPaths. '
- 'Actual (exact): $actualErrors',
+ 'Data: $data. Expected (exact): ${expectedErrorsWithPaths.map((e) => e.toString()).toSet()}. '
+ 'Actual (exact): ${actualErrors.map((e) => e.toString()).toSet()}',
);
}
@@ -1763,4 +1767,151 @@
);
});
});
+
+ group('Tool Communication', () {
+ test('can call a tool', () async {
+ final environment = TestEnvironment(
+ TestMCPClient(),
+ (channel) => TestMCPServerWithTools(
+ channel,
+ tools: [
+ Tool(
+ name: 'foo',
+ inputSchema: ObjectSchema(properties: {'bar': StringSchema()}),
+ ),
+ ],
+ toolHandlers: {
+ 'foo': (CallToolRequest request) {
+ return CallToolResult(
+ content: [
+ TextContent(
+ text: (request.arguments as Map)['bar'] as String,
+ ),
+ ],
+ );
+ },
+ },
+ ),
+ );
+ final serverConnection = environment.serverConnection;
+ await serverConnection.initialize(
+ InitializeRequest(
+ protocolVersion: ProtocolVersion.latestSupported,
+ capabilities: environment.client.capabilities,
+ clientInfo: environment.client.implementation,
+ ),
+ );
+ final request = CallToolRequest(name: 'foo', arguments: {'bar': 'baz'});
+ final result = await serverConnection.callTool(request);
+ expect(result.content, hasLength(1));
+ expect(result.content.first, isA<TextContent>());
+ final textContent = result.content.first as TextContent;
+ expect(textContent.text, 'baz');
+ });
+
+ test('can return a resource link', () async {
+ final environment = TestEnvironment(
+ TestMCPClient(),
+ (channel) => TestMCPServerWithTools(
+ channel,
+ tools: [Tool(name: 'foo', inputSchema: ObjectSchema())],
+ toolHandlers: {
+ 'foo': (request) {
+ return CallToolResult(
+ content: [
+ ResourceLink(
+ name: 'foo',
+ description: 'a description',
+ uri: 'https://google.com',
+ mimeType: 'text/html',
+ ),
+ ],
+ );
+ },
+ },
+ ),
+ );
+ final serverConnection = environment.serverConnection;
+ await serverConnection.initialize(
+ InitializeRequest(
+ protocolVersion: ProtocolVersion.latestSupported,
+ capabilities: environment.client.capabilities,
+ clientInfo: environment.client.implementation,
+ ),
+ );
+ final request = CallToolRequest(name: 'foo', arguments: {});
+ final result = await serverConnection.callTool(request);
+ expect(result.content, hasLength(1));
+ expect(result.content.first, isA<ResourceLink>());
+ final resourceLink = result.content.first as ResourceLink;
+ expect(resourceLink.name, 'foo');
+ expect(resourceLink.description, 'a description');
+ expect(resourceLink.uri, 'https://google.com');
+ expect(resourceLink.mimeType, 'text/html');
+ });
+
+ test('can return structured content', () async {
+ final environment = TestEnvironment(
+ TestMCPClient(),
+ (channel) => TestMCPServerWithTools(
+ channel,
+ tools: [
+ Tool(
+ name: 'foo',
+ inputSchema: ObjectSchema(),
+ outputSchema: ObjectSchema(properties: {'bar': StringSchema()}),
+ ),
+ ],
+ toolHandlers: {
+ 'foo': (request) {
+ return CallToolResult(
+ content: [],
+ structuredContent: {'bar': 'baz'},
+ );
+ },
+ },
+ ),
+ );
+ final serverConnection = environment.serverConnection;
+ await serverConnection.initialize(
+ InitializeRequest(
+ protocolVersion: ProtocolVersion.latestSupported,
+ capabilities: environment.client.capabilities,
+ clientInfo: environment.client.implementation,
+ ),
+ );
+ final request = CallToolRequest(name: 'foo', arguments: {});
+ final result = await serverConnection.callTool(request);
+ expect(result.structuredContent, {'bar': 'baz'});
+ });
+ });
+}
+
+base class TestMCPServerWithTools extends TestMCPServer with ToolsSupport {
+ final List<Tool> _initialTools;
+ final Map<String, FutureOr<CallToolResult> Function(CallToolRequest)>
+ _initialToolHandlers;
+
+ TestMCPServerWithTools(
+ super.channel, {
+ List<Tool> tools = const [],
+ Map<String, FutureOr<CallToolResult> Function(CallToolRequest)>
+ toolHandlers =
+ const {},
+ }) : _initialTools = tools,
+ _initialToolHandlers = toolHandlers;
+
+ @override
+ FutureOr<InitializeResult> initialize(InitializeRequest request) async {
+ final result = await super.initialize(request);
+ for (final tool in _initialTools) {
+ final handler = _initialToolHandlers[tool.name];
+ if (handler != null) {
+ registerTool(tool, handler);
+ } else {
+ throw StateError('No handler provided for tool: ${tool.name}');
+ }
+ }
+ return result;
+ }
}
diff --git a/pkgs/dart_mcp/test/client_and_server_test.dart b/pkgs/dart_mcp/test/client_and_server_test.dart
index 3aa3faa..f2cbb8a 100644
--- a/pkgs/dart_mcp/test/client_and_server_test.dart
+++ b/pkgs/dart_mcp/test/client_and_server_test.dart
@@ -261,6 +261,19 @@
});
group('version negotiation', () {
+ test('client and server respect negotiated protocol version', () async {
+ final environment = TestEnvironment(TestMCPClient(), TestMCPServer.new);
+ final serverConnection = environment.serverConnection;
+ final initializeResult = await serverConnection.initialize(
+ InitializeRequest(
+ protocolVersion: ProtocolVersion.oldestSupported,
+ capabilities: environment.client.capabilities,
+ clientInfo: environment.client.implementation,
+ ),
+ );
+ expect(initializeResult.protocolVersion, ProtocolVersion.oldestSupported);
+ expect(serverConnection.protocolVersion, ProtocolVersion.oldestSupported);
+ });
test('server can downgrade the version', () async {
final environment = TestEnvironment(
TestMCPClient(),
diff --git a/pkgs/dart_mcp_server/pubspec.yaml b/pkgs/dart_mcp_server/pubspec.yaml
index b78cbf7..4df5932 100644
--- a/pkgs/dart_mcp_server/pubspec.yaml
+++ b/pkgs/dart_mcp_server/pubspec.yaml
@@ -3,7 +3,6 @@
An MCP server for Dart projects, exposing various developer tools to AI
models.
publish_to: none
-
environment:
sdk: ^3.7.0
@@ -25,8 +24,7 @@
language_server_protocol:
git:
url: https://github.com/dart-lang/sdk.git
- path:
- third_party/pkg/language_server_protocol
+ path: third_party/pkg/language_server_protocol
meta: ^1.16.0
path: ^1.9.1
pool: ^1.5.1