Skip to content

Commit 885a4c5

Browse files
authored
Add --log-file argument to log all protocol traffic to a file (#176)
Closes #171 Also ensures that `shutdown` is called when the peer connection closes, and exposes a `done` future on the MCPServer class.
1 parent 7ca3eba commit 885a4c5

File tree

9 files changed

+99
-12
lines changed

9 files changed

+99
-12
lines changed

pkgs/dart_mcp/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.2.2-wip
2+
3+
- Move the `done` future from the `ServerConnection` into `MCPBase` so it is
4+
available to the `MPCServer` class as well.
5+
16
## 0.2.1
27

38
- Fix the `protocolLogSink` support when using `MCPClient.connectStdioServer`.

pkgs/dart_mcp/lib/src/client/client.dart

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ base class MCPClient {
5454
///
5555
/// If [protocolLogSink] is provided, all messages sent between the client and
5656
/// server will be forwarded to that [Sink] as well, with `<<<` preceding
57-
/// incoming messages and `>>>` preceding outgoing messages.
57+
/// incoming messages and `>>>` preceding outgoing messages. It is the
58+
/// responsibility of the caller to close this sink.
5859
Future<ServerConnection> connectStdioServer(
5960
String command,
6061
List<String> arguments, {
@@ -90,7 +91,8 @@ base class MCPClient {
9091
///
9192
/// If [protocolLogSink] is provided, all messages sent on [channel] will be
9293
/// forwarded to that [Sink] as well, with `<<<` preceding incoming messages
93-
/// and `>>>` preceding outgoing messages.
94+
/// and `>>>` preceding outgoing messages. It is the responsibility of the
95+
/// caller to close this sink.
9496
ServerConnection connectServer(
9597
StreamChannel<String> channel, {
9698
Sink<String>? protocolLogSink,
@@ -187,10 +189,6 @@ base class ServerConnection extends MCPBase {
187189
final _logController =
188190
StreamController<LoggingMessageNotification>.broadcast();
189191

190-
/// Completes when [shutdown] is called.
191-
Future<void> get done => _done.future;
192-
final Completer<void> _done = Completer<void>();
193-
194192
/// A 1:1 connection from a client to a server using [channel].
195193
///
196194
/// If the client supports "roots", then it should provide an implementation
@@ -256,7 +254,6 @@ base class ServerConnection extends MCPBase {
256254
_resourceUpdatedController.close(),
257255
_logController.close(),
258256
]);
259-
_done.complete();
260257
}
261258

262259
/// Called after a successful call to [initialize].

pkgs/dart_mcp/lib/src/shared.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ base class MCPBase {
3939
/// Whether the connection with the peer is active.
4040
bool get isActive => !_peer.isClosed;
4141

42+
/// Completes after [shutdown] is called.
43+
Future<void> get done => _done.future;
44+
final _done = Completer<void>();
45+
46+
/// Initializes an MCP connection on [channel].
47+
///
48+
/// If [protocolLogSink] is provided, all incoming and outgoing messages will
49+
/// added logged to it. It is the responsibility of the caller to close the
50+
/// sink.
4251
MCPBase(StreamChannel<String> channel, {Sink<String>? protocolLogSink}) {
4352
_peer = Peer(_maybeForwardMessages(channel, protocolLogSink));
4453
registerNotificationHandler(
@@ -48,7 +57,7 @@ base class MCPBase {
4857

4958
registerRequestHandler(PingRequest.methodName, _handlePing);
5059

51-
_peer.listen();
60+
_peer.listen().whenComplete(shutdown);
5261
}
5362

5463
/// Handles cleanup of all streams and other resources on shutdown.
@@ -60,6 +69,7 @@ base class MCPBase {
6069
await Future.wait([
6170
for (var controller in progressControllers) controller.close(),
6271
]);
72+
if (!_done.isCompleted) _done.complete();
6373
}
6474

6575
/// Registers a handler for the method [name] on this server.

pkgs/dart_mcp/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: dart_mcp
2-
version: 0.2.1
2+
version: 0.2.2-wip
33
description: A package for making MCP servers and clients.
44
repository: https://github.com/dart-lang/ai/tree/main/pkgs/dart_mcp
55
issue_tracker: https://github.com/dart-lang/ai/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Adart_mcp

pkgs/dart_mcp_server/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@
3939
* Add the beginnings of a Dart tooling MCP server.
4040
* Instruct clients to prefer MCP tools over running tools in the shell.
4141
* Reduce output size of `run_tests` tool to save on input tokens.
42+
* Add `--log-file` argument to log all protocol traffic to a file.

pkgs/dart_mcp_server/bin/main.dart

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ void main(List<String> args) async {
2525
final flutterSdkPath =
2626
parsedArgs.option(flutterSdkOption) ??
2727
io.Platform.environment['FLUTTER_SDK'];
28+
final logFilePath = parsedArgs.option(logFileOption);
29+
final logFileSink =
30+
logFilePath == null ? null : createLogSink(io.File(logFilePath));
2831
runZonedGuarded(
2932
() {
3033
server = DartMCPServer(
@@ -40,7 +43,8 @@ void main(List<String> args) async {
4043
),
4144
forceRootsFallback: parsedArgs.flag(forceRootsFallback),
4245
sdk: Sdk.find(dartSdkPath: dartSdkPath, flutterSdkPath: flutterSdkPath),
43-
);
46+
protocolLogSink: logFileSink,
47+
)..done.whenComplete(() => logFileSink?.close());
4448
},
4549
(e, s) {
4650
if (server != null) {
@@ -94,9 +98,37 @@ final argParser =
9498
'cursor which claim to have roots support but do not actually '
9599
'support it.',
96100
)
101+
..addOption(
102+
logFileOption,
103+
help:
104+
'Path to a file to log all MPC protocol traffic to. File will be '
105+
'overwritten if it exists.',
106+
)
97107
..addFlag(help, abbr: 'h', help: 'Show usage text');
98108

99109
const dartSdkOption = 'dart-sdk';
100110
const flutterSdkOption = 'flutter-sdk';
101111
const forceRootsFallback = 'force-roots-fallback';
102112
const help = 'help';
113+
const logFileOption = 'log-file';
114+
115+
/// Creates a `Sink<String>` for [logFile].
116+
Sink<String> createLogSink(io.File logFile) {
117+
logFile.createSync(recursive: true);
118+
final fileByteSink = logFile.openWrite(
119+
mode: io.FileMode.write,
120+
encoding: utf8,
121+
);
122+
return fileByteSink.transform(
123+
StreamSinkTransformer.fromHandlers(
124+
handleData: (data, innerSink) {
125+
innerSink.add(utf8.encode(data));
126+
// It's a log, so we want to make sure it's always up-to-date.
127+
fileByteSink.flush();
128+
},
129+
handleDone: (innerSink) {
130+
innerSink.close();
131+
},
132+
),
133+
);
134+
}

pkgs/dart_mcp_server/lib/src/server.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ final class DartMCPServer extends MCPServer
3838
@visibleForTesting this.processManager = const LocalProcessManager(),
3939
@visibleForTesting this.fileSystem = const LocalFileSystem(),
4040
this.forceRootsFallback = false,
41+
super.protocolLogSink,
4142
}) : super.fromStreamChannel(
4243
implementation: ServerImplementation(
4344
name: 'dart and flutter tooling',
Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,38 @@
11
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
import 'package:test/test.dart';
8+
import 'package:test_descriptor/test_descriptor.dart' as d;
9+
10+
import 'test_harness.dart';
11+
412
void main() {
5-
// TODO: write tests for any Dart Tooling MCP Server functionality that is
6-
// not covered by individual feature tests.
13+
group('--log-file', () {
14+
late d.FileDescriptor logDescriptor;
15+
late TestHarness testHarness;
16+
17+
setUp(() async {
18+
logDescriptor = d.file('log.txt');
19+
testHarness = await TestHarness.start(
20+
inProcess: false,
21+
cliArgs: ['--log-file', logDescriptor.io.path],
22+
);
23+
});
24+
25+
test('logs traffic to a file', () async {
26+
expect(
27+
await File(logDescriptor.io.path).readAsLines(),
28+
containsAll([
29+
allOf(startsWith('<<<'), contains('"method":"initialize"')),
30+
allOf(startsWith('>>>'), contains('"serverInfo"')),
31+
allOf(startsWith('<<<'), contains('"notifications/initialized"')),
32+
]),
33+
);
34+
// Ensure the file handle is released before the file is cleaned up.
35+
await testHarness.serverConnectionPair.serverConnection.shutdown();
36+
});
37+
});
738
}

pkgs/dart_mcp_server/test/test_harness.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,14 @@ class TestHarness {
6464
/// MCP server is ran in process.
6565
///
6666
/// Use [startDebugSession] to start up apps and connect to them.
67+
///
68+
/// If [cliArgs] are passed, they will be given to the MCP server. This is
69+
/// only supported when [inProcess] is `false`, which is enforced via
70+
/// assertions.
6771
static Future<TestHarness> start({
6872
bool inProcess = false,
6973
FileSystem? fileSystem,
74+
List<String> cliArgs = const [],
7075
}) async {
7176
final sdk = Sdk.find(
7277
dartSdkPath: Platform.environment['DART_SDK'],
@@ -82,6 +87,7 @@ class TestHarness {
8287
inProcess,
8388
fileSystem,
8489
sdk,
90+
cliArgs,
8591
);
8692
final connection = serverConnectionPair.serverConnection;
8793
connection.onLog.listen((log) {
@@ -388,10 +394,13 @@ Future<ServerConnectionPair> _initializeMCPServer(
388394
bool inProcess,
389395
FileSystem fileSystem,
390396
Sdk sdk,
397+
List<String> cliArgs,
391398
) async {
392399
ServerConnection connection;
393400
DartMCPServer? server;
394401
if (inProcess) {
402+
assert(cliArgs.isEmpty);
403+
395404
/// The client side of the communication channel - the stream is the
396405
/// incoming data and the sink is outgoing data.
397406
final clientController = StreamController<String>();
@@ -421,6 +430,7 @@ Future<ServerConnectionPair> _initializeMCPServer(
421430
'pub', // Using `pub` gives us incremental compilation
422431
'run',
423432
'bin/main.dart',
433+
...cliArgs,
424434
]);
425435
}
426436

0 commit comments

Comments
 (0)