blob: cdc53c257906dc65bdb64bf6f196821d6de7b03a [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';
/// Mix this in to any MCPServer to add support tracking the [Root]s as given
/// by the client in an opinionated way.
///
/// Listens to change events and updates the set of [roots].
base mixin RootsTrackingSupport on LoggingSupport {
/// All known workspace [Root]s from the last call to [listRoots].
///
/// May be a [Future] if we are currently requesting the roots.
FutureOr<List<Root>> get roots => switch (_rootsState) {
_RootsState.upToDate => _roots!,
_RootsState.pending => _rootsCompleter!.future,
};
/// The current state of [roots], whether it is up to date or waiting on
/// updated values.
_RootsState _rootsState = _RootsState.pending;
/// The list of [roots] if [_rootsState] is [_RootsState.upToDate],
/// otherwise `null`.
List<Root>? _roots;
/// Completer for any pending [listRoots] call if [_rootsState] is
/// [_RootsState.pending], otherwise `null`.
Completer<List<Root>>? _rootsCompleter = Completer();
/// Whether or not the connected client supports [listRoots].
///
/// Only safe to call after calling [initialize] on `super` since this is
/// based on the client capabilities.
bool get supportsRoots => clientCapabilities.roots != null;
/// Whether or not the connected client supports reporting changes to the
/// list of roots.
///
/// Only safe to call after calling [initialize] on `super` since this is
/// based on the client capabilities.
bool get supportsRootsChanged =>
clientCapabilities.roots?.listChanged == true;
@override
FutureOr<InitializeResult> initialize(InitializeRequest request) {
initialized.then((_) async {
if (!supportsRoots) {
log(
LoggingLevel.warning,
'Client does not support the roots capability, some functionality '
'may be disabled.',
);
} else {
if (supportsRootsChanged) {
rootsListChanged!.listen((event) {
updateRoots();
});
}
await updateRoots();
}
});
return super.initialize(request);
}
/// Updates the list of [roots] by calling [listRoots].
///
/// If the current [_rootsCompleter] was not yet completed, then we wait to
/// complete it until we get an updated list of roots, so that we don't get
/// stale results from [listRoots] requests that are still in flight during
/// a change notification.
@mustCallSuper
Future<void> updateRoots() async {
_rootsState = _RootsState.pending;
final previousCompleter = _rootsCompleter;
// Always create a new completer so we can handle race conditions by
// checking completer identity.
final newCompleter = _rootsCompleter = Completer();
_roots = null;
if (previousCompleter != null) {
// Complete previously scheduled completers with our completers value.
previousCompleter.complete(newCompleter.future);
}
ListRootsResult? result;
try {
result = await listRoots(ListRootsRequest());
} on RpcException catch (e) {
log(LoggingLevel.error, 'Error calling listRoots: $e');
} finally {
// Only complete the completer if it's still the one we created. Otherwise
// we wait for the next result to come back and throw away this result.
if (_rootsCompleter == newCompleter) {
final roots = result == null ? <Root>[] : result.roots;
newCompleter.complete(roots);
_roots = roots;
_rootsCompleter = null;
_rootsState = _RootsState.upToDate;
}
}
}
}
/// The current state of the roots information.
enum _RootsState {
/// No change notification since our last update.
upToDate,
/// Waiting for a `listRoots` response.
pending,
}