blob: 12d02245a297dbd6093efabd3b14d21e15555caf [file] [edit]
// 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:async';
import 'dart:convert';
import 'dart:io' as io;
import 'dart:io';
import 'dart:math' show Random;
import 'package:dart_mcp/server.dart';
import 'package:dart_mcp/stdio.dart';
import 'package:json_rpc_2/json_rpc_2.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, ToolsSupport {
/// Whether or not we got approval to run the `needs_permission` tool..
bool approved = false;
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',
);
@override
FutureOr<InitializeResult> initialize(InitializeRequest request) {
registerTool(
Tool(
name: 'needs_permission',
description: 'A tool that requires permission to run',
inputSchema: Schema.object(),
),
_handleNeedsPermissionTool,
);
return super.initialize(request);
}
Future<CallToolResult> _handleNeedsPermissionTool(
CallToolRequest request,
) async {
if (!approved) {
final elicitationId = Random.secure().nextInt(999999).toString();
// Start a simple web server on a random port.
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
try {
server.listen((HttpRequest request) async {
switch (request.method) {
case 'GET':
request.response
..statusCode = 200
..headers.contentType = ContentType.html
..write(_permissionWebPage);
break;
case 'POST':
// Read the permission radio button value from the form.
final formContents =
Uri(
query:
await request
.cast<List<int>>()
.transform(const Utf8Decoder())
.join(),
).queryParameters;
approved = formContents['permission'] == 'yes';
notifyElicitationComplete(
ElicitationCompleteNotification(elicitationId: elicitationId),
);
await server.close();
break;
default:
request.response.statusCode = HttpStatus.methodNotAllowed;
break;
}
await request.response.close();
});
} catch (e) {
log(LoggingLevel.warning, 'Error waiting for approval: $e');
}
// See https://modelcontextprotocol.io/specification/draft/client/elicitation#url-elicitation-required-error
throw RpcException(
McpErrorCodes.urlElicitationRequired,
'Permission must be granted first',
data:
ElicitRequest.url(
message: 'This tool requires approval to run',
url: 'http://${server.address.address}:${server.port}',
elicitationId: elicitationId,
)
as Map<String, Object?>,
);
}
await _startElicitationFlow();
return CallToolResult(content: [Content.text(text: 'Success!')]);
}
Future<void> _startElicitationFlow() async {
// You must wait for initialization to complete before you can make an
// elicitation request.
await initialized;
({String name, int age, String gender})? userInfo;
while (userInfo == null) {
userInfo = await _elicitInfo();
}
await _elicitUrl(userInfo, '12345');
}
/// Elicits a name from the user, and logs a message based on the response.
Future<({String name, int age, String gender})?> _elicitInfo() async {
final response = await elicit(
ElicitRequest.form(
message: 'I would like to ask you some personal information.',
requestedSchema: Schema.object(
properties: {
'name': Schema.string(),
'age': Schema.int(),
'gender': Schema.string(enumValues: ['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',
);
return (name: name, age: age, gender: gender);
case ElicitationAction.decline:
log(LoggingLevel.warning, 'Request for name was declined');
case ElicitationAction.cancel:
log(LoggingLevel.warning, 'Request for name was cancelled');
}
return null;
}
/// Elicits a URL from the user, asking them to navigate to a URL, and
/// then logging when we get the request.
Future<void> _elicitUrl(
({String name, int age, String gender}) userInfo,
String elicitationId,
) async {
// Start a simple web server on a random port.
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
try {
server.listen((HttpRequest request) async {
switch (request.method) {
case 'GET':
request.response
..statusCode = 200
..headers.contentType = ContentType.html
..write(_apiKeyWebPage);
break;
case 'POST':
// Read the posted api key from the form.
final formContents =
Uri(
query:
await request
.cast<List<int>>()
.transform(const Utf8Decoder())
.join(),
).queryParameters;
// Do not log this in real code of course!
log(LoggingLevel.warning, 'Form Contents: $formContents');
notifyElicitationComplete(
ElicitationCompleteNotification(elicitationId: elicitationId),
);
await server.close();
break;
default:
request.response.statusCode = HttpStatus.methodNotAllowed;
break;
}
await request.response.close();
});
final response = await elicit(
ElicitRequest.url(
message: 'Please navigate to a URL',
url: 'http://${server.address.address}:${server.port}',
elicitationId: elicitationId,
),
);
switch (response.action) {
case ElicitationAction.accept:
log(LoggingLevel.warning, 'Request to navigate to URI was accepted');
case ElicitationAction.decline:
log(LoggingLevel.warning, 'Request to navigate to URI was declined');
case ElicitationAction.cancel:
log(LoggingLevel.warning, 'Request to navigate to URI was cancelled');
}
} catch (e) {
log(LoggingLevel.warning, 'Error during URL elicitation: $e');
await server.close();
}
}
}
/// The basic web page to elicit an API key from the user.
final String _apiKeyWebPage = '''
<!DOCTYPE html>
<html>
<head>
<title>API Key Elicitation Example</title>
</head>
<body>
<form action="" method="post">
<label for="api_key">API Key:</label>
<input type="password" name="api_key" />
<input type="submit" />
</form>
</body>
</html>
''';
final String _permissionWebPage = '''
<!DOCTYPE html>
<html>
<head>
<title>Permission Elicitation Example</title>
</head>
<body>
<form action="" method="post">
<strong>Do we have permission to run this tool?</strong><br />
<label for="yes">Yes</label><input type="radio" name="permission" id="yes" value="yes" /><br />
<label for="no">No</label><input type="radio" name="permission" id="no" value="no" checked /><br />
<input type="submit" />
</form>
</body>
</html>
''';