From 6567c2dc97a94adf308a792773f9045541d1dcbf Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Wed, 16 Jul 2025 17:14:04 +0000 Subject: [PATCH 1/3] add elicitation example, fix some issues with the elicitation APIs --- pkgs/dart_mcp/CHANGELOG.md | 3 + .../dart_mcp/example/elicitations_client.dart | 128 ++++++++++++++++++ .../dart_mcp/example/elicitations_server.dart | 69 ++++++++++ pkgs/dart_mcp/example/tools_client.dart | 4 +- pkgs/dart_mcp/lib/src/api/elicitation.dart | 12 +- pkgs/dart_mcp/lib/src/api/initialization.dart | 4 + 6 files changed, 212 insertions(+), 8 deletions(-) create mode 100644 pkgs/dart_mcp/example/elicitations_client.dart create mode 100644 pkgs/dart_mcp/example/elicitations_server.dart 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..e417851d --- /dev/null +++ b/pkgs/dart_mcp/example/elicitations_client.dart @@ -0,0 +1,128 @@ +// 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(', ')})' + : ''; + stdout.write('$name$allowedValues: '); + final value = stdin.readLineSync()!; + arguments[name] = switch (type) { + JsonType.string || JsonType.enumeration => value, + JsonType.num => num.parse(value), + JsonType.int => int.parse(value), + JsonType.bool => bool.parse(value), + JsonType.object || + JsonType.list || + JsonType.nil || + null => throw StateError('Unsupported field type $type'), + }; + } + 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..0be22fb1 --- /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 with tools support', + version: '0.1.0', + ), + instructions: 'Just list and call the tools :D', + ) { + // 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({}); } From 588060e775f57c713432887b89bf1583c8b8afa1 Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Wed, 16 Jul 2025 17:31:30 +0000 Subject: [PATCH 2/3] handle errors, validate the schema --- .../dart_mcp/example/elicitations_client.dart | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/pkgs/dart_mcp/example/elicitations_client.dart b/pkgs/dart_mcp/example/elicitations_client.dart index e417851d..19780831 100644 --- a/pkgs/dart_mcp/example/elicitations_client.dart +++ b/pkgs/dart_mcp/example/elicitations_client.dart @@ -91,19 +91,42 @@ Do you want to accept (a), reject (r), or cancel (c) the elicitation? type == JsonType.enumeration ? ' (${(property.value as EnumSchema).values.join(', ')})' : ''; - stdout.write('$name$allowedValues: '); - final value = stdin.readLineSync()!; - arguments[name] = switch (type) { - JsonType.string || JsonType.enumeration => value, - JsonType.num => num.parse(value), - JsonType.int => int.parse(value), - JsonType.bool => bool.parse(value), - JsonType.object || - JsonType.list || - JsonType.nil || - null => throw StateError('Unsupported field type $type'), - }; + // 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); } From 5f574b18e24746bdab7168af04e77d7cdca4134c Mon Sep 17 00:00:00 2001 From: Jake Macdonald Date: Wed, 16 Jul 2025 18:14:46 +0000 Subject: [PATCH 3/3] update server description --- pkgs/dart_mcp/example/elicitations_server.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/dart_mcp/example/elicitations_server.dart b/pkgs/dart_mcp/example/elicitations_server.dart index 0be22fb1..477a0790 100644 --- a/pkgs/dart_mcp/example/elicitations_server.dart +++ b/pkgs/dart_mcp/example/elicitations_server.dart @@ -23,10 +23,10 @@ base class MCPServerWithElicitation extends MCPServer MCPServerWithElicitation(super.channel) : super.fromStreamChannel( implementation: Implementation( - name: 'An example dart server with tools support', + name: 'An example dart server which makes elicitations', version: '0.1.0', ), - instructions: 'Just list and call the tools :D', + 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.