update MCPClient.connectStdioServer to operate on streams and sinks (#204)

Updated MCPClient.connectStdioServer to take a stdin and stdout stream instead of a binary name and arguments. It no longer spawns processes on its own. This removes the `dart:io` dependency and allows more flexibility for how processes are spawned (can use package:process or even no process at all).

The migration is pretty easy, just launch your own process and pass in stdin/stdout from that.

- Closes https://github.com/dart-lang/ai/issues/195
- Enables the client to run on the web
- Run all tests on the web, with wasm and dart2js, as well as AOT and kernel (these tests are all very fast)
diff --git a/.github/workflows/dart_mcp.yaml b/.github/workflows/dart_mcp.yaml
index b8fc372..a92d08f 100644
--- a/.github/workflows/dart_mcp.yaml
+++ b/.github/workflows/dart_mcp.yaml
@@ -44,4 +44,4 @@
       - run: dart format --output=none --set-exit-if-changed .
         if: ${{ matrix.sdk == 'dev' }}
 
-      - run: dart test
+      - run: dart test -p chrome,vm -c dart2wasm,dart2js,kernel,exe
diff --git a/mcp_examples/bin/workflow_client.dart b/mcp_examples/bin/workflow_client.dart
index 08cf973..95af431 100644
--- a/mcp_examples/bin/workflow_client.dart
+++ b/mcp_examples/bin/workflow_client.dart
@@ -409,11 +409,16 @@
     for (var server in serverCommands) {
       final parts = server.split(' ');
       try {
+        final process = await Process.start(
+          parts.first,
+          parts.skip(1).toList(),
+        );
         serverConnections.add(
-          await connectStdioServer(
-            parts.first,
-            parts.skip(1).toList(),
+          connectStdioServer(
+            process.stdin,
+            process.stdout,
             protocolLogSink: logSink,
+            onDone: process.kill,
           ),
         );
       } catch (e) {
diff --git a/pkgs/dart_mcp/CHANGELOG.md b/pkgs/dart_mcp/CHANGELOG.md
index 008ac99..28118c8 100644
--- a/pkgs/dart_mcp/CHANGELOG.md
+++ b/pkgs/dart_mcp/CHANGELOG.md
@@ -10,13 +10,16 @@
     `propertyNamesInvalid`, `propertyValueInvalid`, `itemInvalid` and
     `prefixItemInvalid`.
 - Added a `custom` validation error type.
-- Auto-validate schemas for all tools by default. This can be disabled by
-  passing `validateArguments: false` to `registerTool`.
-  - This is breaking since this method is overridden by the Dart MCP server.
+- **Breaking**: Auto-validate schemas for all tools by default. This can be
+  disabled by passing `validateArguments: false` to `registerTool`.
 - Updates to the latest MCP spec, [2025-06-08](https://modelcontextprotocol.io/specification/2025-06-18/changelog)
   - Adds support for Elicitations to allow the server to ask the user questions.
   - Adds `ResourceLink` as a tool return content type.
   - Adds support for structured tool output.
+- **Breaking**: Change `MCPClient.connectStdioServer` signature to accept stdin
+  and stdout streams instead of starting processes itself. This enables custom
+  process spawning (such as using package:process), and also enables the client
+  to run in browser environments.
 
 ## 0.2.2
 
diff --git a/pkgs/dart_mcp/dart_test.yaml b/pkgs/dart_mcp/dart_test.yaml
new file mode 100644
index 0000000..6335bdd
--- /dev/null
+++ b/pkgs/dart_mcp/dart_test.yaml
@@ -0,0 +1,4 @@
+override_platforms:
+  chrome:
+    settings:
+      headless: true
diff --git a/pkgs/dart_mcp/example/simple_client.dart b/pkgs/dart_mcp/example/simple_client.dart
index bec522a..4a86d0f 100644
--- a/pkgs/dart_mcp/example/simple_client.dart
+++ b/pkgs/dart_mcp/example/simple_client.dart
@@ -2,6 +2,8 @@
 // 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 {
@@ -9,10 +11,16 @@
     Implementation(name: 'example dart client', version: '0.1.0'),
   );
   print('connecting to server');
-  final server = await client.connectStdioServer('dart', [
+
+  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');
diff --git a/pkgs/dart_mcp/lib/src/client/client.dart b/pkgs/dart_mcp/lib/src/client/client.dart
index 1b1a919..b70b7b6 100644
--- a/pkgs/dart_mcp/lib/src/client/client.dart
+++ b/pkgs/dart_mcp/lib/src/client/client.dart
@@ -5,8 +5,6 @@
 import 'dart:async';
 import 'dart:collection';
 import 'dart:convert';
-// TODO: Refactor to drop this dependency?
-import 'dart:io';
 
 import 'package:async/async.dart' hide Result;
 import 'package:meta/meta.dart';
@@ -50,29 +48,21 @@
   @visibleForTesting
   final Set<ServerConnection> connections = {};
 
-  /// Connect to a new MCP server by invoking [command] with [arguments] and
-  /// talking to that process over stdin/stdout.
+  /// Connect to a new MCP server over [stdin] and [stdout].
   ///
   /// If [protocolLogSink] is provided, all messages sent between the client and
   /// server will be 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.
-  Future<ServerConnection> connectStdioServer(
-    String command,
-    List<String> arguments, {
+  ///
+  /// If [onDone] is passed, it will be invoked when the connection shuts down.
+  ServerConnection connectStdioServer(
+    StreamSink<List<int>> stdin,
+    Stream<List<int>> stdout, {
     Sink<String>? protocolLogSink,
-  }) async {
-    final process = await Process.start(command, arguments);
-    process.stderr
-        .transform(utf8.decoder)
-        .transform(const LineSplitter())
-        .listen((line) {
-          stderr.writeln('[StdErr from server $command]: $line');
-        });
-    final channel = StreamChannel.withCloseGuarantee(
-          process.stdout,
-          process.stdin,
-        )
+    void Function()? onDone,
+  }) {
+    final channel = StreamChannel.withCloseGuarantee(stdout, stdin)
         .transform(StreamChannelTransformer.fromCodec(utf8))
         .transformStream(const LineSplitter())
         .transformSink(
@@ -83,13 +73,16 @@
           ),
         );
     final connection = connectServer(channel, protocolLogSink: protocolLogSink);
-    unawaited(connection.done.then((_) => process.kill()));
+    if (onDone != null) connection.done.then((_) => onDone());
     return connection;
   }
 
   /// Returns a connection for an MCP server using a [channel], which is already
   /// established.
   ///
+  /// Each [String] sent over [channel] represents an entire JSON request or
+  /// response.
+  ///
   /// If [protocolLogSink] is provided, all messages sent on [channel] will be
   /// forwarded to that [Sink] as well, with `<<<` preceding incoming messages
   /// and `>>>` preceding outgoing messages. It is the responsibility of the
diff --git a/pkgs/dart_mcp/test/server/resources_support_test.dart b/pkgs/dart_mcp/test/server/resources_support_test.dart
index e8cd660..536d88c 100644
--- a/pkgs/dart_mcp/test/server/resources_support_test.dart
+++ b/pkgs/dart_mcp/test/server/resources_support_test.dart
@@ -3,8 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:io';
-import 'dart:isolate';
 
 import 'package:async/async.dart';
 import 'package:dart_mcp/server.dart';
@@ -204,7 +202,10 @@
   test('Resource templates can be listed and queried', () async {
     final environment = TestEnvironment(
       TestMCPClient(),
-      TestMCPServerWithResources.new,
+      (channel) => TestMCPServerWithResources(
+        channel,
+        fileContents: {'package:foo/foo.dart': 'hello world!'},
+      ),
     );
     await environment.initializeServer();
 
@@ -220,24 +221,24 @@
     );
 
     final readResourceResponse = await serverConnection.readResource(
-      ReadResourceRequest(uri: 'package:test/test.dart'),
+      ReadResourceRequest(uri: 'package:foo/foo.dart'),
     );
     expect(
       (readResourceResponse.contents.single as TextResourceContents).text,
-      await File.fromUri(
-        (await Isolate.resolvePackageUri(Uri.parse('package:test/test.dart')))!,
-      ).readAsString(),
+      'hello world!',
     );
   });
 }
 
 final class TestMCPServerWithResources extends TestMCPServer
     with ResourcesSupport {
+  final Map<String, String> fileContents;
+
   @override
   /// Shorten this delay for the test so they run quickly.
   Duration get resourceUpdateThrottleDelay => Duration.zero;
 
-  TestMCPServerWithResources(super.channel);
+  TestMCPServerWithResources(super.channel, {this.fileContents = const {}});
 
   @override
   FutureOr<InitializeResult> initialize(InitializeRequest request) {
@@ -260,14 +261,12 @@
     if (!request.uri.endsWith('.dart')) {
       throw UnsupportedError('Only dart files can be read');
     }
-    final resolvedUri =
-        (await Isolate.resolvePackageUri(Uri.parse(request.uri)))!;
 
     return ReadResourceResult(
       contents: [
         TextResourceContents(
           uri: request.uri,
-          text: await File.fromUri(resolvedUri).readAsString(),
+          text: fileContents[request.uri]!,
         ),
       ],
     );
diff --git a/pkgs/dart_mcp_server/test/test_harness.dart b/pkgs/dart_mcp_server/test/test_harness.dart
index 3e5abdf..1f36e54 100644
--- a/pkgs/dart_mcp_server/test/test_harness.dart
+++ b/pkgs/dart_mcp_server/test/test_harness.dart
@@ -449,12 +449,18 @@
     addTearDown(server.shutdown);
     connection = client.connectServer(clientChannel);
   } else {
-    connection = await client.connectStdioServer(sdk.dartExecutablePath, [
+    final process = await Process.start(sdk.dartExecutablePath, [
       'pub', // Using `pub` gives us incremental compilation
       'run',
       'bin/main.dart',
       ...cliArgs,
     ]);
+    addTearDown(process.kill);
+    connection = client.connectStdioServer(
+      process.stdin,
+      process.stdout,
+      onDone: process.kill,
+    );
   }
 
   final initializeResult = await connection.initialize(