Add more and better examples (#228)
Towards https://github.com/dart-lang/ai/issues/220.
I also added a utility function for created stdio stream channels to simplify the examples (and real user code), and fixed a bug in the PromptMessage constructor.
Still missing a few examples, but this covers the most common use cases.
cc @gaaclarke
diff --git a/mcp_examples/bin/file_system_server.dart b/mcp_examples/bin/file_system_server.dart
index 08ead76..d019aad 100644
--- a/mcp_examples/bin/file_system_server.dart
+++ b/mcp_examples/bin/file_system_server.dart
@@ -6,23 +6,13 @@
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 95af431..235bd6b 100644
--- a/mcp_examples/bin/workflow_client.dart
+++ b/mcp_examples/bin/workflow_client.dart
@@ -10,6 +10,7 @@
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 @@
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 6f0b5f5..52517b7 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 eaf8155..45f95e9 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 0000000..94f491e
--- /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 ?? <PromptArgument>[])
+ 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 0000000..76a1e29
--- /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 0000000..50d1bfc
--- /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 0000000..1b84145
--- /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 0000000..6294125
--- /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<void>.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<void>.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<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;
+ }
+}
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 0000000..7adbc80
--- /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<InitializeResult> 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_client.dart b/pkgs/dart_mcp/example/simple_client.dart
deleted file mode 100644
index 4a86d0f..0000000
--- a/pkgs/dart_mcp/example/simple_client.dart
+++ /dev/null
@@ -1,70 +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:io';
-
-import 'package:dart_mcp/client.dart';
-
-void main() async {
- final client = MCPClient(
- Implementation(name: 'example dart client', version: '0.1.0'),
- );
- print('connecting to server');
-
- final process = await Process.start('dart', [
- 'run',
- 'example/simple_server.dart',
- ]);
- final server = client.connectStdioServer(
- process.stdin,
- process.stdout,
- onDone: process.kill,
- );
- print('server started');
-
- print('initializing server');
- final initializeResult = await server.initialize(
- InitializeRequest(
- protocolVersion: ProtocolVersion.latestSupported,
- capabilities: client.capabilities,
- clientInfo: client.implementation,
- ),
- );
- 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}',
- );
- }
-
- if (initializeResult.capabilities.tools == null) {
- await server.shutdown();
- throw StateError('Server doesn\'t support tools!');
- }
-
- server.notifyInitialized(InitializedNotification());
- print('sent initialized notification');
-
- 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');
- final result = await server.callTool(
- CallToolRequest(name: 'hello_world'),
- );
- if (result.isError == true) {
- throw StateError('Tool call failed: ${result.content}');
- } else {
- print('Tool call succeeded: ${result.content}');
- }
- }
- }
-
- await client.shutdown();
-}
diff --git a/pkgs/dart_mcp/example/simple_server.dart b/pkgs/dart_mcp/example/simple_server.dart
deleted file mode 100644
index abe3c72..0000000
--- 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<InitializeResult> 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/tools_client.dart b/pkgs/dart_mcp/example/tools_client.dart
new file mode 100644
index 0000000..7946988
--- /dev/null
+++ b/pkgs/dart_mcp/example/tools_client.dart
@@ -0,0 +1,85 @@
+// 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 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/tools_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 tools capability.
+ if (initializeResult.capabilities.tools == null) {
+ await server.shutdown();
+ throw StateError('Server doesn\'t support tools!');
+ }
+
+ // 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}');
+ // 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: 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 0000000..967bf77
--- /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<CallToolResult> _concat(CallToolRequest request) => CallToolResult(
+ content: [
+ TextContent(
+ text: (request.arguments!['parts'] as List<dynamic>)
+ .cast<String>()
+ .join(''),
+ ),
+ ],
+ );
+}
diff --git a/pkgs/dart_mcp/lib/src/api/prompts.dart b/pkgs/dart_mcp/lib/src/api/prompts.dart
index 86fa48e..90126ec 100644
--- a/pkgs/dart_mcp/lib/src/api/prompts.dart
+++ b/pkgs/dart_mcp/lib/src/api/prompts.dart
@@ -135,7 +135,7 @@
/// This is similar to `SamplingMessage`, but also supports the embedding of
/// resources from the MCP server.
extension type PromptMessage.fromMap(Map<String, Object?> _value) {
- factory PromptMessage({required Role role, required List<Content> 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 b70b7b6..125f7bf 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 @@
@visibleForTesting
final Set<ServerConnection> 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 @@
/// 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<List<int>> stdin,
Stream<List<int>> stdout, {
Sink<String>? 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 @@
/// 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<String> channel, {
Sink<String>? protocolLogSink,
diff --git a/pkgs/dart_mcp/lib/stdio.dart b/pkgs/dart_mcp/lib/stdio.dart
new file mode 100644
index 0000000..795c535
--- /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<String> stdioChannel({
+ required Stream<List<int>> input,
+ required StreamSink<List<int>> 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 133b768..cc35efc 100644
--- a/pkgs/dart_mcp/test/api/prompts_test.dart
+++ b/pkgs/dart_mcp/test/api/prompts_test.dart
@@ -40,7 +40,7 @@
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 @@
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 4eb854b..382730f 100644
--- a/pkgs/dart_mcp_server/lib/src/server.dart
+++ b/pkgs/dart_mcp_server/lib/src/server.dart
@@ -8,11 +8,11 @@
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 @@
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 9f95850..9333d7e 100644
--- a/pkgs/dart_mcp_server/test/test_harness.dart
+++ b/pkgs/dart_mcp_server/test/test_harness.dart
@@ -9,6 +9,7 @@
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 @@
...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 c90a12c..f628319 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<String> args) async {
print('Getting registered tools...');
@@ -45,11 +47,10 @@
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(