Skip to content
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
7 changes: 7 additions & 0 deletions packages/mcp_dart_cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
9 changes: 9 additions & 0 deletions packages/mcp_dart_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ mcp_dart create <project_name> --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

Expand Down Expand Up @@ -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:
Expand Down
14 changes: 13 additions & 1 deletion packages/mcp_dart_cli/bin/mcp_dart.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> arguments) async {
if (arguments.contains('--version') || arguments.contains('-v')) {
stdout.writeln(packageVersion);
exit(0);
}

final logger = Logger();
final runner = CommandRunner<int>(
'mcp_dart',
Expand All @@ -15,10 +23,14 @@ void main(List<String> 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);
Expand Down
1 change: 1 addition & 0 deletions packages/mcp_dart_cli/dart_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
concurrency: 1
8 changes: 8 additions & 0 deletions packages/mcp_dart_cli/example/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
57 changes: 57 additions & 0 deletions packages/mcp_dart_cli/lib/src/update_command.dart
Original file line number Diff line number Diff line change
@@ -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<int> {
/// {@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<int> 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;
}
}
1 change: 1 addition & 0 deletions packages/mcp_dart_cli/lib/src/version.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const packageVersion = '0.1.5';
23 changes: 23 additions & 0 deletions packages/mcp_dart_cli/lib/src/version_check.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:mason_logger/mason_logger.dart';
import 'package:pub_updater/pub_updater.dart';

import 'version.dart';

Future<void> 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
}
}
3 changes: 2 additions & 1 deletion packages/mcp_dart_cli/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions packages/mcp_dart_cli/test/src/create_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions packages/mcp_dart_cli/test/src/doctor_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});

Expand Down
48 changes: 25 additions & 23 deletions packages/mcp_dart_cli/test/src/serve_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>('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<int>('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);
});
});
});
}
95 changes: 95 additions & 0 deletions packages/mcp_dart_cli/test/src/update_command_test.dart
Original file line number Diff line number Diff line change
@@ -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<UpdateCommand>());
});

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);
});
});
}
23 changes: 23 additions & 0 deletions packages/mcp_dart_cli/test/version_test.dart
Original file line number Diff line number Diff line change
@@ -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.',
);
});
}