diff --git a/pkgs/dart_mcp/CHANGELOG.md b/pkgs/dart_mcp/CHANGELOG.md index 52517b7e..84fb23eb 100644 --- a/pkgs/dart_mcp/CHANGELOG.md +++ b/pkgs/dart_mcp/CHANGELOG.md @@ -6,6 +6,9 @@ - Add new `package:dart_mcp/stdio.dart` library with a `stdioChannel` utility for creating a stream channel that separates messages by newlines. - Added more examples. +- Change the `schema` parameter for elicitation requests to an `ObjectSchema` to + match the spec. +- Deprecate the `Elicitations` server capability, this doesn't exist in the spec. ## 0.3.0 diff --git a/pkgs/dart_mcp/example/elicitations_client.dart b/pkgs/dart_mcp/example/elicitations_client.dart new file mode 100644 index 00000000..19780831 --- /dev/null +++ b/pkgs/dart_mcp/example/elicitations_client.dart @@ -0,0 +1,151 @@ +// 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. + +/// A client that connects to a server and supports elicitation requests. +library; + +import 'dart:async'; +import 'dart:io'; + +import 'package:dart_mcp/client.dart'; +import 'package:dart_mcp/stdio.dart'; +import 'package:stream_channel/stream_channel.dart'; + +void main() async { + // Create a client, which is the top level object that manages all + // server connections. + final client = TestMCPClientWithElicitationSupport( + Implementation(name: 'example dart client', version: '0.1.0'), + ); + print('connecting to server'); + + // Start the server as a separate process. + final process = await Process.start('dart', [ + 'run', + 'example/elicitations_server.dart', + ]); + // Connect the client to the server. + final server = client.connectServer( + stdioChannel(input: process.stdout, output: process.stdin), + ); + // When the server connection is closed, kill the process. + unawaited(server.done.then((_) => process.kill())); + + print('server started'); + + // Initialize the server and let it know our capabilities. + print('initializing server'); + final initializeResult = await server.initialize( + InitializeRequest( + protocolVersion: ProtocolVersion.latestSupported, + capabilities: client.capabilities, + clientInfo: client.implementation, + ), + ); + print('initialized: $initializeResult'); + + // Notify the server that we are initialized. + server.notifyInitialized(); + print('sent initialized notification'); + + print('waiting for elicitation requests'); +} + +/// A client that supports elicitation requests using the [ElicitationSupport] +/// mixin. +/// +/// Prompts the user for values on stdin. +final class TestMCPClientWithElicitationSupport extends MCPClient + with ElicitationSupport { + TestMCPClientWithElicitationSupport(super.implementation); + + @override + /// Handle the actual elicitation from the server by reading from stdin. + FutureOr handleElicitation(ElicitRequest request) { + // Ask the user if they are willing to provide the information first. + print(''' +Elicitation received from server: ${request.message} + +Do you want to accept (a), reject (r), or cancel (c) the elicitation? +'''); + final answer = stdin.readLineSync(); + final action = switch (answer) { + 'a' => ElicitationAction.accept, + 'r' => ElicitationAction.reject, + 'c' => ElicitationAction.cancel, + _ => throw ArgumentError('Invalid answer: $answer'), + }; + + // If they don't accept it, just return the reason. + if (action != ElicitationAction.accept) { + return ElicitResult(action: action); + } + + // User has accepted the elicitation, prompt them for each value. + final arguments = {}; + for (final property in request.requestedSchema.properties!.entries) { + final name = property.key; + final type = property.value.type; + final allowedValues = + type == JsonType.enumeration + ? ' (${(property.value as EnumSchema).values.join(', ')})' + : ''; + // Ask the user in a loop until the value provided matches the schema, + // at which point we will `break` from the loop. + while (true) { + stdout.write('$name$allowedValues: '); + final userValue = stdin.readLineSync()!; + try { + // Convert the value to the correct type. + final convertedValue = switch (type) { + JsonType.string || JsonType.enumeration => userValue, + JsonType.num => num.parse(userValue), + JsonType.int => int.parse(userValue), + JsonType.bool => bool.parse(userValue), + JsonType.object || + JsonType.list || + JsonType.nil || + null => throw StateError('Unsupported field type $type'), + }; + // Actually validate the value based on the schema. + final errors = property.value.validate(convertedValue); + if (errors.isEmpty) { + // No errors, we can assign the value and exit the loop. + arguments[name] = convertedValue; + break; + } else { + print('Invalid value, got the following errors:'); + for (final error in errors) { + print(' - $error'); + } + } + } catch (e) { + // Handles parse errors etc. + print('Invalid value, got the following errors:\n - $e'); + } + } + } + // Return the final result with the arguments. + return ElicitResult(action: ElicitationAction.accept, content: arguments); + } + + /// Whenever connecting to a server, we also listen for log messages. + /// + /// The server we connect to will log the elicitation responses it receives. + @override + ServerConnection connectServer( + StreamChannel channel, { + Sink? protocolLogSink, + }) { + final connection = super.connectServer( + channel, + protocolLogSink: protocolLogSink, + ); + // Whenever a log message is received, print it to the console. + connection.onLog.listen((message) { + print('[${message.level}]: ${message.data}'); + }); + return connection; + } +} diff --git a/pkgs/dart_mcp/example/elicitations_server.dart b/pkgs/dart_mcp/example/elicitations_server.dart new file mode 100644 index 00000000..477a0790 --- /dev/null +++ b/pkgs/dart_mcp/example/elicitations_server.dart @@ -0,0 +1,69 @@ +// 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. + +/// A server that makes an elicitation request to the client using the +/// [ElicitationRequestSupport] mixin. +library; + +import 'dart:io' as io; + +import 'package:dart_mcp/server.dart'; +import 'package:dart_mcp/stdio.dart'; + +void main() { + // Create the server and connect it to stdio. + MCPServerWithElicitation(stdioChannel(input: io.stdin, output: io.stdout)); +} + +/// This server uses the [ElicitationRequestSupport] mixin to make elicitation +/// requests to the client. +base class MCPServerWithElicitation extends MCPServer + with LoggingSupport, ElicitationRequestSupport { + MCPServerWithElicitation(super.channel) + : super.fromStreamChannel( + implementation: Implementation( + name: 'An example dart server which makes elicitations', + version: '0.1.0', + ), + instructions: 'Handle the elicitations and ask the user for the values', + ) { + // You must wait for initialization to complete before you can make an + // elicitation request. + initialized.then((_) => _elicitName()); + } + + /// Elicits a name from the user, and logs a message based on the response. + void _elicitName() async { + final response = await elicit( + ElicitRequest( + message: 'I would like to ask you some personal information.', + requestedSchema: Schema.object( + properties: { + 'name': Schema.string(), + 'age': Schema.int(), + 'gender': Schema.enumeration(values: ['male', 'female', 'other']), + }, + ), + ), + ); + switch (response.action) { + case ElicitationAction.accept: + final {'age': int age, 'name': String name, 'gender': String gender} = + (response.content as Map); + log( + LoggingLevel.warning, + 'Hello $name! I see that you are $age years ' + 'old and identify as $gender', + ); + case ElicitationAction.reject: + log(LoggingLevel.warning, 'Request for name was rejected'); + case ElicitationAction.cancel: + log(LoggingLevel.warning, 'Request for name was cancelled'); + } + + // Ask again after a second. + await Future.delayed(const Duration(seconds: 1)); + _elicitName(); + } +} diff --git a/pkgs/dart_mcp/example/tools_client.dart b/pkgs/dart_mcp/example/tools_client.dart index 79469887..fec4418c 100644 --- a/pkgs/dart_mcp/example/tools_client.dart +++ b/pkgs/dart_mcp/example/tools_client.dart @@ -2,7 +2,9 @@ // 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. -// A client that connects to a server and exercises the tools API. +/// A client that connects to a server and exercises the tools API. +library; + import 'dart:async'; import 'dart:io'; diff --git a/pkgs/dart_mcp/lib/src/api/elicitation.dart b/pkgs/dart_mcp/lib/src/api/elicitation.dart index edc83ee2..38abc759 100644 --- a/pkgs/dart_mcp/lib/src/api/elicitation.dart +++ b/pkgs/dart_mcp/lib/src/api/elicitation.dart @@ -11,7 +11,7 @@ extension type ElicitRequest._fromMap(Map _value) factory ElicitRequest({ required String message, - required Schema requestedSchema, + required ObjectSchema requestedSchema, }) { assert( validateRequestedSchema(requestedSchema), @@ -39,8 +39,8 @@ extension type ElicitRequest._fromMap(Map _value) /// /// You can use [validateRequestedSchema] to validate that a schema conforms /// to these limitations. - Schema get requestedSchema { - final requestedSchema = _value['requestedSchema'] as Schema?; + ObjectSchema get requestedSchema { + final requestedSchema = _value['requestedSchema'] as ObjectSchema?; if (requestedSchema == null) { throw ArgumentError( 'Missing required requestedSchema field in $ElicitRequest', @@ -53,14 +53,12 @@ extension type ElicitRequest._fromMap(Map _value) /// limitations of the spec. /// /// See also: [requestedSchema] for a description of the spec limitations. - static bool validateRequestedSchema(Schema schema) { + static bool validateRequestedSchema(ObjectSchema schema) { if (schema.type != JsonType.object) { return false; } - final objectSchema = schema as ObjectSchema; - final properties = objectSchema.properties; - + final properties = schema.properties; if (properties == null) { return true; // No properties to validate. } diff --git a/pkgs/dart_mcp/lib/src/api/initialization.dart b/pkgs/dart_mcp/lib/src/api/initialization.dart index 85bb2a7b..14e01d02 100644 --- a/pkgs/dart_mcp/lib/src/api/initialization.dart +++ b/pkgs/dart_mcp/lib/src/api/initialization.dart @@ -194,6 +194,7 @@ extension type ServerCapabilities.fromMap(Map _value) { Prompts? prompts, Resources? resources, Tools? tools, + @Deprecated('Do not use, only clients have this capability') Elicitation? elicitation, }) => ServerCapabilities.fromMap({ if (experimental != null) 'experimental': experimental, @@ -261,9 +262,11 @@ extension type ServerCapabilities.fromMap(Map _value) { } /// Present if the server supports elicitation. + @Deprecated('Do not use, only clients have this capability') Elicitation? get elicitation => _value['elicitation'] as Elicitation?; /// Sets [elicitation] if it is null, otherwise asserts. + @Deprecated('Do not use, only clients have this capability') set elicitation(Elicitation? value) { assert(elicitation == null); _value['elicitation'] = value; @@ -333,6 +336,7 @@ extension type Tools.fromMap(Map _value) { } /// Elicitation parameter for [ServerCapabilities]. +@Deprecated('Do not use, only clients have this capability') extension type Elicitation.fromMap(Map _value) { factory Elicitation() => Elicitation.fromMap({}); }