blob: 580befe0381e89c5e9ebcc8cd00ab12fb1f93c08 [file]
// 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 the schema files under
/// https://github.com/modelcontextprotocol/modelcontextprotocol/tree/main/schema,
/// one directory per revision. See DEVELOPING.md.
library;
import 'dart:collection';
import 'package:collection/collection.dart';
import '../utils/constants.dart';
part 'completions.dart';
part 'elicitation.dart';
part 'error_codes.dart';
part 'icons.dart';
part 'initialization.dart';
part 'input_required.dart';
part 'logging.dart';
part 'prompts.dart';
part 'resources.dart';
part 'roots.dart';
part 'sampling.dart';
part 'subscriptions.dart';
part 'tools.dart';
/// Enum of the known protocol versions.
enum ProtocolVersion {
v2024_11_05(
'2024-11-05',
supportsStreamableHttp: false,
addedMethods: {
CompleteRequest.methodName,
InitializeRequest.methodName,
SetLevelRequest.methodName,
CancelledNotification.methodName,
InitializedNotification.methodName,
LoggingMessageNotification.methodName,
ProgressNotification.methodName,
PromptListChangedNotification.methodName,
ResourceListChangedNotification.methodName,
ResourceUpdatedNotification.methodName,
RootsListChangedNotification.methodName,
ToolListChangedNotification.methodName,
PingRequest.methodName,
GetPromptRequest.methodName,
ListPromptsRequest.methodName,
ListResourcesRequest.methodName,
ReadResourceRequest.methodName,
SubscribeRequest.methodName,
ListResourceTemplatesRequest.methodName,
UnsubscribeRequest.methodName,
ListRootsRequest.methodName,
CreateMessageRequest.methodName,
CallToolRequest.methodName,
ListToolsRequest.methodName,
},
),
v2025_03_26('2025-03-26', supportsStreamableHttp: false),
v2025_06_18(
'2025-06-18',
supportsStreamableHttp: false,
addedMethods: {ElicitRequest.methodName},
),
v2025_11_25(
'2025-11-25',
supportsStreamableHttp: false,
addedMethods: {
ElicitationCompleteNotification.methodName,
'notifications/tasks/status',
'tasks/cancel',
'tasks/get',
'tasks/list',
'tasks/result',
},
),
v2026_07_28(
'2026-07-28',
supportsStreamableHttp: true,
addedMethods: {
SubscriptionsAcknowledgedNotification.methodName,
DiscoverRequest.methodName,
SubscriptionsListenRequest.methodName,
},
removedMethods: {
ElicitRequest.methodName,
InitializeRequest.methodName,
SetLevelRequest.methodName,
ElicitationCompleteNotification.methodName,
InitializedNotification.methodName,
RootsListChangedNotification.methodName,
'notifications/tasks/status',
PingRequest.methodName,
SubscribeRequest.methodName,
UnsubscribeRequest.methodName,
ListRootsRequest.methodName,
CreateMessageRequest.methodName,
'tasks/cancel',
'tasks/get',
'tasks/list',
'tasks/result',
},
);
const ProtocolVersion(
this.versionString, {
required this.supportsStreamableHttp,
this.addedMethods = const {},
this.removedMethods = const {},
});
/// Whether this package's Streamable HTTP transport supports this revision.
final bool supportsStreamableHttp;
/// The methods this revision introduced.
final Set<String> addedMethods;
/// The methods this revision stopped accepting.
///
/// A method belongs here even when the revision kept its type, as long as it
/// took away the way to send it. 2026-07-28 keeps [ElicitRequest],
/// [ListRootsRequest] and [CreateMessageRequest] for the [InputRequest] an
/// [InputRequiredResult] carries, and none of the three can be sent on its
/// own there.
final Set<String> removedMethods;
/// Whether [method] is one this revision accepts.
///
/// Walks back from this revision to the first one, so a method holds until
/// some later revision lists it in [removedMethods]. A method no revision
/// ever added is not part of any of them.
bool methodIsValid(String method) {
for (var version = this; ; version = values[version.index - 1]) {
if (version.removedMethods.contains(method)) return false;
if (version.addedMethods.contains(method)) return true;
if (version.index == 0) return false;
}
}
/// 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.
///
/// This is the newest version the legacy `initialize` handshake negotiates.
/// A transport for a request-scoped protocol revision, such as
/// `handleStreamableHttpRequest` in `package:dart_mcp/streamable_http.dart`,
/// carries its own set of supported versions.
static const latestSupported = ProtocolVersion.v2025_11_25;
/// 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.
///
/// 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 (`-`). From the 2025-11-25 revision, implementations SHOULD
/// use reverse DNS notation (e.g., `com.example/` rather than
/// `example.com/`), and any prefix whose second label is
/// `modelcontextprotocol` or `mcp` is reserved for MCP use. For example:
/// `io.modelcontextprotocol/`, `dev.mcp/`, `org.modelcontextprotocol.api/`,
/// and `com.mcp.tools/` are all reserved, while `com.example.mcp/` is not,
/// because its second label is `example`. The 2025-06-18 revision reserved
/// any prefix carrying `modelcontextprotocol` or `mcp` in a label other
/// than the last.
/// - Name: Unless empty, MUST begin and end with an alphanumeric character
/// (`[a-z0-9A-Z]`). MAY contain hyphens (`-`), underscores (`_`), dots
/// (`.`), and alphanumerics in between.
///
/// From the 2026-07-28 revision, `traceparent`, `tracestate`, and `baggage`
/// are reserved for OpenTelemetry trace context, as an exception to the prefix
/// rule. When present, their values MUST follow the W3C Trace Context and
/// W3C Baggage formats respectively.
extension type Meta.fromMap(Map<String, Object?> _value) {
Object? operator [](String key) => _value[key];
}
/// Basic metadata required by multiple types.
///
/// Not to be confused with the `_meta` property in the spec, which has a
/// different purpose.
extension type BaseMetadata.fromMap(Map<String, Object?> _value) {
factory BaseMetadata({required String name, String? title}) =>
BaseMetadata.fromMap({
Keys.name: name,
if (title != null) Keys.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[Keys.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[Keys.title] as String?;
}
/// 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[Keys.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({
if (progressToken != null) Keys.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[Keys.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)
implements WithMetadata {
/// 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[Keys.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[Keys.meta] as Meta?;
}
/// Base interface for all responses to requests.
extension type Result._(Map<String, Object?> _value) {
Meta? get meta => _value[Keys.meta] as Meta?;
/// The type of this result, which determines how to parse it.
///
/// Known types are "complete" and "input_required", but a server may send
/// any other string. Servers on protocol versions before 2026-07-28 omit
/// the field; the schema requires treating that as "complete", so an absent
/// value is reported here as "complete".
String get resultType =>
_value[Keys.resultType] as String? ?? ResultTypes.complete;
/// Whether the server asked for more input before it can answer, which makes
/// this an [InputRequiredResult].
///
/// An `is` check cannot tell that apart, because these are just extension
/// types and they share the runtime type `Map<String, Object?>`.
bool get isInputRequired => resultType == ResultTypes.inputRequired;
}
/// 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({
Keys.requestId: requestId,
if (reason != null) Keys.reason: reason,
if (meta != null) Keys.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[Keys.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[Keys.reason] as String?;
}
/// An opaque request ID.
extension type RequestId(/*String|int*/ Object _) {}
/// 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.
extension type PingRequest._(Map<String, Object?> _) implements Request {
static const methodName = 'ping';
factory PingRequest({MetaWithProgressToken? meta}) =>
PingRequest._({if (meta != null) Keys.meta: meta});
}
/// 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({
Keys.progressToken: progressToken,
Keys.progress: progress,
if (total != null) Keys.total: total,
if (meta != null) Keys.meta: meta,
if (message != null) Keys.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[Keys.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[Keys.progress] as num;
/// Total number of items to process (or total progress required), if
/// known.
num? get total => _value[Keys.total] as num?;
/// An optional message describing the current progress.
String? get message => _value[Keys.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[Keys.cursor] as Cursor?;
/// A copy of this request that asks for the page at [cursor].
///
/// Copying keeps every entry the caller put on the request, including ones
/// this package does not read.
PaginatedRequest copyWithCursor(Cursor cursor) =>
PaginatedRequest._fromMap({..._value, Keys.cursor: 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[Keys.nextCursor] as Cursor?;
}
/// A "mixin"-like extension type for any result type that contains caching
/// hints at the keys "ttlMs" and "cacheScope".
///
/// 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 CacheableResult._fromMap(Map<String, Object?> _value)
implements Result {
/// A hint for how long, in milliseconds, the client may cache this response
/// before re-fetching, analogous to HTTP `Cache-Control` max-age.
///
/// A value of 0 marks the response as immediately stale.
///
/// Servers on protocol versions before 2026-07-28 omit the field; the spec
/// says clients should assume 0 in that case, and should treat a negative
/// value from a non-conforming server as 0, so both are reported here as 0.
int get ttlMs {
final value = _value[Keys.ttlMs] as int? ?? 0;
return value < 0 ? 0 : value;
}
/// The intended scope of the cached response, analogous to HTTP
/// `Cache-Control: public` vs `Cache-Control: private`.
///
/// An absent or unrecognized value is reported here as `null` rather than
/// as a default. The schema comment says the field defaults to "public",
/// but the same SEP argues the field is required precisely "because there
/// is no safe default for older servers", and leaves a default to the
/// discretion of each SDK. Reporting `null` follows the second reading:
/// synthesizing "public" for a server that never declared a scope is the
/// one guess that can leak a private response into a shared cache. Servers
/// on protocol versions before 2026-07-28 omit the field, and their results
/// also carry no `ttlMs`, so they are already immediately stale.
CacheScope? get cacheScope {
final name = _value[Keys.cacheScope] as String?;
if (name == null) return null;
return CacheScope.values.firstWhereOrNull((scope) => scope.name == name);
}
}
/// The intended scope of a cached [CacheableResult] response.
enum CacheScope {
/// The response contains no user-specific data; any client or intermediary
/// may cache it and serve it across authorization contexts.
public,
/// The response may be cached and reused only within the same authorization
/// context; caches must not be shared across authorization contexts.
private,
}
/// 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(Keys.type));
assert(
value[Keys.type] != ToolUseContent.expectedType &&
value[Keys.type] != ToolResultContent.expectedType,
'Sampling tool content cannot be read as Content.',
);
// Tool use and tool result belong to sampling messages, where they read
// as a `SamplingMessageContentBlock`. A `tools/call` result carrying one
// is a mix-up the static types already refuse, and a map arriving here
// skips those types, so the assert covers that path.
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[Keys.type] == TextContent.expectedType;
/// Whether or not this is an [ImageContent].
bool get isImage => _value[Keys.type] == ImageContent.expectedType;
/// Whether or not this is an [AudioContent].
bool get isAudio => _value[Keys.type] == AudioContent.expectedType;
/// Whether or not this is an [EmbeddedResource].
bool get isEmbeddedResource =>
_value[Keys.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[Keys.type] as String;
}
/// Text provided to or from an LLM.
extension type TextContent.fromMap(Map<String, Object?> _value)
implements Content, SamplingMessageContentBlock, Annotated, WithMetadata {
static const expectedType = 'text';
factory TextContent({
required String text,
Annotations? annotations,
Meta? meta,
}) => TextContent.fromMap({
Keys.text: text,
Keys.type: expectedType,
if (annotations != null) Keys.annotations: annotations,
if (meta != null) Keys.meta: meta,
});
String get type {
final type = _value[Keys.type] as String;
assert(type == expectedType);
return type;
}
/// The text content.
String get text => _value[Keys.text] as String;
}
/// An image provided to or from an LLM.
extension type ImageContent.fromMap(Map<String, Object?> _value)
implements Content, SamplingMessageContentBlock, Annotated, WithMetadata {
static const expectedType = 'image';
factory ImageContent({
required String data,
required String mimeType,
Annotations? annotations,
Meta? meta,
}) => ImageContent.fromMap({
Keys.data: data,
Keys.mimeType: mimeType,
Keys.type: expectedType,
if (annotations != null) Keys.annotations: annotations,
if (meta != null) Keys.meta: meta,
});
String get type {
final type = _value[Keys.type] as String;
assert(type == expectedType);
return type;
}
/// The base64 encoded image data.
String get data => _value[Keys.data] as String;
/// The MIME type of the image.
///
/// Different providers may support different image types.
String get mimeType => _value[Keys.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, SamplingMessageContentBlock, Annotated, WithMetadata {
static const expectedType = 'audio';
factory AudioContent({
required String data,
required String mimeType,
Annotations? annotations,
Meta? meta,
}) => AudioContent.fromMap({
Keys.data: data,
Keys.mimeType: mimeType,
Keys.type: expectedType,
if (annotations != null) Keys.annotations: annotations,
if (meta != null) Keys.meta: meta,
});
String get type {
final type = _value[Keys.type] as String;
assert(type == expectedType);
return type;
}
/// The base64 encoded audio data.
String get data => _value[Keys.data] as String;
/// The MIME type of the audio.
///
/// Different providers may support different audio types.
String get mimeType => _value[Keys.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, WithMetadata {
static const expectedType = 'resource';
factory EmbeddedResource({
required ResourceContents resource,
Annotations? annotations,
Meta? meta,
}) => EmbeddedResource.fromMap({
Keys.resource: resource,
Keys.type: expectedType,
if (annotations != null) Keys.annotations: annotations,
if (meta != null) Keys.meta: meta,
});
String get type {
final type = _value[Keys.type] as String;
assert(type == expectedType);
return type;
}
/// Either [TextResourceContents] or [BlobResourceContents].
ResourceContents get resource => _value[Keys.resource] as ResourceContents;
@Deprecated('Use `.resource.mimeType`.')
String? get mimeType => _value[Keys.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,
String? description,
required String uri,
String? mimeType,
List<Icon>? icons,
Annotations? annotations,
Meta? meta,
}) => ResourceLink.fromMap({
Keys.name: name,
if (title != null) Keys.title: title,
if (description != null) Keys.description: description,
Keys.uri: uri,
if (mimeType != null) Keys.mimeType: mimeType,
if (icons != null) Keys.icons: icons,
Keys.type: expectedType,
if (annotations != null) Keys.annotations: annotations,
if (meta != null) Keys.meta: meta,
});
String get type {
final type = _value[Keys.type] as String;
assert(type == expectedType);
return type;
}
/// The description of the resource.
String? get description => _value[Keys.description] as String?;
/// The URI of the resource.
String get uri {
final uri = _value[Keys.uri] as String?;
if (uri == null) {
throw ArgumentError('Missing uri field in $ResourceLink.');
}
return uri;
}
/// The MIME type of the resource.
String? get mimeType => _value[Keys.mimeType] as String?;
/// The size of the resource in bytes.
int? get size => _value[Keys.size] as int?;
/// Optional set of sized icons that the client can display in a user
/// interface.
List<Icon>? get icons => (_value[Keys.icons] as List?)?.cast<Icon>();
}
/// 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[Keys.annotations] as Annotations?;
}
/// The annotations for an [Annotated] object.
extension type Annotations.fromMap(Map<String, Object?> _value) {
factory Annotations({
List<Role>? audience,
DateTime? lastModified,
double? priority,
}) {
assert(priority == null || (priority >= 0 && priority <= 1));
return Annotations.fromMap({
if (audience != null)
Keys.audience: [for (var role in audience) role.name],
if (lastModified != null)
Keys.lastModified: lastModified.toIso8601String(),
if (priority != null) Keys.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[Keys.audience] as List?;
if (audience == null) return null;
return [
for (var role in audience)
Role.values.firstWhere((value) => value.name == role),
];
}
/// 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[Keys.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
/// 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[Keys.priority] as num?)?.toDouble();
}