blob: 73fb647d11fe196f7f6b64ad07b0b63319881651 [file] [log] [blame]
// 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.
/// Interfaces are based on https://github.com/modelcontextprotocol/specification/blob/main/schema/2024-11-05/schema.json
library;
import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:json_rpc_2/json_rpc_2.dart';
part 'completions.dart';
part 'initialization.dart';
part 'logging.dart';
part 'prompts.dart';
part 'resources.dart';
part 'roots.dart';
part 'sampling.dart';
part 'tools.dart';
/// Enum of the known protocol versions.
enum ProtocolVersion {
v2024_11_05('2024-11-05'),
v2025_03_26('2025-03-26');
const ProtocolVersion(this.versionString);
/// Returns the [ProtocolVersion] based on the [version] string, or `null` if
/// it was not recognized.
static ProtocolVersion? tryParse(String version) =>
values.firstWhereOrNull((v) => v.versionString == version);
/// The oldest version supported by the current API.
static const oldestSupported = ProtocolVersion.v2024_11_05;
/// The most recent version supported by the current API.
static const latestSupported = ProtocolVersion.v2025_03_26;
/// The version string used over the wire to identify this version.
final String versionString;
/// Whether or not this API is compatible with the current version.
///
/// **Note**: There may be extra fields included.
bool get isSupported => this >= oldestSupported && this <= latestSupported;
bool operator <(ProtocolVersion other) => index < other.index;
bool operator <=(ProtocolVersion other) => index <= other.index;
bool operator >(ProtocolVersion other) => index > other.index;
bool operator >=(ProtocolVersion other) => index >= other.index;
}
/// A progress token, used to associate progress notifications with the original
/// request.
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.
extension type Meta.fromMap(Map<String, Object?> _value) {}
/// A "mixin"-like extension type for any extension type that might contain a
/// [ProgressToken] at the key "progressToken".
///
/// Should be "mixed in" by implementing this type from other extension types.
extension type WithProgressToken.fromMap(Map<String, Object?> _value) {
ProgressToken? get progressToken => _value['progressToken'] as ProgressToken?;
}
/// A [Meta] object with a known progress token key.
///
/// Has arbitrary other keys.
extension type MetaWithProgressToken.fromMap(Map<String, Object?> _value)
implements Meta, WithProgressToken {
factory MetaWithProgressToken({ProgressToken? progressToken}) =>
MetaWithProgressToken.fromMap({'progressToken': progressToken});
}
/// Base interface for all request types.
///
/// Should not be constructed directly, and has no public constructor.
extension type Request._fromMap(Map<String, Object?> _value) {
/// If specified, the caller is requesting out-of-band progress notifications
/// for this request (as represented by notifications/progress).
///
/// The value of this parameter is an opaque token that will be attached to
/// any subsequent notifications. The receiver is not obligated to provide
/// these notifications.
MetaWithProgressToken? get meta => _value['_meta'] as MetaWithProgressToken?;
}
/// Base interface for all notifications.
extension type Notification(Map<String, Object?> _value) {
/// This parameter name is reserved by MCP to allow clients and servers to
/// attach additional metadata to their notifications.
Meta? get meta => _value['_meta'] as Meta?;
}
/// Base interface for all responses to requests.
extension type Result._(Map<String, Object?> _value) {
Meta? get meta => _value['_meta'] as Meta?;
}
/// A response that indicates success but carries no data.
extension type EmptyResult.fromMap(Map<String, Object?> _) implements Result {
factory EmptyResult() => EmptyResult.fromMap(const {});
}
/// This notification can be sent by either side to indicate that it is
/// cancelling a previously-issued request.
///
/// The request SHOULD still be in-flight, but due to communication latency, it
/// is always possible that this notification MAY arrive after the request has
/// already finished.
///
/// This notification indicates that the result will be unused, so any
/// associated processing SHOULD cease.
///
/// A client MUST NOT attempt to cancel its `initialize` request.
extension type CancelledNotification.fromMap(Map<String, Object?> _value)
implements Notification {
static const methodName = 'notifications/cancelled';
factory CancelledNotification({
required RequestId requestId,
String? reason,
Meta? meta,
}) {
return CancelledNotification.fromMap({
'requestId': requestId,
if (reason != null) 'reason': reason,
if (meta != null) '_meta': meta,
});
}
/// The ID of the request to cancel.
///
/// This MUST correspond to the ID of a request previously issued in the same
/// direction.
RequestId? get requestId => _value['requestId'] as RequestId?;
/// An optional string describing the reason for the cancellation. This MAY be
/// logged or presented to the user.
String? get reason => _value['reason'] as String?;
}
/// An opaque request ID.
extension type RequestId( /*String|int*/ Parameter _) {}
/// A ping, issued by either the server or the client, to check that the other
/// party is still alive.
///
/// The receiver must promptly respond, or else may be disconnected.
///
/// The request itself has no parameters and should always be just `null`.
extension type PingRequest._(Null _) {
static const methodName = 'ping';
}
/// An out-of-band notification used to inform the receiver of a progress
/// update for a long-running request.
extension type ProgressNotification.fromMap(Map<String, Object?> _value)
implements Notification {
static const methodName = 'notifications/progress';
factory ProgressNotification({
required ProgressToken progressToken,
required num progress,
num? total,
Meta? meta,
String? message,
}) => ProgressNotification.fromMap({
'progressToken': progressToken,
'progress': progress,
if (total != null) 'total': total,
if (meta != null) '_meta': meta,
if (message != null) 'message': message,
});
/// The progress token which was given in the initial request, used to
/// associate this notification with the request that is proceeding.
ProgressToken get progressToken => _value['progressToken'] as ProgressToken;
/// The progress thus far.
///
/// This should increase every time progress is made, even if the total is
/// unknown.
num get progress => _value['progress'] as num;
/// Total number of items to process (or total progress required), if
/// known.
num? get total => _value['total'] as num?;
/// An optional message describing the current progress.
String? get message => _value['message'] as String?;
}
/// A "mixin"-like extension type for any request that contains a [Cursor] at
/// the key "cursor".
///
/// Should be "mixed in" by implementing this type from other extension types.
///
/// This type is not intended to be constructed directly and thus has no public
/// constructor.
extension type PaginatedRequest._fromMap(Map<String, Object?> _value)
implements Request {
/// An opaque token representing the current pagination position.
///
/// If provided, the server should return results starting after this cursor.
Cursor? get cursor => _value['cursor'] as Cursor?;
}
/// A "mixin"-like extension type for any result type that contains a [Cursor]
/// at the key "cursor".
///
/// Should be "mixed in" by implementing this type from other extension types.
///
/// This type is not intended to be constructed directly and thus has no public
/// constructor.
extension type PaginatedResult._fromMap(Map<String, Object?> _value)
implements Result {
Cursor? get nextCursor => _value['nextCursor'] as Cursor?;
}
/// Could be either [TextContent], [ImageContent], [AudioContent] or
/// [EmbeddedResource].
///
/// Use [isText], [isImage] and [isEmbeddedResource] before casting to the more
/// specific types, or switch on the [type] and then cast.
///
/// Doing `is` checks does not work because these are just extension types, they
/// all have the same runtime type (`Map<String, Object?>`).
extension type Content._(Map<String, Object?> _value) {
factory Content.fromMap(Map<String, Object?> value) {
assert(value.containsKey('type'));
return Content._(value);
}
/// Alias for [TextContent.new].
static const text = TextContent.new;
/// Alias for [ImageContent.new].
static const image = ImageContent.new;
/// Alias for [AudioContent.new].
static const audio = AudioContent.new;
/// Alias for [EmbeddedResource.new].
static const embeddedResource = EmbeddedResource.new;
/// Whether or not this is a [TextContent].
bool get isText => _value['type'] == TextContent.expectedType;
/// Whether or not this is an [ImageContent].
bool get isImage => _value['type'] == ImageContent.expectedType;
/// Whether or not this is an [AudioContent].
bool get isAudio => _value['type'] == AudioContent.expectedType;
/// Whether or not this is an [EmbeddedResource].
bool get isEmbeddedResource =>
_value['type'] == EmbeddedResource.expectedType;
/// The type of content.
///
/// You can use this in a switch to handle the various types (see the static
/// `expectedType` getters), or you can use [isText], [isImage], [isAudio] and
/// [isEmbeddedResource] to determine the type and then do the cast.
String get type => _value['type'] as String;
}
/// Text provided to or from an LLM.
extension type TextContent.fromMap(Map<String, Object?> _value)
implements Content, Annotated {
static const expectedType = 'text';
factory TextContent({required String text, Annotations? annotations}) =>
TextContent.fromMap({
'text': text,
'type': expectedType,
if (annotations != null) 'annotations': annotations,
});
String get type {
final type = _value['type'] as String;
assert(type == expectedType);
return type;
}
/// The text content.
String get text => _value['text'] as String;
}
/// An image provided to or from an LLM.
extension type ImageContent.fromMap(Map<String, Object?> _value)
implements Content, Annotated {
static const expectedType = 'image';
factory ImageContent({
required String data,
required String mimeType,
Annotations? annotations,
}) => ImageContent.fromMap({
'data': data,
'mimeType': mimeType,
'type': expectedType,
if (annotations != null) 'annotations': annotations,
});
String get type {
final type = _value['type'] as String;
assert(type == expectedType);
return type;
}
/// The base64 encoded image data.
String get data => _value['data'] as String;
/// The MIME type of the image.
///
/// Different providers may support different image types.
String get mimeType => _value['mimeType'] as String;
}
/// Audio provided to or from an LLM.
///
/// Only supported since version [ProtocolVersion.v2025_03_26].
extension type AudioContent.fromMap(Map<String, Object?> _value)
implements Content, Annotated {
static const expectedType = 'audio';
factory AudioContent({
required String data,
required String mimeType,
Annotations? annotations,
}) => AudioContent.fromMap({
'data': data,
'mimeType': mimeType,
'type': expectedType,
if (annotations != null) 'annotations': annotations,
});
String get type {
final type = _value['type'] as String;
assert(type == expectedType);
return type;
}
/// The base64 encoded audio data.
String get data => _value['data'] as String;
/// The MIME type of the audio.
///
/// Different providers may support different audio types.
String get mimeType => _value['mimeType'] as String;
}
/// The contents of a resource, embedded into a prompt or tool call result.
///
/// 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 {
static const expectedType = 'resource';
factory EmbeddedResource({
required Content resource,
Annotations? annotations,
}) => EmbeddedResource.fromMap({
'resource': resource,
'type': expectedType,
if (annotations != null) 'annotations': annotations,
});
String get type {
final type = _value['resource'] as String;
assert(type == expectedType);
return type;
}
/// Either [TextResourceContents] or [BlobResourceContents].
ResourceContents get resource => _value['resource'] as ResourceContents;
String? get mimeType => _value['mimeType'] as String?;
}
/// Base type for objects that include optional annotations for the client.
///
/// The client can use annotations to inform how objects are used or displayed.
extension type Annotated._fromMap(Map<String, Object?> _value) {
/// Annotations for this object.
Annotations? get annotations => _value['annotations'] as Annotations?;
}
/// The annotations for an [Annotated] object.
extension type Annotations.fromMap(Map<String, Object?> _value) {
factory Annotations({List<Role>? audience, double? priority}) {
assert(priority == null || (priority >= 0 && priority <= 1));
return Annotations.fromMap({
if (audience != null) 'audience': [for (var role in audience) role.name],
if (priority != null) 'priority': priority,
});
}
/// Describes who the intended customer of this object or data is.
///
/// It can include multiple entries to indicate content useful for
/// multiple audiences (e.g., `[Role.user, Role.assistant]`).
List<Role>? get audience {
final audience = _value['audience'] as List?;
if (audience == null) return null;
return [
for (var role in audience) Role.values.firstWhere((e) => e.name == role),
];
}
/// Describes how important this data is for operating the server.
///
/// A value of 1 means "most important," and indicates that the data is
/// effectively required, while 0 means "least important," and indicates
/// that the data is entirely optional.
///
/// Must be between 0 and 1.
double? get priority => _value['priority'] as double?;
}