blob: 6521b86f85cb22508b13d4ad5dae73030607cae9 [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';
/// A mixin for MCP servers which support the `tools` capability.
///
/// Servers should register tools using the [registerTool] method, typically
/// inside the [initialize] method, but they may also be registered after
/// initialization if needed.
///
/// See https://modelcontextprotocol.io/docs/concepts/tools.
base mixin ToolsSupport on MCPServer {
/// The registered tools by name.
final Map<String, Tool> _registeredTools = {};
/// The registered tool implementations by name.
final Map<String, FutureOr<CallToolResult> Function(CallToolRequest)>
_registeredToolImpls = {};
/// Invoked by the client as a part of initialization.
///
/// Tools should usually be registered in this function using [registerTool]
/// when possible.
///
/// If tools are registered after [initialized] completes, then the server
/// will notify the client
@override
FutureOr<InitializeResult> initialize(InitializeRequest request) async {
registerRequestHandler(ListToolsRequest.methodName, _listTools);
registerRequestHandler(CallToolRequest.methodName, _callTool);
final result = await super.initialize(request);
(result.capabilities.tools ??= Tools()).listChanged = true;
return result;
}
/// Register [tool] 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 tools list has changed.
///
/// Throws a [StateError] if there is already a [Tool] registered with the
/// same name.
///
/// If [validateArguments] is true, then request arguments are automatically
/// validated against the [tool]s input schema.
void registerTool(
Tool tool,
FutureOr<CallToolResult> Function(CallToolRequest) impl, {
bool validateArguments = true,
}) {
if (_registeredTools.containsKey(tool.name)) {
throw StateError(
'Failed to register tool ${tool.name}, it is already registered',
);
}
_registeredTools[tool.name] = tool;
_registeredToolImpls[tool.name] =
validateArguments
? (request) {
final errors = tool.inputSchema.validate(
request.arguments ?? const <String, Object?>{},
);
if (errors.isNotEmpty) {
return CallToolResult(
content: [
for (final error in errors)
Content.text(text: error.toErrorString()),
],
isError: true,
);
}
return impl(request);
}
: impl;
if (ready) {
_notifyToolListChanged();
}
}
/// Un-registers a [Tool] by [name].
///
/// Does not error if the tool hasn't been registered yet.
void unregisterTool(String name) {
_registeredTools.remove(name);
_registeredToolImpls.remove(name);
if (ready) {
_notifyToolListChanged();
}
}
/// Returns the list of supported tools for this server.
ListToolsResult _listTools([ListToolsRequest? request]) =>
ListToolsResult(tools: [for (var tool in _registeredTools.values) tool]);
/// Invoked when one of the registered tools is called.
Future<CallToolResult> _callTool(CallToolRequest request) async {
final impl = _registeredToolImpls[request.name];
if (impl == null) {
return CallToolResult(
isError: true,
content: [
TextContent(text: 'No tool registered with the name ${request.name}'),
],
);
}
try {
return await impl(request);
} catch (e, s) {
return CallToolResult(
isError: true,
content: [TextContent(text: '$e\n$s')],
);
}
}
/// Called whenever the list of tools changes, it is the job of the client to
/// then ask again for the list of tools.
void _notifyToolListChanged() => sendNotification(
ToolListChangedNotification.methodName,
ToolListChangedNotification(),
);
}