blob: a47f1b9a433c0174625ecca5e0928a67a249dbb5 [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.
import 'dart:async';
import 'dart:collection';
import 'package:dart_mcp/server.dart';
import 'package:meta/meta.dart';
import '../utils/constants.dart';
/// Adds a fallback mode for roots when they aren't supported.
///
/// Overrides [listRoots] to return the manually added roots through
/// an MCP tool command.
///
/// Overrides [rootsListChanged] to return a custom stream of events based
/// on the tool calls.
base mixin RootsFallbackSupport on ToolsSupport, RootsTrackingSupport {
/// Set of custom roots
final Set<Root> _customRoots = HashSet<Root>(
equals: (a, b) => a.uri == b.uri,
hashCode: (root) => root.uri.hashCode,
);
/// Whether or not to force the fallback mode for roots, regardless of the
/// client's reported support.
///
/// Override this to enable it.
bool get forceRootsFallback => false;
/// Whether fallback mode is enabled.
///
/// Unsafe to call until after the server is initialized.
bool get _fallbackEnabled => forceRootsFallback || !super.supportsRoots;
/// Always supported, either by the client or this mixin.
@override
bool get supportsRoots => true;
@override
bool get supportsRootsChanged =>
// If the client supports roots, then we only support root change events
// if they do. If we are implementing the support, we always support it.
_fallbackEnabled ? true : super.supportsRootsChanged;
@override
Stream<RootsListChangedNotification>? get rootsListChanged =>
// If the client supports roots, just use their stream (or lack thereof).
// If they don't, use our own stream.
_fallbackEnabled
? _rootsListChangedFallbackController?.stream
: super.rootsListChanged;
StreamController<RootsListChangedNotification>?
_rootsListChangedFallbackController;
@override
FutureOr<InitializeResult> initialize(InitializeRequest request) async {
try {
return super.initialize(request);
} finally {
// Can't call `super.supportsRoots` until after `super.initialize`.
if (_fallbackEnabled) {
registerTool(removeRootsTool, _removeRoots);
registerTool(addRootsTool, _addRoots);
_rootsListChangedFallbackController =
StreamController<RootsListChangedNotification>.broadcast();
}
}
}
/// Delegates to the inherited implementation if fallback mode is not enabled,
/// otherwise returns our own custom roots.
@override
Future<ListRootsResult> listRoots(ListRootsRequest request) async =>
_fallbackEnabled
? ListRootsResult(roots: _customRoots.toList())
: super.listRoots(request);
/// Adds the roots in [request] the custom roots and calls [updateRoots].
///
/// Should only be called if [_fallbackEnabled] is `true`.
Future<CallToolResult> _addRoots(CallToolRequest request) async {
if (!_fallbackEnabled) {
throw StateError(
'This tool should not be invoked if the client supports roots',
);
}
(request.arguments![ParameterNames.roots] as List).cast<Root>().forEach(
_customRoots.add,
);
_rootsListChangedFallbackController?.add(RootsListChangedNotification());
return success;
}
/// Removes the roots in [request] from the custom roots and calls
/// [updateRoots].
///
/// Should only be called if [_fallbackEnabled] is true.
Future<CallToolResult> _removeRoots(CallToolRequest request) async {
if (!_fallbackEnabled) {
throw StateError(
'This tool should not be invoked if the client supports roots',
);
}
final roots = (request.arguments![ParameterNames.uris] as List)
.cast<String>()
.map((uri) => Root(uri: uri));
_customRoots.removeAll(roots);
_rootsListChangedFallbackController?.add(RootsListChangedNotification());
return success;
}
@override
Future<void> shutdown() async {
await super.shutdown();
await _rootsListChangedFallbackController?.close();
}
@visibleForTesting
static final addRootsTool = Tool(
name: 'add_roots',
description:
'Adds one or more project roots. Tools are only allowed to run under '
'these roots, so you must call this function before passing any roots '
'to any other tools.',
annotations: ToolAnnotations(title: 'Add roots', readOnlyHint: false),
inputSchema: Schema.object(
properties: {
ParameterNames.roots: Schema.list(
description: 'All the project roots to add to this server.',
items: Schema.object(
properties: {
ParameterNames.uri: Schema.string(
description: 'The URI of the root.',
),
ParameterNames.name: Schema.string(
description: 'An optional name of the root.',
),
},
required: [ParameterNames.uri],
),
),
},
),
);
@visibleForTesting
static final removeRootsTool = Tool(
name: 'remove_roots',
description:
'Removes one or more project roots previously added via '
'the add_roots tool.',
annotations: ToolAnnotations(title: 'Remove roots', readOnlyHint: false),
inputSchema: Schema.object(
properties: {
ParameterNames.uris: Schema.list(
description: 'All the project roots to remove from this server.',
items: Schema.string(description: 'The URIs of the roots to remove.'),
),
},
),
);
}