diff --git a/pkgs/dart_mcp/CHANGELOG.md b/pkgs/dart_mcp/CHANGELOG.md index 08eb159c..700381a2 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 73fb647d..2ead41da 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:collection/collection.dart'; 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 @@ part 'tools.dart'; /// 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 @@ enum ProtocolVersion { 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 @@ extension type ProgressToken( /*String|int*/ Object _) {} /// 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 _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 _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 @@ extension type MetaWithProgressToken.fromMap(Map _value) 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 _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 _value) { +extension type Request._fromMap(Map _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 @@ extension type Content._(Map _value) { /// Text provided to or from an LLM. extension type TextContent.fromMap(Map _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 @@ extension type TextContent.fromMap(Map _value) /// An image provided to or from an LLM. extension type ImageContent.fromMap(Map _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 @@ extension type ImageContent.fromMap(Map _value) /// /// Only supported since version [ProtocolVersion.v2025_03_26]. extension type AudioContent.fromMap(Map _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 @@ extension type AudioContent.fromMap(Map _value) /// 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 _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 @@ extension type EmbeddedResource.fromMap(Map _value) 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 _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 @@ extension type Annotated._fromMap(Map _value) { /// The annotations for an [Annotated] object. extension type Annotations.fromMap(Map _value) { - factory Annotations({List? audience, double? priority}) { + factory Annotations({ + List? 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 @@ extension type Annotations.fromMap(Map _value) { ]; } + /// 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 c5072f93..1d8b9295 100644 --- a/pkgs/dart_mcp/lib/src/api/completions.dart +++ b/pkgs/dart_mcp/lib/src/api/completions.dart @@ -14,18 +14,20 @@ extension type CompleteRequest.fromMap(Map _value) 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 @@ extension type CompleteRequest.fromMap(Map _value) } 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 @@ extension type CompletionArgument.fromMap(Map _value) { 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 _value) { + factory CompletionContext({Map? arguments}) => + CompletionContext.fromMap({'arguments': arguments}); + + /// Previously-resolved variables in a URI template or prompt. + Map? get arguments => + (_value['arguments'] as Map?)?.cast(); +} + +/// Union type for references, see [PromptReference] and +/// [ResourceTemplateReference]. extension type Reference._(Map _value) { factory Reference.fromMap(Map value) { assert(value.containsKey('type')); @@ -115,8 +131,9 @@ extension type Reference._(Map _value) { /// 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 @@ extension type Reference._(Map _value) { } /// A reference to a resource or resource template definition. -extension type ResourceReference.fromMap(Map _value) +extension type ResourceTemplateReference.fromMap(Map _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 @@ extension type ResourceReference.fromMap(Map _value) String get uri => _value['uri'] as String; } +@Deprecated('Use ResourceTemplateReference instead') +typedef ResourceReference = ResourceTemplateReference; + /// Identifies a prompt. extension type PromptReference.fromMap(Map _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 @@ extension type PromptReference.fromMap(Map _value) 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 00000000..edc83ee2 --- /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 _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 _value) + implements Result { + factory ElicitResult({ + required ElicitationAction action, + Map? 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? get content => + _value['content'] as Map?; +} + +/// 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 80da37c4..81d7b6be 100644 --- a/pkgs/dart_mcp/lib/src/api/initialization.dart +++ b/pkgs/dart_mcp/lib/src/api/initialization.dart @@ -113,10 +113,12 @@ extension type ClientCapabilities.fromMap(Map _value) { Map? experimental, RootsCapabilities? roots, Map? 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 @@ extension type ClientCapabilities.fromMap(Map _value) { 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 @@ extension type RootsCapabilities.fromMap(Map _value) { } } +/// Whether the client supports elicitation. +extension type ElicitationCapability.fromMap(Map _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 @@ extension type ServerCapabilities.fromMap(Map _value) { 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 @@ extension type ServerCapabilities.fromMap(Map _value) { 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 @@ extension type Tools.fromMap(Map _value) { } } +/// Elicitation parameter for [ServerCapabilities]. +extension type Elicitation.fromMap(Map _value) { + factory Elicitation() => Elicitation.fromMap({}); +} + /// Describes the name and version of an MCP implementation. -extension type Implementation.fromMap(Map _value) { - factory Implementation({required String name, required String version}) => - Implementation.fromMap({'name': name, 'version': version}); +extension type Implementation.fromMap(Map _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 name => _value['name'] as String; - String get version => _value['version'] as String; + 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 a658725b..86fa48e2 100644 --- a/pkgs/dart_mcp/lib/src/api/prompts.dart +++ b/pkgs/dart_mcp/lib/src/api/prompts.dart @@ -86,7 +86,8 @@ extension type GetPromptResult.fromMap(Map _value) } /// A prompt or prompt template that the server offers. -extension type Prompt.fromMap(Map _value) { +extension type Prompt.fromMap(Map _value) + implements BaseMetadata { factory Prompt({ required String name, String? description, @@ -97,9 +98,6 @@ extension type Prompt.fromMap(Map _value) { 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 @@ extension type Prompt.fromMap(Map _value) { } /// Describes an argument that a prompt can accept. -extension type PromptArgument.fromMap(Map _value) { +extension type PromptArgument.fromMap(Map _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 5b6da075..799502d2 100644 --- a/pkgs/dart_mcp/lib/src/api/resources.dart +++ b/pkgs/dart_mcp/lib/src/api/resources.dart @@ -191,7 +191,7 @@ extension type ResourceUpdatedNotification.fromMap(Map _value) /// A known resource that the server is capable of reading. extension type Resource.fromMap(Map _value) - implements Annotated { + implements Annotated, BaseMetadata, WithMetadata { factory Resource({ required String uri, required String name, @@ -199,6 +199,7 @@ extension type Resource.fromMap(Map _value) String? description, String? mimeType, int? size, + Meta? meta, }) => Resource.fromMap({ 'uri': uri, 'name': name, @@ -206,16 +207,12 @@ extension type Resource.fromMap(Map _value) 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 @@ extension type Resource.fromMap(Map _value) /// A template description for resources available on the server. extension type ResourceTemplate.fromMap(Map _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 @@ extension type ResourceTemplate.fromMap(Map _value) /// /// Could be either [TextResourceContents] or [BlobResourceContents], /// use [isText] and [isBlob] before casting to the more specific type. -extension type ResourceContents.fromMap(Map _value) { +extension type ResourceContents.fromMap(Map _value) + implements WithMetadata { /// Whether or not this represents [TextResourceContents]. bool get isText => _value.containsKey('text'); @@ -297,10 +294,12 @@ extension type TextResourceContents.fromMap(Map _value) 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 @@ extension type BlobResourceContents.fromMap(Map _value) 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 f7194a36..94139e60 100644 --- a/pkgs/dart_mcp/lib/src/api/roots.dart +++ b/pkgs/dart_mcp/lib/src/api/roots.dart @@ -43,15 +43,26 @@ extension type ListRootsResult.fromMap(Map _value) } /// Represents a root directory or file that the server can operate on. -extension type Root.fromMap(Map _value) { - factory Root({required String uri, String? name}) => - Root.fromMap({'uri': uri, if (name != null) 'name': name}); +extension type Root.fromMap(Map _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 @@ extension type Root.fromMap(Map _value) { /// 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 _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 407566d3..8ea339b5 100644 --- a/pkgs/dart_mcp/lib/src/api/tools.dart +++ b/pkgs/dart_mcp/lib/src/api/tools.dart @@ -53,15 +53,17 @@ extension type CallToolResult.fromMap(Map _value) factory CallToolResult({ Meta? meta, required List content, + Map? 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 get content { final content = (_value['content'] as List?)?.cast(); if (content == null) { @@ -70,6 +72,11 @@ extension type CallToolResult.fromMap(Map _value) return content; } + /// The content as structured output, if the [Tool] declared an + /// `outputSchema`. + Map? get structuredContent => + _value['structuredContent'] as Map?; + /// 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 @@ extension type ToolListChangedNotification.fromMap(Map _value) } /// Definition for a tool the client can call. -extension type Tool.fromMap(Map _value) { +extension type Tool.fromMap(Map _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 @@ extension type Tool.fromMap(Map _value) { (_value['annotations'] as Map?)?.cast() 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 @@ extension type Tool.fromMap(Map _value) { } 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 cde9930d..1b1a919c 100644 --- a/pkgs/dart_mcp/lib/src/client/client.dart +++ b/pkgs/dart_mcp/lib/src/client/client.dart @@ -15,6 +15,7 @@ import 'package:stream_channel/stream_channel.dart'; import '../api/api.dart'; import '../shared.dart'; +part 'elicitation_support.dart'; part 'roots_support.dart'; part 'sampling_support.dart'; @@ -104,6 +105,7 @@ base class MCPClient { 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 @@ base class MCPClient { /// 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 @@ base class ServerConnection extends MCPBase { super.protocolLogSink, RootsSupport? rootsSupport, SamplingSupport? samplingSupport, + ElicitationSupport? elicitationSupport, }) { if (rootsSupport != null) { registerRequestHandler( @@ -217,6 +220,12 @@ base class ServerConnection extends MCPBase { ); } + if (elicitationSupport != null) { + registerRequestHandler(ElicitRequest.methodName, (ElicitRequest request) { + return elicitationSupport.handleElicitation(request); + }); + } + registerNotificationHandler( PromptListChangedNotification.methodName, _promptListChangedController.sink.add, @@ -277,8 +286,10 @@ base class ServerConnection extends MCPBase { 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 00000000..b847c3d0 --- /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 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 00000000..6bdf0e18 --- /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 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 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 a0ab0e0a..43260fd6 100644 --- a/pkgs/dart_mcp/lib/src/server/server.dart +++ b/pkgs/dart_mcp/lib/src/server/server.dart @@ -12,6 +12,7 @@ import '../api/api.dart'; 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 faddb6b6..e26a2370 100644 --- a/pkgs/dart_mcp/test/api/api_test.dart +++ b/pkgs/dart_mcp/test/api/api_test.dart @@ -61,6 +61,16 @@ void main() { group('API object validation', () { test('throws when required fields are missing', () { + expect(() => Root.fromMap({}).uri, throwsA(isA())); + expect( + () => Implementation.fromMap({'name': 'test'}).version, + throwsA(isA()), + ); + expect( + () => BaseMetadata.fromMap({}).name, + throwsA(isA()), + ); + final empty = {}; // Initialization @@ -104,5 +114,14 @@ void main() { 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 04c542c5..7a9730fa 100644 --- a/pkgs/dart_mcp/test/api/completions_test.dart +++ b/pkgs/dart_mcp/test/api/completions_test.dart @@ -66,6 +66,27 @@ void main() { 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 @@ final class TestMCPServerWithCompletions extends TestMCPServer 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 @@ final class TestMCPServerWithCompletions extends TestMCPServer ); 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 00000000..fa539405 --- /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(); + 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 Function(ElicitRequest request) elicitationHandler; + + @override + FutureOr 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 21bfb744..20f9a0d3 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 @@ void main() { // 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 @@ void main() { ); }); }); + + 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()); + 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()); + 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 _initialTools; + final Map Function(CallToolRequest)> + _initialToolHandlers; + + TestMCPServerWithTools( + super.channel, { + List tools = const [], + Map Function(CallToolRequest)> + toolHandlers = + const {}, + }) : _initialTools = tools, + _initialToolHandlers = toolHandlers; + + @override + FutureOr 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 3aa3faa5..f2cbb8a7 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 @@ void main() { }); 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 b78cbf73..4df5932e 100644 --- a/pkgs/dart_mcp_server/pubspec.yaml +++ b/pkgs/dart_mcp_server/pubspec.yaml @@ -3,7 +3,6 @@ description: >- 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 @@ dependencies: 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