diff --git a/.github/workflows/dart_mcp.yaml b/.github/workflows/dart_mcp.yaml index b8fc3725..a92d08ff 100644 --- a/.github/workflows/dart_mcp.yaml +++ b/.github/workflows/dart_mcp.yaml @@ -44,4 +44,4 @@ jobs: - 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 08cf9730..95af431e 100644 --- a/mcp_examples/bin/workflow_client.dart +++ b/mcp_examples/bin/workflow_client.dart @@ -409,11 +409,16 @@ final class WorkflowClient extends MCPClient with RootsSupport { 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 008ac997..28118c8c 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 00000000..6335bdde --- /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 bec522a9..4a86d0f9 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 @@ void main() async { 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 1b1a919c..b70b7b61 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 @@ base class MCPClient { @visibleForTesting final Set 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 connectStdioServer( - String command, - List arguments, { + /// + /// If [onDone] is passed, it will be invoked when the connection shuts down. + ServerConnection connectStdioServer( + StreamSink> stdin, + Stream> stdout, { Sink? 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 @@ base class MCPClient { ), ); 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 e8cd6601..536d88cc 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 @@ void main() { 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 @@ void main() { ); 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 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 initialize(InitializeRequest request) { @@ -260,14 +261,12 @@ final class TestMCPServerWithResources extends TestMCPServer 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 3e5abdf9..1f36e543 100644 --- a/pkgs/dart_mcp_server/test/test_harness.dart +++ b/pkgs/dart_mcp_server/test/test_harness.dart @@ -449,12 +449,18 @@ Future _initializeMCPServer( 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(