diff --git a/packages/mcp_dart_cli/CHANGELOG.md b/packages/mcp_dart_cli/CHANGELOG.md index 72c296d..a3ec536 100644 --- a/packages/mcp_dart_cli/CHANGELOG.md +++ b/packages/mcp_dart_cli/CHANGELOG.md @@ -1,5 +1,12 @@ +## 0.1.5 + +- **`update` command**: + - Update the CLI to the latest version via `mcp_dart update`. + - Automatic update checks on command execution. + ## 0.1.4 + - **`create` command**: - Improved package name inference when creating a project from a path (e.g. `mcp_dart create ./my-project`). - Internal refactoring for better testability. diff --git a/packages/mcp_dart_cli/README.md b/packages/mcp_dart_cli/README.md index 92c8e41..0f03d83 100644 --- a/packages/mcp_dart_cli/README.md +++ b/packages/mcp_dart_cli/README.md @@ -52,6 +52,7 @@ mcp_dart create --template https://github.com/leehack/mcp_dart/tr - `serve`: Runs the MCP server in the current directory. - `doctor`: Checks the project for common issues and verifies connectivity. - `inspect`: Interacts with an MCP server (local or external). +- `update`: Updates the CLI to the latest version. ### Doctor @@ -137,6 +138,14 @@ mcp_dart serve - `--port` (`-p`): Port for HTTP transport. Defaults to `3000`. - `--watch`: Restart the server on file changes. +### Update + +Updates the CLI to the latest version. + +```bash +mcp_dart update +``` + ## Running Tests To run the tests for this package: diff --git a/packages/mcp_dart_cli/bin/mcp_dart.dart b/packages/mcp_dart_cli/bin/mcp_dart.dart index 6635401..61f3de8 100644 --- a/packages/mcp_dart_cli/bin/mcp_dart.dart +++ b/packages/mcp_dart_cli/bin/mcp_dart.dart @@ -5,8 +5,16 @@ import 'package:mcp_dart_cli/src/create_command.dart'; import 'package:mcp_dart_cli/src/serve_command.dart'; import 'package:mcp_dart_cli/src/doctor_command.dart'; import 'package:mcp_dart_cli/src/inspect_command.dart'; +import 'package:mcp_dart_cli/src/update_command.dart'; +import 'package:mcp_dart_cli/src/version.dart'; +import 'package:mcp_dart_cli/src/version_check.dart'; void main(List arguments) async { + if (arguments.contains('--version') || arguments.contains('-v')) { + stdout.writeln(packageVersion); + exit(0); + } + final logger = Logger(); final runner = CommandRunner( 'mcp_dart', @@ -15,10 +23,14 @@ void main(List arguments) async { ..addCommand(CreateCommand()) ..addCommand(ServeCommand()) ..addCommand(DoctorCommand()) - ..addCommand(InspectCommand(logger: logger)); + ..addCommand(InspectCommand(logger: logger)) + ..addCommand(UpdateCommand(logger: logger)); try { final exitCode = await runner.run(arguments); + if (!arguments.contains('update')) { + await checkForUpdate(logger); + } exit(exitCode ?? 0); } catch (e) { stderr.writeln(e); diff --git a/packages/mcp_dart_cli/dart_test.yaml b/packages/mcp_dart_cli/dart_test.yaml new file mode 100644 index 0000000..6d8e711 --- /dev/null +++ b/packages/mcp_dart_cli/dart_test.yaml @@ -0,0 +1 @@ +concurrency: 1 diff --git a/packages/mcp_dart_cli/example/example.md b/packages/mcp_dart_cli/example/example.md index 427ad48..44c8f42 100644 --- a/packages/mcp_dart_cli/example/example.md +++ b/packages/mcp_dart_cli/example/example.md @@ -147,3 +147,11 @@ mcp_dart inspect --url http://localhost:3000/mcp ```bash mcp_dart inspect --env API_KEY=your_key --env DEBUG=true -- python my_server.py ``` + +## Updating the CLI + +Update to the latest version of `mcp_dart_cli`: + +```bash +mcp_dart update +``` diff --git a/packages/mcp_dart_cli/lib/src/update_command.dart b/packages/mcp_dart_cli/lib/src/update_command.dart new file mode 100644 index 0000000..4f3429b --- /dev/null +++ b/packages/mcp_dart_cli/lib/src/update_command.dart @@ -0,0 +1,57 @@ +import 'package:args/command_runner.dart'; +import 'package:mason_logger/mason_logger.dart'; +import 'package:mcp_dart_cli/src/version.dart'; +import 'package:pub_updater/pub_updater.dart'; + +/// {@template update_command} +/// A command which updates the CLI. +/// {@endtemplate} +class UpdateCommand extends Command { + /// {@macro update_command} + UpdateCommand({ + required Logger logger, + PubUpdater? pubUpdater, + }) : _logger = logger, + _pubUpdater = pubUpdater ?? PubUpdater(); + + final Logger _logger; + final PubUpdater _pubUpdater; + + @override + String get description => 'Update the CLI.'; + + @override + String get name => 'update'; + + @override + Future run() async { + final updateCheckProgress = _logger.progress('Checking for updates'); + late final String latestVersion; + try { + latestVersion = await _pubUpdater.getLatestVersion('mcp_dart_cli'); + } catch (error) { + updateCheckProgress.fail(); + _logger.err('$error'); + return ExitCode.software.code; + } + updateCheckProgress.complete('Checked for updates'); + + final isUpToDate = packageVersion == latestVersion; + if (isUpToDate) { + _logger.info('CLI is already at the latest version.'); + return ExitCode.success.code; + } + + final updateProgress = _logger.progress('Updating to $latestVersion'); + try { + await _pubUpdater.update(packageName: 'mcp_dart_cli'); + } catch (error) { + updateProgress.fail(); + _logger.err('$error'); + return ExitCode.software.code; + } + updateProgress.complete('Updated to $latestVersion'); + + return ExitCode.success.code; + } +} diff --git a/packages/mcp_dart_cli/lib/src/version.dart b/packages/mcp_dart_cli/lib/src/version.dart new file mode 100644 index 0000000..2d8e96a --- /dev/null +++ b/packages/mcp_dart_cli/lib/src/version.dart @@ -0,0 +1 @@ +const packageVersion = '0.1.5'; diff --git a/packages/mcp_dart_cli/lib/src/version_check.dart b/packages/mcp_dart_cli/lib/src/version_check.dart new file mode 100644 index 0000000..740d8f9 --- /dev/null +++ b/packages/mcp_dart_cli/lib/src/version_check.dart @@ -0,0 +1,23 @@ +import 'package:mason_logger/mason_logger.dart'; +import 'package:pub_updater/pub_updater.dart'; + +import 'version.dart'; + +Future checkForUpdate(Logger logger) async { + try { + final pubUpdater = PubUpdater(); + final isUpToDate = await pubUpdater.isUpToDate( + packageName: 'mcp_dart_cli', + currentVersion: packageVersion, + ); + if (!isUpToDate) { + final latestVersion = await pubUpdater.getLatestVersion('mcp_dart_cli'); + logger.info( + 'New version of mcp_dart_cli available! ($packageVersion -> $latestVersion)\n' + 'Run ${cyan.wrap('dart pub global activate mcp_dart_cli')} to update.', + ); + } + } catch (_) { + // Suppress update check errors + } +} diff --git a/packages/mcp_dart_cli/pubspec.yaml b/packages/mcp_dart_cli/pubspec.yaml index 1a65914..82d8f5f 100644 --- a/packages/mcp_dart_cli/pubspec.yaml +++ b/packages/mcp_dart_cli/pubspec.yaml @@ -1,6 +1,6 @@ name: mcp_dart_cli description: CLI for Model Context Protocol (MCP) servers in Dart. -version: 0.1.4 +version: 0.1.5 repository: https://github.com/leehack/mcp_dart environment: @@ -17,6 +17,7 @@ dependencies: mcp_dart: ^1.1.2 mason_logger: ^0.3.3 meta: ^1.17.0 + pub_updater: ^0.5.0 dependency_overrides: mcp_dart: diff --git a/packages/mcp_dart_cli/test/src/create_command_test.dart b/packages/mcp_dart_cli/test/src/create_command_test.dart index e102505..8c2e57f 100644 --- a/packages/mcp_dart_cli/test/src/create_command_test.dart +++ b/packages/mcp_dart_cli/test/src/create_command_test.dart @@ -52,6 +52,7 @@ void main() { logger = MockLogger(); generator = MockMasonGenerator(); tempDir = Directory.systemTemp.createTempSync('mcp_dart_cli_test'); + tempDir = Directory(tempDir.resolveSymbolicLinksSync()); originalCwd = Directory.current; Directory.current = tempDir; diff --git a/packages/mcp_dart_cli/test/src/doctor_command_test.dart b/packages/mcp_dart_cli/test/src/doctor_command_test.dart index bbd17af..557e1b6 100644 --- a/packages/mcp_dart_cli/test/src/doctor_command_test.dart +++ b/packages/mcp_dart_cli/test/src/doctor_command_test.dart @@ -21,6 +21,7 @@ void main() { command = DoctorCommand(logger: logger); originalCwd = Directory.current; tempDir = Directory.systemTemp.createTempSync('doctor_test_'); + tempDir = Directory(tempDir.resolveSymbolicLinksSync()); Directory.current = tempDir; }); diff --git a/packages/mcp_dart_cli/test/src/serve_command_test.dart b/packages/mcp_dart_cli/test/src/serve_command_test.dart index 2670e0c..db536a2 100644 --- a/packages/mcp_dart_cli/test/src/serve_command_test.dart +++ b/packages/mcp_dart_cli/test/src/serve_command_test.dart @@ -28,29 +28,31 @@ void main() { equals('Runs the MCP server in the current directory.')); }); - test('fails if pubspec.yaml is missing', () async { - final runner = CommandRunner('mcp_dart', 'CLI')..addCommand(command); - - await IOOverrides.runZoned( - () async { - final tempDir = Directory.systemTemp.createTempSync(); - addTearDown(() => tempDir.deleteSync(recursive: true)); - - // We can't easily change Directory.current for the *code under test* unless we spawn a process or use IOOverrides to intercept File calls? - // No, IOOverrides intercepts `File()` but `File('foo')` still resolves relative to `Directory.current`. - - // Actually, `Directory.current` is settable. - final originalCwd = Directory.current; - Directory.current = tempDir; - addTearDown(() => Directory.current = originalCwd); - - final exitCode = await runner.run(['serve']); - - expect(exitCode, equals(ExitCode.usage.code)); - verify(() => logger.err( - 'Error: pubspec.yaml not found in current directory.')).called(1); - }, - ); + group('with temp directory', () { + late Directory tempDir; + late Directory originalCwd; + + setUp(() { + originalCwd = Directory.current; + tempDir = Directory.systemTemp.createTempSync(); + tempDir = Directory(tempDir.resolveSymbolicLinksSync()); + Directory.current = tempDir; + }); + + tearDown(() { + Directory.current = originalCwd; + tempDir.deleteSync(recursive: true); + }); + + test('fails if pubspec.yaml is missing', () async { + final runner = CommandRunner('mcp_dart', 'CLI') + ..addCommand(command); + final exitCode = await runner.run(['serve']); + + expect(exitCode, equals(ExitCode.usage.code)); + verify(() => logger.err( + 'Error: pubspec.yaml not found in current directory.')).called(1); + }); }); }); } diff --git a/packages/mcp_dart_cli/test/src/update_command_test.dart b/packages/mcp_dart_cli/test/src/update_command_test.dart new file mode 100644 index 0000000..41fe3cc --- /dev/null +++ b/packages/mcp_dart_cli/test/src/update_command_test.dart @@ -0,0 +1,95 @@ +import 'dart:io'; + +import 'package:mason_logger/mason_logger.dart'; +import 'package:mcp_dart_cli/src/update_command.dart'; +import 'package:mcp_dart_cli/src/version.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:pub_updater/pub_updater.dart'; +import 'package:test/test.dart'; + +class MockLogger extends Mock implements Logger {} + +class MockPubUpdater extends Mock implements PubUpdater {} + +class MockProgress extends Mock implements Progress {} + +void main() { + group('UpdateCommand', () { + late Logger logger; + late PubUpdater pubUpdater; + late UpdateCommand command; + late Progress progress; + + setUp(() { + logger = MockLogger(); + pubUpdater = MockPubUpdater(); + progress = MockProgress(); + command = UpdateCommand(logger: logger, pubUpdater: pubUpdater); + + when(() => logger.progress(any())).thenReturn(progress); + when(() => pubUpdater.getLatestVersion(any())) + .thenAnswer((_) async => packageVersion); + when(() => pubUpdater.update(packageName: any(named: 'packageName'))) + .thenAnswer((_) async => ProcessResult(0, 0, '', '')); + }); + + test('can be instantiated', () { + expect(command, isA()); + }); + + test('handles software error when checking for updates fails', () async { + when(() => pubUpdater.getLatestVersion(any())) + .thenThrow(Exception('oops')); + + final result = await command.run(); + + expect(result, equals(ExitCode.software.code)); + verify(() => logger.err('Exception: oops')).called(1); + verify(() => progress.fail()).called(1); + }); + + test('handles software error when update fails', () async { + when(() => pubUpdater.getLatestVersion(any())) + .thenAnswer((_) async => '9.9.9'); + when(() => pubUpdater.update(packageName: any(named: 'packageName'))) + .thenThrow(Exception('oops')); + + final result = await command.run(); + + expect(result, equals(ExitCode.software.code)); + verify(() => logger.err('Exception: oops')).called(1); + verify(() => progress.fail()).called(1); + }); + + test('logs message when already at latest version', () async { + when(() => pubUpdater.getLatestVersion(any())) + .thenAnswer((_) async => packageVersion); + + final result = await command.run(); + + expect(result, equals(ExitCode.success.code)); + verify(() => logger.info('CLI is already at the latest version.')) + .called(1); + verifyNever( + () => pubUpdater.update(packageName: any(named: 'packageName'))); + }); + + test('updates to latest version', () async { + when(() => pubUpdater.isUpToDate( + packageName: any(named: 'packageName'), + currentVersion: any(named: 'currentVersion'), + )).thenAnswer((_) async => false); + when(() => pubUpdater.getLatestVersion(any())) + .thenAnswer((_) async => '9.9.9'); + when(() => pubUpdater.update(packageName: any(named: 'packageName'))) + .thenAnswer((_) async => ProcessResult(0, 0, '', '')); + + final result = await command.run(); + + expect(result, equals(ExitCode.success.code)); + verify(() => logger.progress('Updating to 9.9.9')).called(1); + verify(() => pubUpdater.update(packageName: 'mcp_dart_cli')).called(1); + verify(() => progress.complete('Updated to 9.9.9')).called(1); + }); + }); +} diff --git a/packages/mcp_dart_cli/test/version_test.dart b/packages/mcp_dart_cli/test/version_test.dart new file mode 100644 index 0000000..8d3c348 --- /dev/null +++ b/packages/mcp_dart_cli/test/version_test.dart @@ -0,0 +1,23 @@ +import 'dart:io'; + +import 'package:mcp_dart_cli/src/version.dart'; +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; + +void main() { + test('version matches pubspec.yaml', () { + final pubspecFile = File('pubspec.yaml'); + expect(pubspecFile.existsSync(), isTrue); + + final pubspecContent = pubspecFile.readAsStringSync(); + final yaml = loadYaml(pubspecContent) as YamlMap; + final pubspecVersion = yaml['version'] as String; + + expect( + packageVersion, + pubspecVersion, + reason: 'lib/src/version.dart does not match pubspec.yaml. ' + 'Run "dart tool/update_version.dart" to update.', + ); + }); +}