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(