Skip to content

Commit df0c4f1

Browse files
authored
add elicitation example, fix some issues with the elicitation APIs (#229)
More work towards #220
1 parent 61ba1ea commit df0c4f1

File tree

6 files changed

+235
-8
lines changed

6 files changed

+235
-8
lines changed

pkgs/dart_mcp/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
- Add new `package:dart_mcp/stdio.dart` library with a `stdioChannel` utility
77
for creating a stream channel that separates messages by newlines.
88
- Added more examples.
9+
- Change the `schema` parameter for elicitation requests to an `ObjectSchema` to
10+
match the spec.
11+
- Deprecate the `Elicitations` server capability, this doesn't exist in the spec.
912

1013
## 0.3.0
1114

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
/// A client that connects to a server and supports elicitation requests.
6+
library;
7+
8+
import 'dart:async';
9+
import 'dart:io';
10+
11+
import 'package:dart_mcp/client.dart';
12+
import 'package:dart_mcp/stdio.dart';
13+
import 'package:stream_channel/stream_channel.dart';
14+
15+
void main() async {
16+
// Create a client, which is the top level object that manages all
17+
// server connections.
18+
final client = TestMCPClientWithElicitationSupport(
19+
Implementation(name: 'example dart client', version: '0.1.0'),
20+
);
21+
print('connecting to server');
22+
23+
// Start the server as a separate process.
24+
final process = await Process.start('dart', [
25+
'run',
26+
'example/elicitations_server.dart',
27+
]);
28+
// Connect the client to the server.
29+
final server = client.connectServer(
30+
stdioChannel(input: process.stdout, output: process.stdin),
31+
);
32+
// When the server connection is closed, kill the process.
33+
unawaited(server.done.then((_) => process.kill()));
34+
35+
print('server started');
36+
37+
// Initialize the server and let it know our capabilities.
38+
print('initializing server');
39+
final initializeResult = await server.initialize(
40+
InitializeRequest(
41+
protocolVersion: ProtocolVersion.latestSupported,
42+
capabilities: client.capabilities,
43+
clientInfo: client.implementation,
44+
),
45+
);
46+
print('initialized: $initializeResult');
47+
48+
// Notify the server that we are initialized.
49+
server.notifyInitialized();
50+
print('sent initialized notification');
51+
52+
print('waiting for elicitation requests');
53+
}
54+
55+
/// A client that supports elicitation requests using the [ElicitationSupport]
56+
/// mixin.
57+
///
58+
/// Prompts the user for values on stdin.
59+
final class TestMCPClientWithElicitationSupport extends MCPClient
60+
with ElicitationSupport {
61+
TestMCPClientWithElicitationSupport(super.implementation);
62+
63+
@override
64+
/// Handle the actual elicitation from the server by reading from stdin.
65+
FutureOr<ElicitResult> handleElicitation(ElicitRequest request) {
66+
// Ask the user if they are willing to provide the information first.
67+
print('''
68+
Elicitation received from server: ${request.message}
69+
70+
Do you want to accept (a), reject (r), or cancel (c) the elicitation?
71+
''');
72+
final answer = stdin.readLineSync();
73+
final action = switch (answer) {
74+
'a' => ElicitationAction.accept,
75+
'r' => ElicitationAction.reject,
76+
'c' => ElicitationAction.cancel,
77+
_ => throw ArgumentError('Invalid answer: $answer'),
78+
};
79+
80+
// If they don't accept it, just return the reason.
81+
if (action != ElicitationAction.accept) {
82+
return ElicitResult(action: action);
83+
}
84+
85+
// User has accepted the elicitation, prompt them for each value.
86+
final arguments = <String, Object?>{};
87+
for (final property in request.requestedSchema.properties!.entries) {
88+
final name = property.key;
89+
final type = property.value.type;
90+
final allowedValues =
91+
type == JsonType.enumeration
92+
? ' (${(property.value as EnumSchema).values.join(', ')})'
93+
: '';
94+
// Ask the user in a loop until the value provided matches the schema,
95+
// at which point we will `break` from the loop.
96+
while (true) {
97+
stdout.write('$name$allowedValues: ');
98+
final userValue = stdin.readLineSync()!;
99+
try {
100+
// Convert the value to the correct type.
101+
final convertedValue = switch (type) {
102+
JsonType.string || JsonType.enumeration => userValue,
103+
JsonType.num => num.parse(userValue),
104+
JsonType.int => int.parse(userValue),
105+
JsonType.bool => bool.parse(userValue),
106+
JsonType.object ||
107+
JsonType.list ||
108+
JsonType.nil ||
109+
null => throw StateError('Unsupported field type $type'),
110+
};
111+
// Actually validate the value based on the schema.
112+
final errors = property.value.validate(convertedValue);
113+
if (errors.isEmpty) {
114+
// No errors, we can assign the value and exit the loop.
115+
arguments[name] = convertedValue;
116+
break;
117+
} else {
118+
print('Invalid value, got the following errors:');
119+
for (final error in errors) {
120+
print(' - $error');
121+
}
122+
}
123+
} catch (e) {
124+
// Handles parse errors etc.
125+
print('Invalid value, got the following errors:\n - $e');
126+
}
127+
}
128+
}
129+
// Return the final result with the arguments.
130+
return ElicitResult(action: ElicitationAction.accept, content: arguments);
131+
}
132+
133+
/// Whenever connecting to a server, we also listen for log messages.
134+
///
135+
/// The server we connect to will log the elicitation responses it receives.
136+
@override
137+
ServerConnection connectServer(
138+
StreamChannel<String> channel, {
139+
Sink<String>? protocolLogSink,
140+
}) {
141+
final connection = super.connectServer(
142+
channel,
143+
protocolLogSink: protocolLogSink,
144+
);
145+
// Whenever a log message is received, print it to the console.
146+
connection.onLog.listen((message) {
147+
print('[${message.level}]: ${message.data}');
148+
});
149+
return connection;
150+
}
151+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
/// A server that makes an elicitation request to the client using the
6+
/// [ElicitationRequestSupport] mixin.
7+
library;
8+
9+
import 'dart:io' as io;
10+
11+
import 'package:dart_mcp/server.dart';
12+
import 'package:dart_mcp/stdio.dart';
13+
14+
void main() {
15+
// Create the server and connect it to stdio.
16+
MCPServerWithElicitation(stdioChannel(input: io.stdin, output: io.stdout));
17+
}
18+
19+
/// This server uses the [ElicitationRequestSupport] mixin to make elicitation
20+
/// requests to the client.
21+
base class MCPServerWithElicitation extends MCPServer
22+
with LoggingSupport, ElicitationRequestSupport {
23+
MCPServerWithElicitation(super.channel)
24+
: super.fromStreamChannel(
25+
implementation: Implementation(
26+
name: 'An example dart server which makes elicitations',
27+
version: '0.1.0',
28+
),
29+
instructions: 'Handle the elicitations and ask the user for the values',
30+
) {
31+
// You must wait for initialization to complete before you can make an
32+
// elicitation request.
33+
initialized.then((_) => _elicitName());
34+
}
35+
36+
/// Elicits a name from the user, and logs a message based on the response.
37+
void _elicitName() async {
38+
final response = await elicit(
39+
ElicitRequest(
40+
message: 'I would like to ask you some personal information.',
41+
requestedSchema: Schema.object(
42+
properties: {
43+
'name': Schema.string(),
44+
'age': Schema.int(),
45+
'gender': Schema.enumeration(values: ['male', 'female', 'other']),
46+
},
47+
),
48+
),
49+
);
50+
switch (response.action) {
51+
case ElicitationAction.accept:
52+
final {'age': int age, 'name': String name, 'gender': String gender} =
53+
(response.content as Map<String, dynamic>);
54+
log(
55+
LoggingLevel.warning,
56+
'Hello $name! I see that you are $age years '
57+
'old and identify as $gender',
58+
);
59+
case ElicitationAction.reject:
60+
log(LoggingLevel.warning, 'Request for name was rejected');
61+
case ElicitationAction.cancel:
62+
log(LoggingLevel.warning, 'Request for name was cancelled');
63+
}
64+
65+
// Ask again after a second.
66+
await Future<void>.delayed(const Duration(seconds: 1));
67+
_elicitName();
68+
}
69+
}

pkgs/dart_mcp/example/tools_client.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5-
// A client that connects to a server and exercises the tools API.
5+
/// A client that connects to a server and exercises the tools API.
6+
library;
7+
68
import 'dart:async';
79
import 'dart:io';
810

pkgs/dart_mcp/lib/src/api/elicitation.dart

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ extension type ElicitRequest._fromMap(Map<String, Object?> _value)
1111

1212
factory ElicitRequest({
1313
required String message,
14-
required Schema requestedSchema,
14+
required ObjectSchema requestedSchema,
1515
}) {
1616
assert(
1717
validateRequestedSchema(requestedSchema),
@@ -39,8 +39,8 @@ extension type ElicitRequest._fromMap(Map<String, Object?> _value)
3939
///
4040
/// You can use [validateRequestedSchema] to validate that a schema conforms
4141
/// to these limitations.
42-
Schema get requestedSchema {
43-
final requestedSchema = _value['requestedSchema'] as Schema?;
42+
ObjectSchema get requestedSchema {
43+
final requestedSchema = _value['requestedSchema'] as ObjectSchema?;
4444
if (requestedSchema == null) {
4545
throw ArgumentError(
4646
'Missing required requestedSchema field in $ElicitRequest',
@@ -53,14 +53,12 @@ extension type ElicitRequest._fromMap(Map<String, Object?> _value)
5353
/// limitations of the spec.
5454
///
5555
/// See also: [requestedSchema] for a description of the spec limitations.
56-
static bool validateRequestedSchema(Schema schema) {
56+
static bool validateRequestedSchema(ObjectSchema schema) {
5757
if (schema.type != JsonType.object) {
5858
return false;
5959
}
6060

61-
final objectSchema = schema as ObjectSchema;
62-
final properties = objectSchema.properties;
63-
61+
final properties = schema.properties;
6462
if (properties == null) {
6563
return true; // No properties to validate.
6664
}

pkgs/dart_mcp/lib/src/api/initialization.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ extension type ServerCapabilities.fromMap(Map<String, Object?> _value) {
194194
Prompts? prompts,
195195
Resources? resources,
196196
Tools? tools,
197+
@Deprecated('Do not use, only clients have this capability')
197198
Elicitation? elicitation,
198199
}) => ServerCapabilities.fromMap({
199200
if (experimental != null) 'experimental': experimental,
@@ -261,9 +262,11 @@ extension type ServerCapabilities.fromMap(Map<String, Object?> _value) {
261262
}
262263

263264
/// Present if the server supports elicitation.
265+
@Deprecated('Do not use, only clients have this capability')
264266
Elicitation? get elicitation => _value['elicitation'] as Elicitation?;
265267

266268
/// Sets [elicitation] if it is null, otherwise asserts.
269+
@Deprecated('Do not use, only clients have this capability')
267270
set elicitation(Elicitation? value) {
268271
assert(elicitation == null);
269272
_value['elicitation'] = value;
@@ -333,6 +336,7 @@ extension type Tools.fromMap(Map<String, Object?> _value) {
333336
}
334337

335338
/// Elicitation parameter for [ServerCapabilities].
339+
@Deprecated('Do not use, only clients have this capability')
336340
extension type Elicitation.fromMap(Map<String, Object?> _value) {
337341
factory Elicitation() => Elicitation.fromMap({});
338342
}

0 commit comments

Comments
 (0)