Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dart_mcp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 8 additions & 3 deletions mcp_examples/bin/workflow_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
9 changes: 6 additions & 3 deletions pkgs/dart_mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions pkgs/dart_mcp/dart_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
override_platforms:
chrome:
settings:
headless: true
10 changes: 9 additions & 1 deletion pkgs/dart_mcp/example/simple_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,25 @@
// 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 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');
Expand Down
33 changes: 13 additions & 20 deletions pkgs/dart_mcp/lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -50,29 +48,21 @@ base class MCPClient {
@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(
Expand All @@ -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
Expand Down
21 changes: 10 additions & 11 deletions pkgs/dart_mcp/test/server/resources_support_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();

Expand All @@ -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<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) {
Expand All @@ -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]!,
),
],
);
Expand Down
8 changes: 7 additions & 1 deletion pkgs/dart_mcp_server/test/test_harness.dart
Original file line number Diff line number Diff line change
Expand Up @@ -449,12 +449,18 @@ Future<ServerConnectionPair> _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(
Expand Down