Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pkgs/dart_mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
128 changes: 128 additions & 0 deletions pkgs/dart_mcp/example/elicitations_client.dart
Original file line number Diff line number Diff line change
@@ -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<ElicitResult> 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 = <String, Object?>{};
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<String> channel, {
Sink<String>? 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;
}
}
69 changes: 69 additions & 0 deletions pkgs/dart_mcp/example/elicitations_server.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic>);
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<void>.delayed(const Duration(seconds: 1));
_elicitName();
}
}
4 changes: 3 additions & 1 deletion pkgs/dart_mcp/example/tools_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
12 changes: 5 additions & 7 deletions pkgs/dart_mcp/lib/src/api/elicitation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ extension type ElicitRequest._fromMap(Map<String, Object?> _value)

factory ElicitRequest({
required String message,
required Schema requestedSchema,
required ObjectSchema requestedSchema,
}) {
assert(
validateRequestedSchema(requestedSchema),
Expand Down Expand Up @@ -39,8 +39,8 @@ extension type ElicitRequest._fromMap(Map<String, Object?> _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',
Expand All @@ -53,14 +53,12 @@ extension type ElicitRequest._fromMap(Map<String, Object?> _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.
}
Expand Down
4 changes: 4 additions & 0 deletions pkgs/dart_mcp/lib/src/api/initialization.dart
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ extension type ServerCapabilities.fromMap(Map<String, Object?> _value) {
Prompts? prompts,
Resources? resources,
Tools? tools,
@Deprecated('Do not use, only clients have this capability')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops. I thought the server had that in its schema to indicate that it might send elicitations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The capabilities are only there for the client/server indicating they can handle certain requests, not whether or not they will issue them.

I think you could argue it might be useful to know what kind of requests a server would like to make, but that isn't how its structured 🤷‍♂️

Elicitation? elicitation,
}) => ServerCapabilities.fromMap({
if (experimental != null) 'experimental': experimental,
Expand Down Expand Up @@ -261,9 +262,11 @@ extension type ServerCapabilities.fromMap(Map<String, Object?> _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;
Expand Down Expand Up @@ -333,6 +336,7 @@ extension type Tools.fromMap(Map<String, Object?> _value) {
}

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