blob: b9d7945aaeb8b70fcf44db28e9be0480a23310fd [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.
part of 'server.dart';
typedef ReadResourceHandler =
FutureOr<ReadResourceResult?> Function(ReadResourceRequest);
/// A mixin for MCP servers which support the `resources` capability.
///
/// Servers should add [Resource]s using the [addResource] method, typically
/// inside the [initialize] method or constructor, but they may also be added
/// after initialization if needed.
///
/// Resources can later be removed using [removeResource], or the client can be
/// notified of updates using [updateResource].
///
/// Implements the `subscribe` and `listChanges` capabilities for clients, so
/// they can be notified of changes to resources.
///
/// Any [ResourceTemplate]s, should typically be added in [initialize] method or
/// the constructor using [addResourceTemplate]. There is no notification
/// protocol for templates which are added after a client requests them once, so
/// they should be added eagerly.
///
/// See https://modelcontextprotocol.io/docs/concepts/resources.
base mixin ResourcesSupport on MCPServer {
/// The current resources by URI.
final Map<String, Resource> _resources = {};
/// The current resource implementations by URI.
final Map<String, ReadResourceHandler> _resourceImpls = {};
/// All the resource templates supported by this server, see
/// [addResourceTemplate].
final List<({ResourceTemplate template, ReadResourceHandler handler})>
_resourceTemplates = [];
/// The currently subscribed resource [StreamController]s by URI.
final Map<String, StreamController<ResourceUpdatedNotification>>
_subscribedResources = {};
/// The [StreamController] which controls [ResourceListChangedNotification]s.
final StreamController<void> _resourceListChangedController =
StreamController<void>();
/// At most, updates for the same resource or list of resources will only be
/// sent once per this [Duration].
///
/// Override this getter in subtypes in order to configure the delay.
Duration get resourceUpdateThrottleDelay => const Duration(milliseconds: 500);
/// Invoked by the client as a part of initialization.
///
/// Resources should usually be added in this function using [addResource]
/// when possible.
///
/// If resources are added, updated, or removed after [initialized] completes,
/// then the client will be notified of the changes based on their
/// subscription preferences.
@override
FutureOr<InitializeResult> initialize(InitializeRequest request) async {
registerRequestHandler(ListResourcesRequest.methodName, _listResources);
registerRequestHandler(
ListResourceTemplatesRequest.methodName,
_listResourceTemplates,
);
registerRequestHandler(ReadResourceRequest.methodName, _readResource);
registerRequestHandler(SubscribeRequest.methodName, _subscribeResource);
registerRequestHandler(UnsubscribeRequest.methodName, _unsubscribeResource);
final result = await super.initialize(request);
(result.capabilities.resources ??= Resources())
..listChanged = true
..subscribe = true;
_resourceListChangedController.stream
.throttle(resourceUpdateThrottleDelay, trailing: true)
.listen(
(_) => sendNotification(
ResourceListChangedNotification.methodName,
ResourceListChangedNotification(),
),
);
return result;
}
@override
Future<void> shutdown() async {
await super.shutdown();
await _resourceListChangedController.close();
final subscribed = _subscribedResources.values.toList();
_subscribedResources.clear();
await subscribed.map((c) => c.close()).wait;
}
/// Register [resource] to call [impl] when invoked.
///
/// If this server is already initialized and still connected to a client,
/// then the client will be notified that the resources list has changed.
///
/// Throws a [StateError] if there is already a [Resource] registered with the
/// same name.
void addResource(
Resource resource,
FutureOr<ReadResourceResult> Function(ReadResourceRequest) impl,
) {
if (_resources.containsKey(resource.uri)) {
throw StateError(
'Failed to add resource ${resource.name}, there is already a '
'resource that exists at the URI ${resource.uri}.',
);
}
_resources[resource.uri] = resource;
_resourceImpls[resource.uri] = impl;
if (ready) {
_notifyResourceListChanged();
}
}
/// Adds the [ResourceTemplate] [template] with [handler].
///
/// When reading resources, first regular resources added by [addResource]
/// are prioritized. Then, we call the [handler] for each [template], in the
/// order they were added (using this method), and the first one to return a
/// non-null response wins. This package does not automatically handle
/// matching of templates and handlers must accept URIs in any form.
///
/// Throws a [StateError] if there is already a template registered with the
/// same uri template.
void addResourceTemplate(
ResourceTemplate template,
ReadResourceHandler handler,
) {
if (_resourceTemplates.any(
(t) => t.template.uriTemplate == template.uriTemplate,
)) {
throw StateError(
'Failed to add resource template ${template.name}, there is '
'already a resource template with the same uri pattern '
'${template.uriTemplate}.',
);
}
_resourceTemplates.add((template: template, handler: handler));
}
/// Lists all the [ResourceTemplate]s currently available.
ListResourceTemplatesResult _listResourceTemplates(
ListResourceTemplatesRequest request,
) {
return ListResourceTemplatesResult(
resourceTemplates: [
for (var descriptor in _resourceTemplates) descriptor.template,
],
);
}
/// Notifies the client that [resource] has been updated.
///
/// The implementation of that resource can optionally be updated, otherwise
/// the previous implementation will be used.
///
/// Throws a [StateError] if [resource] does not exist.
void updateResource(
Resource resource, {
FutureOr<ReadResourceResult> Function(ReadResourceRequest)? impl,
}) {
if (!_resources.containsKey(resource.uri)) {
throw StateError(
'Failed to update resource ${resource.name}, there is no resource '
'at the URI ${resource.uri}, you must add it first using addResource.',
);
}
if (impl != null) _resourceImpls[resource.uri] = impl;
_subscribedResources[resource.uri]?.add(
ResourceUpdatedNotification(uri: resource.uri),
);
}
/// Removes a [Resource] by [uri].
///
/// Does not error if the resource hasn't been added yet.
void removeResource(String uri) {
_resources.remove(uri);
_resourceImpls.remove(uri);
if (ready) _notifyResourceListChanged();
}
/// Lists all the resources currently available.
ListResourcesResult _listResources(ListResourcesRequest request) {
return ListResourcesResult(resources: _resources.values.toList());
}
/// Reads the resource at `request.uri`.
///
/// Throws an [ArgumentError] if it does not exist (this gets translated into
/// a generic JSON RPC2 error response).
FutureOr<ReadResourceResult> _readResource(
ReadResourceRequest request,
) async {
final impl = _resourceImpls[request.uri];
if (impl == null) {
// Check if it matches any resource template.
for (var descriptor in _resourceTemplates) {
final response = await descriptor.handler(request);
if (response != null) return response;
}
}
final response = await impl?.call(request);
if (response == null) {
throw ArgumentError.value(request.uri, 'uri', 'Resource not found');
}
return response;
}
/// Subscribes the client to the resource at `request.uri`.
FutureOr<EmptyResult> _subscribeResource(SubscribeRequest request) {
if (!_resources.containsKey(request.uri)) {
throw ArgumentError.value(request.uri, 'uri', 'Resource not found');
}
_subscribedResources.putIfAbsent(
request.uri,
() =>
StreamController<ResourceUpdatedNotification>()
..stream
.throttle(resourceUpdateThrottleDelay, trailing: true)
.listen((notification) {
sendNotification(
ResourceUpdatedNotification.methodName,
notification,
);
}),
);
return EmptyResult();
}
/// Unsubscribes the client to the resource at `request.uri`.
Future<EmptyResult> _unsubscribeResource(UnsubscribeRequest request) async {
await _subscribedResources.remove(request.uri)?.close();
return EmptyResult();
}
/// Called whenever the list of resources changes, it is the job of the client
/// to then ask again for the list of tools.
void _notifyResourceListChanged() => _resourceListChangedController.add(null);
}