diff --git a/pkgs/dart_mcp/example/prompts_and_completions_client.dart b/pkgs/dart_mcp/example/prompts_and_completions_client.dart new file mode 100644 index 00000000..6fe8e451 --- /dev/null +++ b/pkgs/dart_mcp/example/prompts_and_completions_client.dart @@ -0,0 +1,150 @@ +// 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 interacts with a server that provides prompts and completions +/// for the prompt arguments. +library; + +import 'dart:async'; +import 'dart:io'; + +import 'package:dart_mcp/client.dart'; +import 'package:dart_mcp/stdio.dart'; + +void main() async { + try { + // Change the stdin mode so we can handle bytes one at a time, to intercept + // the tab key for auto complete. + stdin.echoMode = false; + stdin.lineMode = false; + + // Create the client, which is the top level object that manages all + // server connections. + final client = MCPClient( + 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/prompts_and_completions_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'); + + // Ensure the server supports the prompts capability. + if (initializeResult.capabilities.prompts == null) { + await server.shutdown(); + throw StateError('Server doesn\'t support prompts!'); + } + + // Ensure the server supports the completions capability. + if (initializeResult.capabilities.completions == null) { + await server.shutdown(); + throw StateError('Server doesn\'t support completions!'); + } + + // Notify the server that we are initialized. + server.notifyInitialized(); + print('sent initialized notification'); + + // List all the available prompts from the server. + print('Listing prompts from server'); + final promptsResult = await server.listPrompts(ListPromptsRequest()); + + // Iterate each prompt and have the user fill in the arguments. + for (final prompt in promptsResult.prompts) { + print( + 'Found prompt ${prompt.name}, fill in the following arguments using ' + 'tab to complete them:', + ); + // For each argument, get a value from the user. + final arguments = {}; + for (var argument in prompt.arguments!) { + stdout.write('${argument.name}: '); + // The current user query. + var current = ''; + // Read characters until we get an enter key. + while (true) { + final next = stdin.readByteSync(); + // User pressed tab, lets do an auto complete + if (next == 9) { + final completeResult = await server.requestCompletions( + CompleteRequest( + // The ref is the current prompt name. + ref: PromptReference(name: prompt.name), + argument: CompletionArgument( + name: argument.name, + value: current, + ), + ), + ); + // Just auto-fill the first completion for this example + if (completeResult.completion.values.isNotEmpty) { + final firstResult = completeResult.completion.values.first; + stdout.write(firstResult.substring(current.length)); + current = firstResult; + } + // If there are no completions, just do nothing. + } else if (next == 10) { + // Enter key, assign the argument and break the loop. + arguments[argument.name] = current; + stdout.writeln(''); + break; + } else if (next == 127) { + // Backspace keypress. + if (current.isNotEmpty) { + // Write a backspace followed by a space and then another + // backspace to clear one character. + stdout.write('\b \b'); + // Trim current by one. + current = current.substring(0, current.length - 1); + } + } else { + // A regular character, just add it to current and print it to the + // console. + final character = String.fromCharCode(next); + current += character; + stdout.write(character); + } + } + } + + // Now fetch the full prompt with the arguments filled in. + final promptResult = await server.getPrompt( + GetPromptRequest(name: prompt.name, arguments: arguments), + ); + final promptText = promptResult.messages + .map((m) => (m.content as TextContent).text) + .join(''); + + // Finally, print the prompt to the user. + print('Got full prompt `${prompt.name}`: "$promptText"'); + } + + // Shutdown the client, which will also shutdown the server connection. + await client.shutdown(); + } finally { + // Reset the terminal modes. + stdin.echoMode = true; + stdin.lineMode = true; + } +} diff --git a/pkgs/dart_mcp/example/prompts_server.dart b/pkgs/dart_mcp/example/prompts_and_completions_server.dart similarity index 61% rename from pkgs/dart_mcp/example/prompts_server.dart rename to pkgs/dart_mcp/example/prompts_and_completions_server.dart index 76a1e292..1573aaa2 100644 --- a/pkgs/dart_mcp/example/prompts_server.dart +++ b/pkgs/dart_mcp/example/prompts_and_completions_server.dart @@ -2,9 +2,12 @@ // 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 implements the prompts API using the [PromptsSupport] mixin. +/// A server that implements the prompts API using the [PromptsSupport] mixin, +/// as well as completions for the prompt arguments with the +/// [CompletionsSupport] mixin library; +import 'dart:async'; import 'dart:io' as io; import 'package:dart_mcp/server.dart'; @@ -19,7 +22,11 @@ void main() { /// /// This server uses the [PromptsSupport] mixin to provide prompts to the /// client. -base class MCPServerWithPrompts extends MCPServer with PromptsSupport { +/// +/// It also uses the [CompletionsSupport] mixin to provide support for auto +/// completing prompt argument values. +base class MCPServerWithPrompts extends MCPServer + with PromptsSupport, CompletionsSupport { MCPServerWithPrompts(super.channel) : super.fromStreamChannel( implementation: Implementation( @@ -60,10 +67,46 @@ base class MCPServerWithPrompts extends MCPServer with PromptsSupport { ); } + @override + /// Handles auto completing arguments based on the known [tags] and + /// [platforms]. + FutureOr handleComplete(CompleteRequest request) { + // Check that this is for the expected prompt reference. Prompts are + // referenced by their name. + if (!request.ref.isPrompt || + (request.ref as PromptReference).name != runTestsPrompt.name) { + throw ArgumentError('Unrecognized reference ${request.ref}'); + } + // Get the candidates. + final candidates = switch (request.argument.name) { + 'tags' => tags, + 'platforms' => platforms, + _ => + throw ArgumentError('Unrecognized argument ${request.argument.name}'), + }; + // Return the result by filtering the candidates based on a simple prefix + // match. + return CompleteResult( + completion: Completion( + values: [ + for (final candidate in candidates) + if (candidate.startsWith(request.argument.value)) candidate, + ], + hasMore: false, + ), + ); + } + + /// The known tags we will autocomplete. + static final tags = ['integration', 'unit', 'slow']; + + /// The known platforms we will auto complete. + static final platforms = ['vm', 'chrome']; + /// A prompt that can be used to run tests. /// /// This prompt has two arguments, `tags` and `platforms`. - final runTestsPrompt = Prompt( + static final runTestsPrompt = Prompt( name: 'run_tests', description: 'Run your dart tests', arguments: [ diff --git a/pkgs/dart_mcp/example/prompts_client.dart b/pkgs/dart_mcp/example/prompts_client.dart deleted file mode 100644 index 94f491e4..00000000 --- a/pkgs/dart_mcp/example/prompts_client.dart +++ /dev/null @@ -1,82 +0,0 @@ -// 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 interacts with a server that provides prompts. -library; - -import 'dart:async'; -import 'dart:io'; - -import 'package:dart_mcp/client.dart'; -import 'package:dart_mcp/stdio.dart'; - -void main() async { - // Create the client, which is the top level object that manages all - // server connections. - final client = MCPClient( - 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/prompts_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'); - - // Ensure the server supports the prompts capability. - if (initializeResult.capabilities.prompts == null) { - await server.shutdown(); - throw StateError('Server doesn\'t support prompts!'); - } - - // Notify the server that we are initialized. - server.notifyInitialized(); - print('sent initialized notification'); - - // List all the available prompts from the server. - print('Listing prompts from server'); - final promptsResult = await server.listPrompts(ListPromptsRequest()); - for (final prompt in promptsResult.prompts) { - // For each prompt, get the full prompt text, filling in any arguments. - final promptResult = await server.getPrompt( - GetPromptRequest( - name: prompt.name, - arguments: { - for (var arg in prompt.arguments ?? []) - arg.name: switch (arg.name) { - 'tags' => 'myTag myOtherTag', - 'platforms' => 'vm,chrome', - _ => throw ArgumentError('Unrecognized argument ${arg.name}'), - }, - }, - ), - ); - final promptText = promptResult.messages - .map((m) => (m.content as TextContent).text) - .join(''); - print('Found prompt `${prompt.name}`: "$promptText"'); - } - - // Shutdown the client, which will also shutdown the server connection. - await client.shutdown(); -}