Skip to content

Add --log-file argument to log all protocol traffic to a file #176

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 7 commits into from
Jun 18, 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
5 changes: 5 additions & 0 deletions pkgs/dart_mcp/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.2.2-wip

- Move the `done` future from the `ServerConnection` into `MCPBase` so it is
available to the `MPCServer` class as well.

## 0.2.1

- Fix the `protocolLogSink` support when using `MCPClient.connectStdioServer`.
Expand Down
11 changes: 4 additions & 7 deletions pkgs/dart_mcp/lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ base class MCPClient {
///
/// 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.
/// 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, {
Expand Down Expand Up @@ -90,7 +91,8 @@ base class MCPClient {
///
/// 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.
/// and `>>>` preceding outgoing messages. It is the responsibility of the
/// caller to close this sink.
ServerConnection connectServer(
StreamChannel<String> channel, {
Sink<String>? protocolLogSink,
Expand Down Expand Up @@ -187,10 +189,6 @@ base class ServerConnection extends MCPBase {
final _logController =
StreamController<LoggingMessageNotification>.broadcast();

/// Completes when [shutdown] is called.
Future<void> get done => _done.future;
final Completer<void> _done = Completer<void>();

/// A 1:1 connection from a client to a server using [channel].
///
/// If the client supports "roots", then it should provide an implementation
Expand Down Expand Up @@ -256,7 +254,6 @@ base class ServerConnection extends MCPBase {
_resourceUpdatedController.close(),
_logController.close(),
]);
_done.complete();
}

/// Called after a successful call to [initialize].
Expand Down
12 changes: 11 additions & 1 deletion pkgs/dart_mcp/lib/src/shared.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ base class MCPBase {
/// Whether the connection with the peer is active.
bool get isActive => !_peer.isClosed;

/// Completes after [shutdown] is called.
Future<void> get done => _done.future;
final _done = Completer<void>();

/// Initializes an MCP connection on [channel].
///
/// If [protocolLogSink] is provided, all incoming and outgoing messages will
/// added logged to it. It is the responsibility of the caller to close the
/// sink.
MCPBase(StreamChannel<String> channel, {Sink<String>? protocolLogSink}) {
_peer = Peer(_maybeForwardMessages(channel, protocolLogSink));
registerNotificationHandler(
Expand All @@ -48,7 +57,7 @@ base class MCPBase {

registerRequestHandler(PingRequest.methodName, _handlePing);

_peer.listen();
_peer.listen().whenComplete(shutdown);
}

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

/// Registers a handler for the method [name] on this server.
Expand Down
2 changes: 1 addition & 1 deletion pkgs/dart_mcp/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: dart_mcp
version: 0.2.1
version: 0.2.2-wip
description: A package for making MCP servers and clients.
repository: https://github.com/dart-lang/ai/tree/main/pkgs/dart_mcp
issue_tracker: https://github.com/dart-lang/ai/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Adart_mcp
Expand Down
1 change: 1 addition & 0 deletions pkgs/dart_mcp_server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@
* Add the beginnings of a Dart tooling MCP server.
* Instruct clients to prefer MCP tools over running tools in the shell.
* Reduce output size of `run_tests` tool to save on input tokens.
* Add `--log-file` argument to log all protocol traffic to a file.
34 changes: 33 additions & 1 deletion pkgs/dart_mcp_server/bin/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ void main(List<String> args) async {
final flutterSdkPath =
parsedArgs.option(flutterSdkOption) ??
io.Platform.environment['FLUTTER_SDK'];
final logFilePath = parsedArgs.option(logFileOption);
final logFileSink =
logFilePath == null ? null : createLogSink(io.File(logFilePath));
runZonedGuarded(
() {
server = DartMCPServer(
Expand All @@ -40,7 +43,8 @@ void main(List<String> args) async {
),
forceRootsFallback: parsedArgs.flag(forceRootsFallback),
sdk: Sdk.find(dartSdkPath: dartSdkPath, flutterSdkPath: flutterSdkPath),
);
protocolLogSink: logFileSink,
)..done.whenComplete(() => logFileSink?.close());
},
(e, s) {
if (server != null) {
Expand Down Expand Up @@ -94,9 +98,37 @@ final argParser =
'cursor which claim to have roots support but do not actually '
'support it.',
)
..addOption(
logFileOption,
help:
'Path to a file to log all MPC protocol traffic to. File will be '
'overwritten if it exists.',
)
..addFlag(help, abbr: 'h', help: 'Show usage text');

const dartSdkOption = 'dart-sdk';
const flutterSdkOption = 'flutter-sdk';
const forceRootsFallback = 'force-roots-fallback';
const help = 'help';
const logFileOption = 'log-file';

/// Creates a `Sink<String>` for [logFile].
Sink<String> createLogSink(io.File logFile) {
logFile.createSync(recursive: true);
final fileByteSink = logFile.openWrite(
mode: io.FileMode.write,
encoding: utf8,
);
return fileByteSink.transform(
StreamSinkTransformer.fromHandlers(
handleData: (data, innerSink) {
innerSink.add(utf8.encode(data));
// It's a log, so we want to make sure it's always up-to-date.
fileByteSink.flush();
},
handleDone: (innerSink) {
innerSink.close();
},
),
);
}
1 change: 1 addition & 0 deletions pkgs/dart_mcp_server/lib/src/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ final class DartMCPServer extends MCPServer
@visibleForTesting this.processManager = const LocalProcessManager(),
@visibleForTesting this.fileSystem = const LocalFileSystem(),
this.forceRootsFallback = false,
super.protocolLogSink,
}) : super.fromStreamChannel(
implementation: ServerImplementation(
name: 'dart and flutter tooling',
Expand Down
35 changes: 33 additions & 2 deletions pkgs/dart_mcp_server/test/dart_tooling_mcp_server_test.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
// 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:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart' as d;

import 'test_harness.dart';

void main() {
// TODO: write tests for any Dart Tooling MCP Server functionality that is
// not covered by individual feature tests.
group('--log-file', () {
late d.FileDescriptor logDescriptor;
late TestHarness testHarness;

setUp(() async {
logDescriptor = d.file('log.txt');
testHarness = await TestHarness.start(
inProcess: false,
cliArgs: ['--log-file', logDescriptor.io.path],
);
});

test('logs traffic to a file', () async {
expect(
await File(logDescriptor.io.path).readAsLines(),
containsAll([
allOf(startsWith('<<<'), contains('"method":"initialize"')),
allOf(startsWith('>>>'), contains('"serverInfo"')),
allOf(startsWith('<<<'), contains('"notifications/initialized"')),
]),
);
// Ensure the file handle is released before the file is cleaned up.
await testHarness.serverConnectionPair.serverConnection.shutdown();
});
});
}
10 changes: 10 additions & 0 deletions pkgs/dart_mcp_server/test/test_harness.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,14 @@ class TestHarness {
/// MCP server is ran in process.
///
/// Use [startDebugSession] to start up apps and connect to them.
///
/// If [cliArgs] are passed, they will be given to the MCP server. This is
/// only supported when [inProcess] is `false`, which is enforced via
/// assertions.
static Future<TestHarness> start({
bool inProcess = false,
FileSystem? fileSystem,
List<String> cliArgs = const [],
}) async {
final sdk = Sdk.find(
dartSdkPath: Platform.environment['DART_SDK'],
Expand All @@ -82,6 +87,7 @@ class TestHarness {
inProcess,
fileSystem,
sdk,
cliArgs,
);
final connection = serverConnectionPair.serverConnection;
connection.onLog.listen((log) {
Expand Down Expand Up @@ -388,10 +394,13 @@ Future<ServerConnectionPair> _initializeMCPServer(
bool inProcess,
FileSystem fileSystem,
Sdk sdk,
List<String> cliArgs,
) async {
ServerConnection connection;
DartMCPServer? server;
if (inProcess) {
assert(cliArgs.isEmpty);

/// The client side of the communication channel - the stream is the
/// incoming data and the sink is outgoing data.
final clientController = StreamController<String>();
Expand Down Expand Up @@ -421,6 +430,7 @@ Future<ServerConnectionPair> _initializeMCPServer(
'pub', // Using `pub` gives us incremental compilation
'run',
'bin/main.dart',
...cliArgs,
]);
}

Expand Down
Loading