Skip to content

Commit 5c61472

Browse files
authored
fix(mcp): stdio messages (#1534)
* fix: removed logger boilerplate for non-error messages * fix: tests do not check log message * fix: refactored error messages to stderr apart from CallToolResult * fix: tests to handle mcp command failures * refactor: removed logger from mcp server
1 parent c81787b commit 5c61472

5 files changed

Lines changed: 79 additions & 210 deletions

File tree

lib/src/command_runner.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class VeryGoodCommandRunner extends CompletionCommandRunner<int> {
3838
addCommand(TestCommand(logger: _logger));
3939
addCommand(UpdateCommand(logger: _logger, pubUpdater: pubUpdater));
4040
addCommand(DartCommand(logger: _logger));
41-
addCommand(MCPCommand(logger: _logger));
41+
addCommand(MCPCommand());
4242
}
4343

4444
/// Standard timeout duration for the CLI.

lib/src/mcp/mcp_command.dart

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@ import 'dart:io';
44
import 'package:args/command_runner.dart';
55
import 'package:dart_mcp/server.dart';
66
import 'package:dart_mcp/stdio.dart';
7-
import 'package:mason/mason.dart';
7+
import 'package:mason/mason.dart' show ExitCode;
88
import 'package:stream_channel/stream_channel.dart';
99
import 'package:very_good_cli/src/mcp/mcp_server.dart';
1010

1111
/// Type definition for a factory that creates a [VeryGoodMCPServer].
1212
typedef ServerFactory =
1313
MCPServer Function({
1414
required StreamChannel<String> channel,
15-
required Logger logger,
1615
});
1716

1817
/// Factory function to create a [StreamChannel] from input and output streams.
@@ -29,11 +28,9 @@ StreamChannel<String> _defaultChannelFactory() {
2928
class MCPCommand extends Command<int> {
3029
/// {@macro mcp_command}
3130
MCPCommand({
32-
Logger? logger,
3331
ChannelFactory? channelFactory,
3432
ServerFactory? serverFactory,
35-
}) : _logger = logger ?? Logger(),
36-
_channelFactory = channelFactory ?? _defaultChannelFactory,
33+
}) : _channelFactory = channelFactory ?? _defaultChannelFactory,
3734
_serverFactory = serverFactory ?? VeryGoodMCPServer.new;
3835

3936
/// The [name] of the command. But static.
@@ -46,49 +43,22 @@ Start the MCP (Model Context Protocol) server. WARNING: This is an experimental
4643
@override
4744
String get name => commandName;
4845

49-
final Logger _logger;
50-
5146
final ChannelFactory _channelFactory;
5247

5348
final ServerFactory _serverFactory;
5449

5550
@override
5651
Future<int> run() async {
5752
try {
58-
_logger
59-
..info('Starting Very Good CLI MCP Server...')
60-
..info('Server will listen on stdin/stdout for MCP protocol messages');
61-
62-
// Create a channel from stdin/stdout using the stdio helper
6353
final channel = _channelFactory();
64-
65-
// Create and start the MCP server
66-
final server = _serverFactory(channel: channel, logger: _logger);
67-
68-
_logger
69-
..info('MCP Server started successfully')
70-
..info('Available tools:')
71-
..info('''
72-
- create: Create a very good Dart or Flutter project in seconds based on the provided template. Each template has a corresponding sub-command.''')
73-
..info(' - test: Run tests in a Dart or Flutter project.')
74-
..info('''
75-
- packages_get: Install or update Dart/Flutter package dependencies.
76-
Use after creating a project or modifying pubspec.yaml.
77-
Supports recursive installation and package exclusion.''')
78-
..info('''
79-
- packages_check_licenses: Verify package licenses for compliance and validation in a Dart or Flutter project.
80-
Identifies license types (MIT, BSD, Apache, etc.) for all
81-
dependencies. Use to ensure license compatibility.''');
82-
83-
// Wait for the server to complete
84-
// (this will block until the connection is closed)
54+
final server = _serverFactory(channel: channel);
8555
await server.done;
8656

8757
return ExitCode.success.code;
8858
} on Exception catch (e, stackTrace) {
89-
_logger
90-
..err('Failed to start MCP server: $e')
91-
..err('Stack trace: $stackTrace');
59+
stderr
60+
..writeln('[very_good_mcp] Failed to start MCP server: $e')
61+
..writeln('[very_good_mcp] Stack trace: $stackTrace');
9262
return ExitCode.software.code;
9363
}
9464
}

lib/src/mcp/mcp_server.dart

Lines changed: 49 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:async';
2+
import 'dart:io' show stderr;
23

34
import 'package:args/command_runner.dart';
45
import 'package:dart_mcp/server.dart';
@@ -20,8 +21,7 @@ final class VeryGoodMCPServer extends MCPServer with ToolsSupport {
2021
required StreamChannel<String> channel,
2122
Logger? logger,
2223
VeryGoodCommandRunner? commandRunner,
23-
}) : _logger = logger ?? Logger(),
24-
_commandRunner =
24+
}) : _commandRunner =
2525
commandRunner ?? VeryGoodCommandRunner(logger: logger ?? Logger()),
2626
super.fromStreamChannel(
2727
channel,
@@ -34,8 +34,6 @@ final class VeryGoodMCPServer extends MCPServer with ToolsSupport {
3434
'for creating and managing Dart/Flutter projects.',
3535
);
3636

37-
final Logger _logger;
38-
3937
final VeryGoodCommandRunner _commandRunner;
4038

4139
@override
@@ -414,52 +412,19 @@ Only one value can be selected.
414412
Future<CallToolResult> _handleCreate(CallToolRequest request) async {
415413
final args = request.arguments ?? {};
416414
final cliArgs = _parseCreate(args);
417-
final exitCode = await _runCommand(cliArgs);
418-
419-
return CallToolResult(
420-
content: [
421-
TextContent(
422-
text: exitCode == ExitCode.success.code
423-
? 'Project created successfully'
424-
: 'Failed to create project',
425-
),
426-
],
427-
isError: exitCode != ExitCode.success.code,
428-
);
415+
return _runToolCommand(cliArgs, toolName: 'create');
429416
}
430417

431418
Future<CallToolResult> _handleTest(CallToolRequest request) async {
432419
final args = request.arguments ?? {};
433420
final cliArgs = _parseTest(args);
434-
final exitCode = await _runCommand(cliArgs);
435-
436-
return CallToolResult(
437-
content: [
438-
TextContent(
439-
text: exitCode == ExitCode.success.code
440-
? 'Tests completed successfully'
441-
: 'Tests failed',
442-
),
443-
],
444-
isError: exitCode != ExitCode.success.code,
445-
);
421+
return _runToolCommand(cliArgs, toolName: 'test');
446422
}
447423

448424
Future<CallToolResult> _handlePackagesGet(CallToolRequest request) async {
449425
final args = request.arguments ?? {};
450426
final cliArgs = _parsePackagesGet(args);
451-
final exitCode = await _runCommand(cliArgs);
452-
453-
return CallToolResult(
454-
content: [
455-
TextContent(
456-
text: exitCode == ExitCode.success.code
457-
? 'Packages retrieved successfully'
458-
: 'Failed to get packages',
459-
),
460-
],
461-
isError: exitCode != ExitCode.success.code,
462-
);
427+
return _runToolCommand(cliArgs, toolName: 'packages get');
463428
}
464429

465430
Future<CallToolResult> _handlePackagesCheck(CallToolRequest request) async {
@@ -483,37 +448,57 @@ Only one value can be selected.
483448
}
484449

485450
final cliArgs = _parsePackagesCheck(args);
486-
final exitCode = await _runCommand(cliArgs);
487-
488-
return CallToolResult(
489-
content: [
490-
TextContent(
491-
text: exitCode == ExitCode.success.code
492-
? 'Package license check completed successfully'
493-
: 'Package license check failed',
494-
),
495-
],
496-
isError: exitCode != ExitCode.success.code,
497-
);
451+
return _runToolCommand(cliArgs, toolName: 'packages check licenses');
498452
}
499453

500-
/// Runs CLI commands through the command runner.
501-
/// Commands parse their own arguments using their argParser.
502-
Future<int> _runCommand(List<String> args) async {
503-
try {
504-
_logger.detail('Running: very_good ${args.join(' ')}');
454+
/// Runs a CLI command and returns a [CallToolResult] with descriptive
455+
/// error messages including the command that was run and the exit code.
456+
Future<CallToolResult> _runToolCommand(
457+
List<String> args, {
458+
required String toolName,
459+
}) async {
460+
final commandString = 'very_good ${args.join(' ')}';
505461

462+
try {
506463
final exitCode = await _commandRunner.run(args);
507464

508-
return exitCode;
465+
if (exitCode == ExitCode.success.code) {
466+
return CallToolResult(
467+
content: [
468+
TextContent(text: '"$toolName" completed successfully.'),
469+
],
470+
isError: false,
471+
);
472+
}
473+
474+
final message =
475+
'"$toolName" failed with exit code $exitCode.\n'
476+
'Command: $commandString';
477+
stderr.writeln('[very_good_mcp] $message');
478+
return CallToolResult(
479+
content: [TextContent(text: message)],
480+
isError: true,
481+
);
509482
} on UsageException catch (e) {
510-
_logger.err('Usage error: ${e.message}');
511-
return ExitCode.usage.code;
483+
final message =
484+
'"$toolName" usage error: ${e.message}\n'
485+
'Command: $commandString';
486+
stderr.writeln('[very_good_mcp] $message');
487+
return CallToolResult(
488+
content: [TextContent(text: message)],
489+
isError: true,
490+
);
512491
} on Exception catch (e, stackTrace) {
513-
_logger
514-
..err('Command error: $e')
515-
..err('Stack trace: $stackTrace');
516-
return ExitCode.software.code;
492+
final message =
493+
'"$toolName" threw an exception: $e\n'
494+
'Command: $commandString';
495+
stderr
496+
..writeln('[very_good_mcp] $message')
497+
..writeln('[very_good_mcp] Stack trace: $stackTrace');
498+
return CallToolResult(
499+
content: [TextContent(text: message)],
500+
isError: true,
501+
);
517502
}
518503
}
519504
}

0 commit comments

Comments
 (0)