diff --git a/mcp_examples/bin/file_system_server.dart b/mcp_examples/bin/file_system_server.dart index 08ead76f..d019aada 100644 --- a/mcp_examples/bin/file_system_server.dart +++ b/mcp_examples/bin/file_system_server.dart @@ -6,23 +6,13 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; -import 'package:async/async.dart'; import 'package:dart_mcp/server.dart'; +import 'package:dart_mcp/stdio.dart'; import 'package:path/path.dart' as p; -import 'package:stream_channel/stream_channel.dart'; void main() { SimpleFileSystemServer.fromStreamChannel( - StreamChannel.withCloseGuarantee(io.stdin, io.stdout) - .transform(StreamChannelTransformer.fromCodec(utf8)) - .transformStream(const LineSplitter()) - .transformSink( - StreamSinkTransformer.fromHandlers( - handleData: (data, sink) { - sink.add('$data\n'); - }, - ), - ), + stdioChannel(input: io.stdin, output: io.stdout), ); } diff --git a/mcp_examples/bin/workflow_client.dart b/mcp_examples/bin/workflow_client.dart index 95af431e..235bd6b7 100644 --- a/mcp_examples/bin/workflow_client.dart +++ b/mcp_examples/bin/workflow_client.dart @@ -10,6 +10,7 @@ import 'package:args/args.dart'; import 'package:async/async.dart'; import 'package:cli_util/cli_logging.dart'; import 'package:dart_mcp/client.dart'; +import 'package:dart_mcp/stdio.dart'; import 'package:google_generative_ai/google_generative_ai.dart' as gemini; /// The list of Gemini models that are accepted as a "--model" argument. @@ -414,12 +415,10 @@ final class WorkflowClient extends MCPClient with RootsSupport { parts.skip(1).toList(), ); serverConnections.add( - connectStdioServer( - process.stdin, - process.stdout, + connectServer( + stdioChannel(input: process.stdout, output: process.stdin), protocolLogSink: logSink, - onDone: process.kill, - ), + )..done.then((_) => process.kill()), ); } catch (e) { logger.stderr('Failed to connect to server $server: $e'); diff --git a/pkgs/dart_mcp/CHANGELOG.md b/pkgs/dart_mcp/CHANGELOG.md index 6f0b5f52..52517b7e 100644 --- a/pkgs/dart_mcp/CHANGELOG.md +++ b/pkgs/dart_mcp/CHANGELOG.md @@ -2,6 +2,10 @@ - Fixes communication problem when a `MCPServer` is instantiated without instructions. +- Fix the `content` argument to `PromptMessage` to be a single `Content` object. +- 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. ## 0.3.0 diff --git a/pkgs/dart_mcp/example/README.md b/pkgs/dart_mcp/example/README.md index eaf81553..45f95e9e 100644 --- a/pkgs/dart_mcp/example/README.md +++ b/pkgs/dart_mcp/example/README.md @@ -1,10 +1,16 @@ -# Simple Client and Server +# Client and Server examples -See `bin/simple_client.dart` and `bin/simple_server.dart` for a basic example of -how to use the `MCPClient` and `MCPServer` classes. These don't use any LLM to -invoke tools. +For each client or server feature, there is a corresponding example here with +the {feature}_client.dart and {feature}_server.dart file names. Sometimes +multiple features are demonstrated together where appropriate, in which case the +file name will indicate this. -# Full Features Examples +To run the examples, run the client file directly, so for instance +`dart run example/tools_client.dart` with run the example client which invokes +tools, connected to the example server that provides tools +(at `example/tools_server.dart`). + +# Full Featured Examples See https://github.com/dart-lang/ai/tree/main/mcp_examples for some more full featured examples using gemini to automatically invoke tools. diff --git a/pkgs/dart_mcp/example/prompts_client.dart b/pkgs/dart_mcp/example/prompts_client.dart new file mode 100644 index 00000000..94f491e4 --- /dev/null +++ b/pkgs/dart_mcp/example/prompts_client.dart @@ -0,0 +1,82 @@ +// 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(); +} diff --git a/pkgs/dart_mcp/example/prompts_server.dart b/pkgs/dart_mcp/example/prompts_server.dart new file mode 100644 index 00000000..76a1e292 --- /dev/null +++ b/pkgs/dart_mcp/example/prompts_server.dart @@ -0,0 +1,80 @@ +// 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 implements the prompts API using the [PromptsSupport] 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. + MCPServerWithPrompts(stdioChannel(input: io.stdin, output: io.stdout)); +} + +/// Our actual MCP server. +/// +/// This server uses the [PromptsSupport] mixin to provide prompts to the +/// client. +base class MCPServerWithPrompts extends MCPServer with PromptsSupport { + MCPServerWithPrompts(super.channel) + : super.fromStreamChannel( + implementation: Implementation( + name: 'An example dart server with prompts support', + version: '0.1.0', + ), + instructions: 'Just list the prompts :D', + ) { + // Actually add the prompt. + addPrompt(runTestsPrompt, _runTestsPrompt); + } + + /// The prompt implementation, takes in a [request] and builds the prompt + /// by substituting in arguments. + GetPromptResult _runTestsPrompt(GetPromptRequest request) { + // The actual arguments should be comma separated, but we allow for space + // separated and then convert it here. + final tags = (request.arguments?['tags'] as String?)?.split(' ').join(','); + final platforms = (request.arguments?['platforms'] as String?) + ?.split(' ') + .join(','); + return GetPromptResult( + messages: [ + // This is a prompt that should execute as if it came from the user, + // instructing the LLM to run a specific CLI command based on the + // arguments given. + PromptMessage( + role: Role.user, + content: Content.text( + text: + 'Execute the shell command `dart test --failures-only' + '${tags != null ? ' -t $tags' : ''}' + '${platforms != null ? ' -p $platforms' : ''}' + '`', + ), + ), + ], + ); + } + + /// A prompt that can be used to run tests. + /// + /// This prompt has two arguments, `tags` and `platforms`. + final runTestsPrompt = Prompt( + name: 'run_tests', + description: 'Run your dart tests', + arguments: [ + PromptArgument( + name: 'tags', + description: 'The test tags to include, space or comma separated', + ), + PromptArgument( + name: 'platforms', + description: 'The platforms to run on, space or comma separated', + ), + ], + ); +} diff --git a/pkgs/dart_mcp/example/resources_client.dart b/pkgs/dart_mcp/example/resources_client.dart new file mode 100644 index 00000000..50d1bfc3 --- /dev/null +++ b/pkgs/dart_mcp/example/resources_client.dart @@ -0,0 +1,87 @@ +// 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 exercises the resources API. +import 'dart:async'; +import 'dart:io'; + +import 'package:dart_mcp/client.dart'; +import 'package:dart_mcp/stdio.dart'; + +void main() async { + // Create a 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/resources_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 resources capability. + if (initializeResult.capabilities.resources == null) { + await server.shutdown(); + throw StateError('Server doesn\'t support resources!'); + } + + // Notify the server that we are initialized. + server.notifyInitialized(); + print('sent initialized notification'); + + // List all the available resources from the server. + print('Listing resources from server'); + final resourcesResult = await server.listResources(ListResourcesRequest()); + for (final resource in resourcesResult.resources) { + // For each resource, read its content. + final content = (await server.readResource( + ReadResourceRequest(uri: resource.uri), + )).contents.map((part) => (part as TextResourceContents).text).join(''); + print( + 'Found resource: ${resource.name} with uri ${resource.uri} and contents: ' + '"$content"', + ); + } + + // List all the available resource templates from the server. + print('Listing resource templates from server'); + final templatesResult = await server.listResourceTemplates( + ListResourceTemplatesRequest(), + ); + for (final template in templatesResult.resourceTemplates) { + print('Found resource template `${template.uriTemplate}`'); + // For each template, fill in the path variable and read the resource. + for (var path in ['zip', 'zap']) { + final uri = template.uriTemplate.replaceFirst(RegExp('{.*}'), path); + final contents = (await server.readResource( + ReadResourceRequest(uri: uri), + )).contents.map((part) => (part as TextResourceContents).text).join(''); + print('Read resource `$uri`: "$contents"'); + } + } + + // Shutdown the client, which will also shutdown the server connection. + await client.shutdown(); +} diff --git a/pkgs/dart_mcp/example/resources_server.dart b/pkgs/dart_mcp/example/resources_server.dart new file mode 100644 index 00000000..1b841453 --- /dev/null +++ b/pkgs/dart_mcp/example/resources_server.dart @@ -0,0 +1,64 @@ +// 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 implements the resources API using the [ResourcesSupport] +/// 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. + MCPServerWithResources(stdioChannel(input: io.stdin, output: io.stdout)); +} + +/// An MCP server with resource and resource template support. +/// +/// This server uses the [ResourcesSupport] mixin to provide resources to the +/// client. +base class MCPServerWithResources extends MCPServer with ResourcesSupport { + MCPServerWithResources(super.channel) + : super.fromStreamChannel( + implementation: Implementation( + name: 'An example dart server with resources support', + version: '0.1.0', + ), + instructions: 'Just list and read the resources :D', + ) { + // Add a standard resource with a fixed URI. + addResource( + Resource(uri: 'example://resource.txt', name: 'An example resource'), + (request) => ReadResourceResult( + contents: [TextResourceContents(text: 'Example!', uri: request.uri)], + ), + ); + + // A resource template which always just returns the path portion of the + // requested URI as the content of the resource. + addResourceTemplate( + ResourceTemplate( + uriTemplate: 'example_template://{path}', + name: 'Example resource template', + ), + (request) { + // This template only handles resource URIs with this exact prefix, + // returning null defers to the next resource template handler. + if (!request.uri.startsWith('example_template://')) { + return null; + } + return ReadResourceResult( + contents: [ + TextResourceContents( + text: request.uri.substring('example_template://'.length), + uri: request.uri, + ), + ], + ); + }, + ); + } +} diff --git a/pkgs/dart_mcp/example/roots_and_logging_client.dart b/pkgs/dart_mcp/example/roots_and_logging_client.dart new file mode 100644 index 00000000..62941255 --- /dev/null +++ b/pkgs/dart_mcp/example/roots_and_logging_client.dart @@ -0,0 +1,95 @@ +// 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 exercises the roots and logging APIs. +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 = MCPClientWithRoots( + 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/roots_and_logging_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 logging capability. + if (initializeResult.capabilities.logging == null) { + await server.shutdown(); + throw StateError('Server doesn\'t support logging!'); + } + + // Notify the server that we are initialized. + server.notifyInitialized(); + print('sent initialized notification'); + + // Wait a second and then add a new root, the server is going to send a log + // back confirming that it got the notification that the roots changed. + await Future.delayed(const Duration(seconds: 1)); + client.addRoot(Root(uri: 'new_root://some_path', name: 'A new root')); + + // Give the logs a chance to propagate. + await Future.delayed(const Duration(seconds: 1)); + // Shutdown the client, which will also shutdown the server connection. + await client.shutdown(); +} + +/// A custom client that uses the [RootsSupport] mixin. +/// +/// This allows the client to manage a set of roots and notify servers of +/// changes to them. +final class MCPClientWithRoots extends MCPClient with RootsSupport { + MCPClientWithRoots(super.implementation) { + // Add an initial root for the current working directory. + addRoot(Root(uri: Directory.current.path, name: 'Working dir')); + } + + /// Whenever connecting to a server, we also listen for log messages. + /// + /// The server we connect to will log the roots that it sees, both on startup + /// and any time they change. + @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/roots_and_logging_server.dart b/pkgs/dart_mcp/example/roots_and_logging_server.dart new file mode 100644 index 00000000..7adbc80e --- /dev/null +++ b/pkgs/dart_mcp/example/roots_and_logging_server.dart @@ -0,0 +1,75 @@ +// 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 tracks the client for roots with the [RootsTrackingSupport] +/// mixin and implements logging with the [LoggingSupport] mixin. +library; + +import 'dart:async'; +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. + MCPServerWithRootsTrackingSupport( + stdioChannel(input: io.stdin, output: io.stdout), + ); +} + +/// Our actual MCP server. +/// +/// This server uses the [LoggingSupport] and [RootsTrackingSupport] mixins to +/// receive root changes from the client and send log messages to it. +base class MCPServerWithRootsTrackingSupport extends MCPServer + with LoggingSupport, RootsTrackingSupport { + MCPServerWithRootsTrackingSupport(super.channel) + : super.fromStreamChannel( + implementation: Implementation( + name: 'An example dart server with roots tracking support', + version: '0.1.0', + ), + instructions: 'Just list and call the tools :D', + ) { + // Once the server is initialized, we can start listening for root changes + // and printing the current roots. + // + // No communication is allowed prior to initialization, even logging. + initialized.then((_) async { + _logRoots(); + // Whenever the roots list changes, we log a message and print the new + // roots. + // + // This stream is not set up until after initialization. + rootsListChanged?.listen((_) { + log(LoggingLevel.warning, 'Server got roots list change notification'); + _logRoots(); + }); + }); + } + + @override + Future initialize(InitializeRequest request) async { + // We require the client to support roots. + if (request.capabilities.roots == null) { + throw StateError('Client doesn\'t support roots!'); + } + + return await super.initialize(request); + } + + /// Logs the current list of roots. + void _logRoots() async { + final initialRoots = await listRoots(ListRootsRequest()); + final rootsLines = initialRoots.roots + .map((r) => ' - ${r.name}: ${r.uri}') + .join('\n'); + log( + LoggingLevel.warning, + 'Current roots:\n' + '$rootsLines', + ); + } +} diff --git a/pkgs/dart_mcp/example/simple_server.dart b/pkgs/dart_mcp/example/simple_server.dart deleted file mode 100644 index abe3c72e..00000000 --- a/pkgs/dart_mcp/example/simple_server.dart +++ /dev/null @@ -1,47 +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. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:async/async.dart'; -import 'package:dart_mcp/server.dart'; -import 'package:stream_channel/stream_channel.dart'; - -void main() { - DartMCPServer( - StreamChannel.withCloseGuarantee(io.stdin, io.stdout) - .transform(StreamChannelTransformer.fromCodec(utf8)) - .transformStream(const LineSplitter()) - .transformSink( - StreamSinkTransformer.fromHandlers( - handleData: (data, sink) { - sink.add('$data\n'); - }, - ), - ), - ); -} - -/// Our actual MCP server. -base class DartMCPServer extends MCPServer with ToolsSupport { - DartMCPServer(super.channel) - : super.fromStreamChannel( - implementation: Implementation( - name: 'example dart server', - version: '0.1.0', - ), - instructions: 'A basic tool that can respond with "hello world!"', - ); - - @override - FutureOr initialize(InitializeRequest request) { - registerTool( - Tool(name: 'hello_world', inputSchema: ObjectSchema()), - (_) => CallToolResult(content: [TextContent(text: 'hello world!')]), - ); - return super.initialize(request); - } -} diff --git a/pkgs/dart_mcp/example/simple_client.dart b/pkgs/dart_mcp/example/tools_client.dart similarity index 51% rename from pkgs/dart_mcp/example/simple_client.dart rename to pkgs/dart_mcp/example/tools_client.dart index 4a86d0f9..79469887 100644 --- a/pkgs/dart_mcp/example/simple_client.dart +++ b/pkgs/dart_mcp/example/tools_client.dart @@ -2,27 +2,35 @@ // 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. +import 'dart:async'; import 'dart:io'; import 'package:dart_mcp/client.dart'; +import 'package:dart_mcp/stdio.dart'; void main() async { + // Create a 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/simple_server.dart', + 'example/tools_server.dart', ]); - final server = client.connectStdioServer( - process.stdin, - process.stdout, - onDone: process.kill, + // 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( @@ -32,39 +40,46 @@ void main() async { ), ); print('initialized: $initializeResult'); - if (!initializeResult.protocolVersion!.isSupported) { - throw StateError( - 'Protocol version mismatch, expected a version between ' - '${ProtocolVersion.oldestSupported} and ' - '${ProtocolVersion.latestSupported}, but received ' - '${initializeResult.protocolVersion}', - ); - } + // Ensure the server supports the tools capability. if (initializeResult.capabilities.tools == null) { await server.shutdown(); throw StateError('Server doesn\'t support tools!'); } - server.notifyInitialized(InitializedNotification()); + // Notify the server that we are initialized. + server.notifyInitialized(); print('sent initialized notification'); + // List all the available tools from the server. print('Listing tools from server'); final toolsResult = await server.listTools(ListToolsRequest()); for (final tool in toolsResult.tools) { print('Found Tool: ${tool.name}'); - if (tool.name == 'hello_world') { - print('Calling `hello_world` tool'); + // Normally, you would expose these tools to an LLM to call them as it + // sees fit. To keep this example simple and not require any API keys, we + // just manually call the `concat` tool. + if (tool.name == 'concat') { + print('Calling `${tool.name}` tool'); + // Should return "abcd". final result = await server.callTool( - CallToolRequest(name: 'hello_world'), + CallToolRequest( + name: tool.name, + arguments: { + 'parts': ['a', 'b', 'c', 'd'], + }, + ), ); if (result.isError == true) { throw StateError('Tool call failed: ${result.content}'); } else { print('Tool call succeeded: ${result.content}'); } + } else { + throw ArgumentError('Unexpected tool ${tool.name}'); } } + // Shutdown the client, which will also shutdown the server connection. await client.shutdown(); } diff --git a/pkgs/dart_mcp/example/tools_server.dart b/pkgs/dart_mcp/example/tools_server.dart new file mode 100644 index 00000000..967bf77d --- /dev/null +++ b/pkgs/dart_mcp/example/tools_server.dart @@ -0,0 +1,57 @@ +// 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 implements the tools API using the [ToolsSupport] mixin. +library; + +import 'dart:async'; +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. + MCPServerWithTools(stdioChannel(input: io.stdin, output: io.stdout)); +} + +/// This server uses the [ToolsSupport] mixin to provide tools to the client. +base class MCPServerWithTools extends MCPServer with ToolsSupport { + MCPServerWithTools(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', + ) { + registerTool(concatTool, _concat); + } + + /// A tool that concatenates a list of strings. + final concatTool = Tool( + name: 'concat', + description: 'concatenates many string parts into one string', + inputSchema: Schema.object( + properties: { + 'parts': Schema.list( + description: 'The parts to concatenate together', + items: Schema.string(), + ), + }, + required: ['parts'], + ), + ); + + /// The implementation of the `concat` tool. + FutureOr _concat(CallToolRequest request) => CallToolResult( + content: [ + TextContent( + text: (request.arguments!['parts'] as List) + .cast() + .join(''), + ), + ], + ); +} diff --git a/pkgs/dart_mcp/lib/src/api/prompts.dart b/pkgs/dart_mcp/lib/src/api/prompts.dart index 86fa48e2..90126eca 100644 --- a/pkgs/dart_mcp/lib/src/api/prompts.dart +++ b/pkgs/dart_mcp/lib/src/api/prompts.dart @@ -135,7 +135,7 @@ enum Role { user, assistant } /// This is similar to `SamplingMessage`, but also supports the embedding of /// resources from the MCP server. extension type PromptMessage.fromMap(Map _value) { - factory PromptMessage({required Role role, required List content}) => + factory PromptMessage({required Role role, required Content content}) => PromptMessage.fromMap({'role': role.name, 'content': content}); /// The expected [Role] for this message in the prompt (multi-message diff --git a/pkgs/dart_mcp/lib/src/client/client.dart b/pkgs/dart_mcp/lib/src/client/client.dart index b70b7b61..125f7bf3 100644 --- a/pkgs/dart_mcp/lib/src/client/client.dart +++ b/pkgs/dart_mcp/lib/src/client/client.dart @@ -4,12 +4,11 @@ import 'dart:async'; import 'dart:collection'; -import 'dart:convert'; -import 'package:async/async.dart' hide Result; import 'package:meta/meta.dart'; import 'package:stream_channel/stream_channel.dart'; +import '../../stdio.dart'; import '../api/api.dart'; import '../shared.dart'; @@ -48,7 +47,8 @@ base class MCPClient { @visibleForTesting final Set connections = {}; - /// Connect to a new MCP server over [stdin] and [stdout]. + /// Connect to a new MCP server over [stdin] and [stdout], where these + /// correspond to the stdio streams of the server process (not the client). /// /// If [protocolLogSink] is provided, all messages sent between the client and /// server will be forwarded to that [Sink] as well, with `<<<` preceding @@ -56,22 +56,14 @@ base class MCPClient { /// responsibility of the caller to close this sink. /// /// If [onDone] is passed, it will be invoked when the connection shuts down. + @Deprecated('Use stdioChannel and connectServer instead.') ServerConnection connectStdioServer( StreamSink> stdin, Stream> stdout, { Sink? protocolLogSink, void Function()? onDone, }) { - final channel = StreamChannel.withCloseGuarantee(stdout, stdin) - .transform(StreamChannelTransformer.fromCodec(utf8)) - .transformStream(const LineSplitter()) - .transformSink( - StreamSinkTransformer.fromHandlers( - handleData: (data, sink) { - sink.add('$data\n'); - }, - ), - ); + final channel = stdioChannel(input: stdout, output: stdin); final connection = connectServer(channel, protocolLogSink: protocolLogSink); if (onDone != null) connection.done.then((_) => onDone()); return connection; @@ -87,6 +79,9 @@ base class MCPClient { /// forwarded to that [Sink] as well, with `<<<` preceding incoming messages /// and `>>>` preceding outgoing messages. It is the responsibility of the /// caller to close this sink. + /// + /// To perform cleanup when this connection is closed, use the + /// [ServerConnection.done] future. ServerConnection connectServer( StreamChannel channel, { Sink? protocolLogSink, diff --git a/pkgs/dart_mcp/lib/stdio.dart b/pkgs/dart_mcp/lib/stdio.dart new file mode 100644 index 00000000..795c5359 --- /dev/null +++ b/pkgs/dart_mcp/lib/stdio.dart @@ -0,0 +1,27 @@ +// 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. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:async/async.dart'; +import 'package:stream_channel/stream_channel.dart'; + +/// Creates a [StreamChannel] for Stdio communication where messages are +/// separated by newlines. +/// +/// This expects incoming messages on [input], and writes messages to [output]. +StreamChannel stdioChannel({ + required Stream> input, + required StreamSink> output, +}) => StreamChannel.withCloseGuarantee(input, output) + .transform(StreamChannelTransformer.fromCodec(utf8)) + .transformStream(const LineSplitter()) + .transformSink( + StreamSinkTransformer.fromHandlers( + handleData: (data, sink) { + sink.add('$data\n'); + }, + ), + ); diff --git a/pkgs/dart_mcp/test/api/prompts_test.dart b/pkgs/dart_mcp/test/api/prompts_test.dart index 133b768d..cc35efcd 100644 --- a/pkgs/dart_mcp/test/api/prompts_test.dart +++ b/pkgs/dart_mcp/test/api/prompts_test.dart @@ -40,7 +40,7 @@ void main() { greetingResult.messages.single, PromptMessage( role: Role.user, - content: [TextContent(text: 'Please greet me joyously')], + content: TextContent(text: 'Please greet me joyously'), ), ); }); @@ -91,9 +91,9 @@ final class TestMCPServerWithPrompts extends TestMCPServer with PromptsSupport { messages: [ PromptMessage( role: Role.user, - content: [ - TextContent(text: 'Please greet me ${request.arguments!['style']}'), - ], + content: TextContent( + text: 'Please greet me ${request.arguments!['style']}', + ), ), ], ); diff --git a/pkgs/dart_mcp_server/lib/src/server.dart b/pkgs/dart_mcp_server/lib/src/server.dart index 4eb854be..382730fa 100644 --- a/pkgs/dart_mcp_server/lib/src/server.dart +++ b/pkgs/dart_mcp_server/lib/src/server.dart @@ -8,11 +8,11 @@ import 'dart:io' as io; import 'package:async/async.dart'; import 'package:dart_mcp/server.dart'; +import 'package:dart_mcp/stdio.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:meta/meta.dart'; import 'package:process/process.dart'; -import 'package:stream_channel/stream_channel.dart'; import 'package:unified_analytics/unified_analytics.dart'; import 'arg_parser.dart'; @@ -92,16 +92,7 @@ final class DartMCPServer extends MCPServer runZonedGuarded( () { server = DartMCPServer( - StreamChannel.withCloseGuarantee(io.stdin, io.stdout) - .transform(StreamChannelTransformer.fromCodec(utf8)) - .transformStream(const LineSplitter()) - .transformSink( - StreamSinkTransformer.fromHandlers( - handleData: (data, sink) { - sink.add('$data\n'); - }, - ), - ), + stdioChannel(input: io.stdin, output: io.stdout), forceRootsFallback: parsedArgs.flag(forceRootsFallbackFlag), sdk: Sdk.find( dartSdkPath: dartSdkPath, diff --git a/pkgs/dart_mcp_server/test/test_harness.dart b/pkgs/dart_mcp_server/test/test_harness.dart index 9f95850b..9333d7e4 100644 --- a/pkgs/dart_mcp_server/test/test_harness.dart +++ b/pkgs/dart_mcp_server/test/test_harness.dart @@ -9,6 +9,7 @@ import 'dart:io' as io show File; import 'package:async/async.dart'; import 'package:dart_mcp/client.dart'; +import 'package:dart_mcp/stdio.dart'; import 'package:dart_mcp_server/src/mixins/dtd.dart'; import 'package:dart_mcp_server/src/server.dart'; import 'package:dart_mcp_server/src/utils/constants.dart'; @@ -424,11 +425,10 @@ Future _initializeMCPServer( ...cliArgs, ]); addTearDown(process.kill); - connection = client.connectStdioServer( - process.stdin, - process.stdout, - onDone: process.kill, + connection = client.connectServer( + stdioChannel(input: process.stdout, output: process.stdin), ); + unawaited(connection.done.then((_) => process.kill())); } final initializeResult = await connection.initialize( diff --git a/pkgs/dart_mcp_server/tool/update_readme.dart b/pkgs/dart_mcp_server/tool/update_readme.dart index c90a12c2..f628319a 100644 --- a/pkgs/dart_mcp_server/tool/update_readme.dart +++ b/pkgs/dart_mcp_server/tool/update_readme.dart @@ -2,9 +2,11 @@ // 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. +import 'dart:async'; import 'dart:io'; import 'package:dart_mcp/client.dart'; +import 'package:dart_mcp/stdio.dart'; void main(List args) async { print('Getting registered tools...'); @@ -45,11 +47,10 @@ Future> _retrieveRegisteredTools() async { Implementation(name: 'list tools client', version: '1.0.0'), ); final process = await Process.start('dart', ['run', 'bin/main.dart']); - final server = client.connectStdioServer( - process.stdin, - process.stdout, - onDone: process.kill, + final server = client.connectServer( + stdioChannel(input: process.stdout, output: process.stdin), ); + unawaited(server.done.then((_) => process.kill())); await server.initialize( InitializeRequest(