diff --git a/CHANGELOG.md b/CHANGELOG.md index 2de9af8..3e8888c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.3.0 + +- Removed the `flutter_launcher` MCP server because it has been integrated into + the Dart MCP server in [dart-lang/ai#292](https://github.com/dart-lang/ai/pull/292) + ## 0.2.2 - Fixes the executable path for the flutter_launcher MCP server. diff --git a/README.md b/README.md index 5956b6e..080beed 100644 --- a/README.md +++ b/README.md @@ -49,16 +49,6 @@ The new commands will be available in new Gemini CLI sessions. The following com - `/modify` - Manages a structured modification session with automated planning. - `/commit` - Automates pre-commit checks and generates a descriptive commit message. -### 3. Available Tools - -This extension also installs an MCP server (`flutter_launcher`) that provides tools for starting, stopping, and interacting with Flutter applications. This server is started automatically, and the following tools are made available: - -- `launch_app`: Launches a Flutter application on a specified device. -- `stop_app`: Stops a running Flutter application. -- `list_devices`: Lists all available devices that can run Flutter applications. -- `get_app_logs`: Retrieves the logs from a running Flutter application. -- `list_running_apps`: Lists all Flutter applications currently running that were started by this extension. - ## 💡 Usage This extension provides powerful commands to automate key phases of the development lifecycle. diff --git a/flutter_launcher_mcp/.gitignore b/flutter_launcher_mcp/.gitignore deleted file mode 100644 index 053e77c..0000000 --- a/flutter_launcher_mcp/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# https://dart.dev/guides/libraries/private-files -# Created by `dart pub` -.dart_tool/ -flutter_launcher_mcp.dill \ No newline at end of file diff --git a/flutter_launcher_mcp/DESIGN.md b/flutter_launcher_mcp/DESIGN.md deleted file mode 100644 index a119428..0000000 --- a/flutter_launcher_mcp/DESIGN.md +++ /dev/null @@ -1,235 +0,0 @@ -# Design Document: `flutter_launcher_mcp` - -## Overview - -This document describes the design of the Dart package, `flutter_launcher_mcp`, which exposes a Model Context Protocol (MCP) server. This server provides a tool to launch Flutter applications, manage their processes, and return their Dart Tooling Daemon (DTD) URI. - -This package provides a robust, MCP-based solution for launching Flutter applications. - -## Detailed Analysis of the Goal or Problem - -The primary goal is to enable external clients (such as AI agents or IDEs) to programmatically launch and interact with Flutter applications. A key piece of information for this interaction is the DTD URI, which is required for debugging, hot-reloading, and other development-time operations. - -This package solves these problems by: - -1. Directly launching and managing the Flutter process from within the MCP server. -2. Capturing the DTD URI in real-time by listening to the process's `stdout` stream. -3. Keeping the Flutter process alive and holding onto its I/O streams, paving the way for future tools to interact with the running application (e.g., sending commands to `stdin`). -4. Exposing this functionality via the standardized MCP, allowing for seamless integration with various clients without requiring them to implement Flutter-specific process management logic. -5. Using dependency injection for process and file system management to ensure the server is easily testable. - -## Detailed Design for the Package - -### Package Structure - -The package structure is a standard Dart package with a clear separation of concerns. - -```txt -flutter_launcher_mcp/ -├── lib/ -│ ├── flutter_launcher_mcp.dart // Main library file -│ └── src/ -│ ├── server.dart // MCP server implementation -│ ├── mixins/ -│ │ └── flutter_launcher.dart -│ └── utils/ -│ ├── analytics.dart -│ ├── cli_utils.dart -│ ├── constants.dart -│ ├── file_system.dart -│ ├── process_manager.dart -│ └── sdk.dart -├── bin/ -│ └── flutter_launcher_mcp.dart // Executable for the MCP server -├── pubspec.yaml -├── README.md -├── CHANGELOG.md -└── DESIGN.md -``` - -### Dependencies - -- `dart_mcp`: For implementing the MCP server. -- `async`: For stream manipulation and asynchronous operations. -- `process`: For managing processes in a testable way. -- `file`: For interacting with the file system in a testable way. - -### `bin/flutter_launcher_mcp.dart` (Executable) - -This file is the entry point for the MCP server. It creates an instance of `FlutterLauncherMCPServer` and connects it to standard I/O, providing concrete implementations for the `ProcessManager`, `FileSystem`, and `Sdk`. - -```dart -// bin/flutter_launcher_mcp.dart -import 'dart:io'; -import 'package:dart_mcp/stdio.dart'; -import 'package:file/local.dart'; -import 'package:flutter_launcher_mcp/src/server.dart'; -import 'package:flutter_launcher_mcp/src/utils/sdk.dart'; -import 'package:process/process.dart'; - -void main() { - const processManager = LocalProcessManager(); - const fileSystem = LocalFileSystem(); - final sdk = Sdk.find(); - FlutterLauncherMCPServer( - stdioChannel(input: stdin, output: stdout), - sdk: sdk, - processManager: processManager, - fileSystem: fileSystem, - ); -} -``` - -### `lib/src/server.dart` and `lib/src/mixins/flutter_launcher.dart` - -The core logic is implemented in the `FlutterLauncherSupport` mixin, which is then applied to the main `FlutterLauncherMCPServer` class. This promotes separation of concerns and reusability. - -#### `FlutterLauncherMCPServer` Class - -This class extends `MCPServer` and composes functionality through mixins. It manages dependencies like `Sdk`, `ProcessManager`, and `FileSystem`. - -```dart -// lib/src/server.dart -import 'package:dart_mcp/server.dart'; -import 'package:file/file.dart'; -import 'package:process/process.dart'; - -import 'utils/file_system.dart'; -import 'utils/process_manager.dart'; -import 'utils/sdk.dart'; -import 'mixins/flutter_launcher.dart'; - -final class FlutterLauncherMCPServer extends MCPServer - with - LoggingSupport, - ToolsSupport, - RootsTrackingSupport, - FlutterLauncherSupport - implements ProcessManagerSupport, FileSystemSupport, SdkSupport { - @override - Sdk sdk; - - @override - final ProcessManager processManager; - - @override - final FileSystem fileSystem; - - FlutterLauncherMCPServer( - super.channel, { - required this.sdk, - required this.processManager, - required this.fileSystem, - /* ... */ - }) : super.fromStreamChannel(/* ... */); -} -``` - -#### `FlutterLauncherSupport` Mixin - -This mixin contains the tool definitions and implementations for interacting with Flutter applications. It manages running processes, their logs, and their DTD URIs. - -**Tools:** - -1. **`launch_app`**: Launches a Flutter application. -2. **`stop_app`**: Stops a running Flutter application by its PID. -3. **`list_devices`**: Lists available Flutter devices. -4. **`get_app_logs`**: Retrieves the captured logs for a running application. -5. **`list_running_apps`**: Lists all applications currently managed by the server. - -**Tool Definitions:** - -```dart -// from lib/src/mixins/flutter_launcher.dart - -// Tool definition for launching -final launchAppTool = Tool( - name: 'launch_app', - description: 'Launches a Flutter application and returns its DTD URI.', - inputSchema: Schema.object( - properties: { - 'root': Schema.string( - description: 'The root directory of the Flutter project.', - ), - 'target': Schema.string( - description: 'The main entry point file of the application.', - ), - 'device': Schema.string( - description: 'The device ID to launch the application on.', - ), - }, - required: ['root', 'device'], - ), - outputSchema: Schema.object( - properties: { - 'dtdUri': Schema.string( - description: 'The DTD URI of the launched Flutter application.', - ), - 'pid': Schema.int( - description: 'The process ID of the launched Flutter application.', - ), - }, - required: ['dtdUri', 'pid'], - ), -); - -// Tool definition for stopping an app -final stopAppTool = Tool( - name: 'stop_app', - description: 'Kills a running Flutter process managed by this server.', - inputSchema: Schema.object( - properties: { - 'pid': Schema.int( - description: 'The process ID of the process to kill.', - ), - }, - required: ['pid'], - ), - outputSchema: Schema.object( - properties: { - 'success': Schema.bool( - description: 'Whether the process was killed successfully.', - ), - }, - required: ['success'], - ), -); - -// Other tools like list_devices, get_app_logs, list_running_apps are also defined here. -``` - -The implementation of `_launchApp` starts the `flutter run` process with the `--print-dtd` flag, captures the DTD URI from the output, and stores the process and its logs. - -## Diagrams - -```mermaid -graph TD - A[Client Application] -->|"MCP Connection (stdio)"| B["flutter_launcher_mcp (MCP Server)"] - B -->|Calls Tool: launch_app| C{Launches 'flutter run --print-dtd'} - C -->|Manages Process| D[Flutter Application Process] - D -->|stdout/stderr streams| B - B -->|"Parses DTD URI & Captures Logs"| B - B -->|"Returns CallToolResult (DTD URI, PID)"| A - - subgraph "Other Tools" - A2[Client Application] -->|"MCP Connection (stdio)"| B - B -->|"Calls Tool: stop_app(PID)"| C - B -->|"Calls Tool: list_devices"| E[flutter devices --machine] - B -->|"Calls Tool: get_app_logs(PID)"| F[Returns stored logs] - B -->|"Calls Tool: list_running_apps"| G[Returns running apps info] - end -``` - -## Summary of the Design - -The design for `flutter_launcher_mcp` creates a self-contained, efficient, and robust MCP server for managing Flutter application processes. By directly handling process creation and I/O streaming, it provides real-time DTD URI retrieval and eliminates the overhead of file-based communication. The architecture is modular, with core functionality encapsulated in a `FlutterLauncherSupport` mixin. - -The server provides a comprehensive set of tools for the entire application lifecycle, including launching (`launch_app`), stopping (`stop_app`), and monitoring (`list_devices`, `get_app_logs`, `list_running_apps`). This provides a solid foundation for sophisticated client integrations. The use of dependency injection for the SDK, process, and file system management ensures the server is easily testable. - -## References to Research URLs - -- [Model Context Protocol Specification](https://modelcontextprotocol.io/docs/concepts/overview) -- [dart_mcp package on pub.dev](https://pub.dev/packages/dart_mcp) -- [Dart `Process` class documentation](https://api.dart.dev/stable/dart-io/Process-class.html) -- [process package on pub.dev](https://pub.dev/packages/process) -- [file package on pub.dev](https://pub.dev/packages/file) diff --git a/flutter_launcher_mcp/README.md b/flutter_launcher_mcp/README.md deleted file mode 100644 index 2018eb4..0000000 --- a/flutter_launcher_mcp/README.md +++ /dev/null @@ -1,260 +0,0 @@ -# flutter_launcher_mcp - -An MCP server for launching and managing Flutter applications. - -## Overview - -`flutter_launcher_mcp` provides a Model Context Protocol (MCP) server that allows external clients (such as AI agents or IDEs) to programmatically launch Flutter applications and obtain their Dart Tooling Daemon (DTD) URI. It also provides a mechanism to gracefully terminate these launched processes. - -This package aims to simplify the integration of Flutter development workflows with various tools by offering a standardized, language-agnostic interface for Flutter process management. - -## Features - -- **Launch Flutter Applications:** Start Flutter applications with custom arguments and retrieve their DTD URI. -- **Process Management:** Keep track of launched Flutter processes and terminate them when no longer needed. -- **Real-time DTD URI Retrieval:** Captures the DTD URI directly from the Flutter process's `stdout` stream. -- **MCP Compliant:** Exposes functionality via the Model Context Protocol for seamless integration with MCP-aware clients. -- **Testable Design:** Uses dependency injection for `ProcessManager` and `FileSystem` to facilitate unit testing. - -## Installation - -To use `flutter_launcher_mcp`, add it as a dependency to your `pubspec.yaml`: - -```yaml -dependencies: - flutter_launcher_mcp: ^0.2.2 -``` - -Then, run `dart pub get`. - -## Usage - -### Running the MCP Server - -The `flutter_launcher_mcp` package provides an executable that runs the MCP server. You can start it from your terminal: - -```bash -dart run flutter_launcher_mcp -``` - -This will start the server, listening for MCP requests on standard input/output. Clients can then connect to this server using the `dart_mcp` client library or any other MCP-compliant client. - -### Available Tools - -The server exposes the following tools: - -#### `launch_app` - -Launches a Flutter application with specified arguments and returns its DTD URI and process ID. - -- **Input Schema:** - - ```json - { - "type": "object", - "properties": { - "root": { - "type": "string", - "description": "The root directory of the Flutter project." - }, - "target": { - "type": "string", - "description": "The main entry point file of the application. Defaults to \"lib/main.dart\"." - }, - "device": { - "type": "string", - "description": "The device ID to launch the application on. To get a list of available devices to present as choices, use the list_devices tool." - } - }, - "required": ["root", "device"] - } - ``` - -- **Output Schema:** - - ```json - { - "type": "object", - "properties": { - "dtdUri": { - "type": "string", - "description": "The DTD URI of the launched Flutter application." - }, - "pid": { - "type": "integer", - "description": "The process ID of the launched Flutter application." - } - }, - "required": ["dtdUri", "pid"] - } - ``` - -#### `stop_app` - -Kills a running Flutter process started by the `launch_app` tool. - -- **Input Schema:** - - ```json - { - "type": "object", - "properties": { - "pid": { - "type": "integer", - "description": "The process ID of the process to kill." - } - }, - "required": ["pid"] - } - ``` - -- **Output Schema:** - - ```json - { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "description": "Whether the process was killed successfully." - } - }, - "required": ["success"] - } - ``` - -#### `list_devices` - -Lists available Flutter devices. - -- **Input Schema:** - - ```json - { - "type": "object" - } - ``` - -- **Output Schema:** - - ```json - { - "type": "object", - "properties": { - "devices": { - "type": "array", - "description": "A list of available device IDs.", - "items": { - "type": "string" - } - } - }, - "required": ["devices"] - } - ``` - -#### `get_app_logs` - -Returns the collected logs for a given flutter run process id. Can only retrieve logs started by the `launch_app` tool. - -- **Input Schema:** - - ```json - { - "type": "object", - "properties": { - "pid": { - "type": "integer", - "description": "The process ID of the flutter run process running the application." - } - }, - "required": ["pid"] - } - ``` - -- **Output Schema:** - - ```json - { - "type": "object", - "properties": { - "logs": { - "type": "array", - "description": "The collected logs for the process.", - "items": { - "type": "string" - } - } - }, - "required": ["logs"] - } - ``` - -#### `list_running_apps` - -Returns the list of running app process IDs and associated DTD URIs for apps started by the `launch_app` tool. - -- **Input Schema:** - - ```json - { - "type": "object" - } - ``` - -- **Output Schema:** - - ```json - { - "type": "object", - "properties": { - "apps": { - "type": "array", - "description": "A list of running applications started by the launch_app tool.", - "items": { - "type": "object", - "properties": { - "pid": { - "type": "integer", - "description": "The process ID of the application." - }, - "dtdUri": { - "type": "string", - "description": "The DTD URI of the application." - } - }, - "required": ["pid", "dtdUri"] - } - } - }, - "required": ["apps"] - } - ``` - -## Development - -### Running Tests - -To run the unit tests for `flutter_launcher_mcp`: - -```bash -dart test -``` - -### Code Formatting and Analysis - -To format your code and run static analysis: - -```bash -dart format . -dart fix --apply -dart analyze -``` - -## Contributing - -Contributions are welcome! Please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. - -## License - -This project is licensed under the [LICENSE](LICENSE) file. diff --git a/flutter_launcher_mcp/analysis_options.yaml b/flutter_launcher_mcp/analysis_options.yaml deleted file mode 100644 index 9e32eac..0000000 --- a/flutter_launcher_mcp/analysis_options.yaml +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright 2025 The Flutter Authors. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -include: package:lints/recommended.yaml diff --git a/flutter_launcher_mcp/bin/flutter_launcher_mcp.dart b/flutter_launcher_mcp/bin/flutter_launcher_mcp.dart deleted file mode 100644 index a9ae9b5..0000000 --- a/flutter_launcher_mcp/bin/flutter_launcher_mcp.dart +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// 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:args/args.dart'; -import 'package:dart_mcp/server.dart'; -import 'package:dart_mcp/stdio.dart'; -import 'package:file/local.dart'; -import 'package:flutter_launcher_mcp/src/server.dart'; -import 'package:flutter_launcher_mcp/src/utils/sdk.dart'; -import 'package:process/process.dart'; - -const logLevelOption = 'log-level'; -const helpFlag = 'help'; - -Future main(List arguments) async { - final parser = ArgParser() - ..addOption( - logLevelOption, - help: 'The initial level of logging to show.', - allowed: LoggingLevel.values.map((e) => e.name.toLowerCase()), - defaultsTo: 'info', - ) - ..addFlag( - helpFlag, - abbr: 'h', - help: 'Shows this help message.', - negatable: false, - ); - final argResults = parser.parse(arguments); - - if (argResults[helpFlag] as bool) { - stdout.writeln('A server for launching Flutter applications.\n'); - stdout.writeln(parser.usage); - return; - } - - final logLevel = LoggingLevel.values.firstWhere( - (level) => level.name.toLowerCase() == argResults[logLevelOption], - ); - - const processManager = LocalProcessManager(); - const fileSystem = LocalFileSystem(); - final sdk = Sdk(); - - final server = FlutterLauncherMCPServer( - stdioChannel(input: stdin, output: stdout), - sdk: sdk, - processManager: processManager, - fileSystem: fileSystem, - initialLogLevel: logLevel, - ); - - await server.sdk.init( - processManager: processManager, - fileSystem: fileSystem, - log: server.log, - ); -} diff --git a/flutter_launcher_mcp/lib/flutter_launcher_mcp.dart b/flutter_launcher_mcp/lib/flutter_launcher_mcp.dart deleted file mode 100644 index df5ca2f..0000000 --- a/flutter_launcher_mcp/lib/flutter_launcher_mcp.dart +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// The main library for the Flutter Launcher MCP server. -/// -/// This library exports the [FlutterLauncherMCPServer], which is the primary -/// class for the server. -library; - -export 'src/server.dart'; diff --git a/flutter_launcher_mcp/lib/src/mixins/flutter_launcher.dart b/flutter_launcher_mcp/lib/src/mixins/flutter_launcher.dart deleted file mode 100644 index 294034b..0000000 --- a/flutter_launcher_mcp/lib/src/mixins/flutter_launcher.dart +++ /dev/null @@ -1,474 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// A mixin that provides tools for launching and managing Flutter applications. -library; - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:dart_mcp/server.dart'; - -import '../utils/process_manager.dart'; -import '../utils/sdk.dart'; - -class _RunningApp { - final Process process; - final List logs = []; - String? dtdUri; - - _RunningApp(this.process); -} - -/// A mixin that provides tools for launching and managing Flutter applications. -/// -/// This mixin registers tools for launching, stopping, and listing Flutter -/// applications, as well as listing available devices and retrieving application -/// logs. It manages the lifecycle of Flutter processes that it launches. -base mixin FlutterLauncherSupport - on ToolsSupport, LoggingSupport, RootsTrackingSupport - implements ProcessManagerSupport, SdkSupport { - final Map _runningApps = {}; - - @override - FutureOr initialize(InitializeRequest request) { - registerTool(launchAppTool, _launchApp); - registerTool(stopAppTool, _stopApp); - registerTool(listDevicesTool, _listDevices); - registerTool(getAppLogsTool, _getAppLogs); - registerTool(listRunningAppsTool, _listRunningApps); - return super.initialize(request); - } - - /// A tool to launch a Flutter application. - final launchAppTool = Tool( - name: 'launch_app', - description: 'Launches a Flutter application and returns its DTD URI.', - inputSchema: Schema.object( - properties: { - 'root': Schema.string( - description: 'The root directory of the Flutter project.', - ), - 'target': Schema.string( - description: - 'The main entry point file of the application. Defaults to "lib/main.dart".', - ), - 'device': Schema.string( - description: - "The device ID to launch the application on. To get a list of available devices to present as choices, use the list_devices tool.", - ), - }, - required: ['root', 'device'], - ), - outputSchema: Schema.object( - properties: { - 'dtdUri': Schema.string( - description: 'The DTD URI of the launched Flutter application.', - ), - 'pid': Schema.int( - description: 'The process ID of the launched Flutter application.', - ), - }, - required: ['dtdUri', 'pid'], - ), - ); - - Future _launchApp(CallToolRequest request) async { - if (sdk.flutterExecutablePath == null) { - return CallToolResult( - isError: true, - content: [ - TextContent( - text: - 'Flutter executable not found. Please ensure the Flutter SDK is in your path and restart the MCP server.', - ), - ], - ); - } - final root = request.arguments!['root'] as String; - final target = request.arguments!['target'] as String?; - final device = request.arguments!['device'] as String; - final completer = Completer<({Uri dtdUri, int pid})>(); - - log( - LoggingLevel.debug, - 'Launching app with root: $root, target: $target, device: $device', - ); - - Process? process; - try { - process = await processManager.start( - [ - sdk.flutterExecutablePath!, - 'run', - '--print-dtd', - '--device-id', - device, - if (target != null) '--target', - if (target != null) target, - ], - workingDirectory: root, - mode: ProcessStartMode.normal, - ); - _runningApps[process.pid] = _RunningApp(process); - log( - LoggingLevel.info, - 'Launched Flutter application with PID: ${process.pid}', - ); - - final stdoutDone = Completer(); - final stderrDone = Completer(); - - late StreamSubscription stdoutSubscription; - late StreamSubscription stderrSubscription; - final dtdUriRegex = RegExp( - r'The Dart Tooling Daemon is available at: (ws://.+:\d+/\S+=)', - ); - - void checkForDtdUri(String line) { - final match = dtdUriRegex.firstMatch(line); - if (match != null && !completer.isCompleted) { - final dtdUri = Uri.parse(match.group(1)!); - log(LoggingLevel.debug, 'Found DTD URI: $dtdUri'); - completer.complete((dtdUri: dtdUri, pid: process!.pid)); - } - } - - stdoutSubscription = process.stdout - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen( - (line) { - log( - LoggingLevel.debug, - '[flutter stdout ${process!.pid}]: $line', - ); - _runningApps[process.pid]?.logs.add('[stdout] $line'); - checkForDtdUri(line); - }, - onDone: () => stdoutDone.complete(), - onError: stdoutDone.completeError, - ); - - stderrSubscription = process.stderr - .transform(utf8.decoder) - .transform(const LineSplitter()) - .listen( - (line) { - log( - LoggingLevel.warning, - '[flutter stderr ${process!.pid}]: $line', - ); - _runningApps[process.pid]?.logs.add('[stderr] $line'); - checkForDtdUri(line); - }, - onDone: () => stderrDone.complete(), - onError: stderrDone.completeError, - ); - - unawaited( - process.exitCode.then((exitCode) async { - // Wait for both streams to finish processing before potentially completing the completer with an error. - await Future.wait([stdoutDone.future, stderrDone.future]); - - log( - LoggingLevel.info, - 'Flutter application ${process!.pid} exited with code $exitCode.', - ); - _runningApps.remove(process.pid); - if (!completer.isCompleted) { - completer.completeError( - 'Flutter application exited with code $exitCode before the DTD URI was found.', - ); - } - // Cancel subscriptions after all processing is done. - stdoutSubscription.cancel(); - stderrSubscription.cancel(); - }), - ); - - final result = await completer.future.timeout( - const Duration(seconds: 90), - ); - _runningApps[result.pid]?.dtdUri = result.dtdUri.toString(); - - return CallToolResult( - content: [ - TextContent( - text: - 'Flutter application launched successfully with PID ${result.pid} with the DTD URI ${result.dtdUri}', - ), - ], - structuredContent: { - 'dtdUri': result.dtdUri.toString(), - 'pid': result.pid, - }, - ); - } catch (e, s) { - log(LoggingLevel.error, 'Error launching Flutter application: $e\n$s'); - if (process != null) { - process.kill(); - // The exitCode handler will perform the rest of the cleanup. - } - return CallToolResult( - isError: true, - content: [ - TextContent(text: 'Failed to launch Flutter application: $e'), - ], - ); - } - } - - /// A tool to stop a running Flutter application. - final stopAppTool = Tool( - name: 'stop_app', - description: - 'Kills a running Flutter process started by the launch_app tool.', - inputSchema: Schema.object( - properties: { - 'pid': Schema.int( - description: 'The process ID of the process to kill.', - ), - }, - required: ['pid'], - ), - outputSchema: Schema.object( - properties: { - 'success': Schema.bool( - description: 'Whether the process was killed successfully.', - ), - }, - required: ['success'], - ), - ); - - Future _stopApp(CallToolRequest request) async { - final pid = request.arguments!['pid'] as int; - log(LoggingLevel.info, 'Attempting to stop application with PID: $pid'); - final app = _runningApps[pid]; - - if (app == null) { - log(LoggingLevel.error, 'Application with PID $pid not found.'); - return CallToolResult( - isError: true, - content: [TextContent(text: 'Application with PID $pid not found.')], - ); - } - - final success = processManager.killPid(pid); - if (success) { - log( - LoggingLevel.info, - 'Successfully sent kill signal to application $pid.', - ); - } else { - log( - LoggingLevel.warning, - 'Failed to send kill signal to application $pid.', - ); - } - - return CallToolResult( - content: [ - TextContent( - text: - 'Application with PID $pid ${success ? 'was stopped' : 'was unable to be stopped'}.', - ), - ], - isError: !success, - structuredContent: {'success': success}, - ); - } - - /// A tool to list available Flutter devices. - final listDevicesTool = Tool( - name: 'list_devices', - description: 'Lists available Flutter devices.', - inputSchema: Schema.object(), - outputSchema: Schema.object( - properties: { - 'devices': Schema.list( - description: 'A list of available device IDs.', - items: Schema.string(), - ), - }, - required: ['devices'], - ), - ); - - Future _listDevices(CallToolRequest request) async { - if (sdk.flutterExecutablePath == null) { - return CallToolResult( - isError: true, - content: [ - TextContent( - text: - 'Flutter executable not found. Please ensure the Flutter SDK is in your path and restart the MCP server.', - ), - ], - ); - } - try { - log(LoggingLevel.debug, 'Listing flutter devices.'); - final result = await processManager.run([ - sdk.flutterExecutablePath!, - 'devices', - '--machine', - ]); - - if (result.exitCode != 0) { - log( - LoggingLevel.error, - 'Flutter devices command failed with exit code ${result.exitCode}. Stderr: ${result.stderr}', - ); - return CallToolResult( - isError: true, - content: [ - TextContent( - text: 'Failed to list Flutter devices: ${result.stderr}', - ), - ], - ); - } - - final stdout = result.stdout as String; - if (stdout.isEmpty) { - log(LoggingLevel.debug, 'No devices found.'); - return CallToolResult( - content: [TextContent(text: 'No devices found.')], - structuredContent: {'devices': []}, - ); - } - - final devices = (jsonDecode(stdout) as List) - .cast>() - .map((device) => device['id'] as String) - .toList(); - log(LoggingLevel.debug, 'Found devices: $devices'); - - return CallToolResult( - content: [TextContent(text: 'Found devices: ${devices.join(', ')}')], - structuredContent: {'devices': devices}, - ); - } catch (e, s) { - log(LoggingLevel.error, 'Error listing Flutter devices: $e\n$s'); - return CallToolResult( - isError: true, - content: [TextContent(text: 'Failed to list Flutter devices: $e')], - ); - } - } - - /// A tool to get the logs for a running Flutter application. - final getAppLogsTool = Tool( - name: 'get_app_logs', - description: - 'Returns the collected logs for a given flutter run process id. Can only retrieve logs started by the launch_app tool.', - inputSchema: Schema.object( - properties: { - 'pid': Schema.int( - description: - 'The process ID of the flutter run process running the application.', - ), - }, - required: ['pid'], - ), - outputSchema: Schema.object( - properties: { - 'logs': Schema.list( - description: 'The collected logs for the process.', - items: Schema.string(), - ), - }, - required: ['logs'], - ), - ); - - Future _getAppLogs(CallToolRequest request) async { - final pid = request.arguments!['pid'] as int; - log(LoggingLevel.info, 'Getting logs for application with PID: $pid'); - final logs = _runningApps[pid]?.logs; - - if (logs == null) { - log( - LoggingLevel.error, - 'Application with PID $pid not found or has no logs.', - ); - return CallToolResult( - isError: true, - content: [ - TextContent( - text: 'Application with PID $pid not found or has no logs.', - ), - ], - ); - } - - return CallToolResult( - content: [TextContent(text: logs.join('\n'))], - structuredContent: {'logs': logs}, - ); - } - - /// A tool to list all running Flutter applications. - final listRunningAppsTool = Tool( - name: 'list_running_apps', - description: - 'Returns the list of running app process IDs and associated DTD URIs for apps started by the launch_app tool.', - inputSchema: Schema.object(), - outputSchema: Schema.object( - properties: { - 'apps': Schema.list( - description: - 'A list of running applications started by the launch_app tool.', - items: Schema.object( - properties: { - 'pid': Schema.int( - description: 'The process ID of the application.', - ), - 'dtdUri': Schema.string( - description: 'The DTD URI of the application.', - ), - }, - required: ['pid', 'dtdUri'], - ), - ), - }, - required: ['apps'], - ), - ); - - Future _listRunningApps(CallToolRequest request) async { - final apps = _runningApps.entries - .where((entry) => entry.value.dtdUri != null) - .map((entry) { - return {'pid': entry.key, 'dtdUri': entry.value.dtdUri!}; - }) - .toList(); - - return CallToolResult( - content: [ - TextContent( - text: - 'Found ${apps.length} running application${apps.length == 1 ? '' : 's'}.\n${apps.map((e) { - return 'PID: ${e['pid']}, DTD URI: ${e['dtdUri']}'; - }).toList().join('\n')}', - ), - ], - structuredContent: {'apps': apps}, - ); - } - - @override - Future shutdown() { - log(LoggingLevel.info, 'Shutting down server, killing all processes.'); - for (final pid in _runningApps.keys) { - log(LoggingLevel.debug, 'Killing process $pid.'); - processManager.killPid(pid); - } - _runningApps.clear(); - return super.shutdown(); - } -} diff --git a/flutter_launcher_mcp/lib/src/server.dart b/flutter_launcher_mcp/lib/src/server.dart deleted file mode 100644 index cf9bf8e..0000000 --- a/flutter_launcher_mcp/lib/src/server.dart +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// The main entry point for the Flutter Launcher MCP server. -library; - -import 'package:dart_mcp/server.dart'; -import 'package:file/file.dart'; -import 'package:process/process.dart'; - -import 'mixins/flutter_launcher.dart'; -import 'utils/file_system.dart'; -import 'utils/process_manager.dart'; -import 'utils/sdk.dart'; - -/// An MCP server for launching and managing Flutter applications. -/// -/// This server composes its functionality from various mixins, including -/// [FlutterLauncherSupport] for handling Flutter-specific tasks. It implements -/// [ProcessManagerSupport], [FileSystemSupport], and [SdkSupport] to provide -// dependencies to the mixins that require them. -final class FlutterLauncherMCPServer extends MCPServer - with - LoggingSupport, - ToolsSupport, - RootsTrackingSupport, - FlutterLauncherSupport - implements ProcessManagerSupport, FileSystemSupport, SdkSupport { - @override - final Sdk sdk; - - @override - final ProcessManager processManager; - - @override - final FileSystem fileSystem; - - /// Creates a new instance of the [FlutterLauncherMCPServer]. - /// - /// The server is initialized with the required [sdk], [processManager], and - /// [fileSystem] instances, which are then made available to the various - /// mixins. - FlutterLauncherMCPServer( - super.channel, { - required this.sdk, - required this.processManager, - required this.fileSystem, - LoggingLevel initialLogLevel = LoggingLevel.info, - }) : super.fromStreamChannel( - implementation: Implementation( - name: 'Flutter Launcher MCP Server', - version: const String.fromEnvironment( - 'FLUTTER_LAUNCHER_VERSION', - defaultValue: '0.0.0-dev', - ), - ), - instructions: - 'Provides tools to launch and manage Flutter applications.', - ) { - loggingLevel = initialLogLevel; - log(LoggingLevel.info, 'FlutterLauncherMCPServer started.'); - } -} diff --git a/flutter_launcher_mcp/lib/src/utils/analytics.dart b/flutter_launcher_mcp/lib/src/utils/analytics.dart deleted file mode 100644 index 28eb09b..0000000 --- a/flutter_launcher_mcp/lib/src/utils/analytics.dart +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// A library for handling analytics within the MCP server. -library; - -import 'package:dart_mcp/server.dart'; -import 'package:unified_analytics/unified_analytics.dart'; - -/// Provides access to an [Analytics] instance for MCP servers. -/// -/// The `DartMCPServer` class implements this class so that [Analytics] -/// methods can be easily mocked during testing. -abstract interface class AnalyticsSupport { - /// The analytics instance, or `null` if analytics are disabled. - Analytics? get analytics; -} - -/// Defines the types of analytics events that can be tracked. -enum AnalyticsEvent { - /// An event that is fired when a tool is called. - callTool, - - /// An event that is fired when a resource is read. - readResource, - - /// An event that is fired when a prompt is retrieved. - getPrompt, -} - -/// The metrics for a resources/read MCP handler. -final class ReadResourceMetrics extends CustomMetrics { - /// The kind of resource that was read. - /// - /// We don't want to record the full URI. - final ResourceKind kind; - - /// The length of the resource. - final int length; - - /// The time it took to read the resource. - final int elapsedMilliseconds; - - /// Creates a new instance of [ReadResourceMetrics]. - ReadResourceMetrics({ - required this.kind, - required this.length, - required this.elapsedMilliseconds, - }); - - @override - Map toMap() => { - _kind: kind.name, - _length: length, - _elapsedMilliseconds: elapsedMilliseconds, - }; -} - -/// The metrics for a prompts/get MCP handler. -final class GetPromptMetrics extends CustomMetrics { - /// The name of the prompt that was retrieved. - final String name; - - /// Whether or not the prompt was given with arguments. - final bool withArguments; - - /// The time it took to generate the prompt. - final int elapsedMilliseconds; - - /// Whether or not the prompt call succeeded. - final bool success; - - /// Creates a new instance of [GetPromptMetrics]. - GetPromptMetrics({ - required this.name, - required this.withArguments, - required this.elapsedMilliseconds, - required this.success, - }); - - @override - Map toMap() => { - _name: name, - _withArguments: withArguments, - _elapsedMilliseconds: elapsedMilliseconds, - _success: success, - }; -} - -/// The metrics for a tools/call MCP handler. -final class CallToolMetrics extends CustomMetrics { - /// The name of the tool that was invoked. - final String tool; - - /// Whether or not the tool call succeeded. - final bool success; - - /// The time it took to invoke the tool. - final int elapsedMilliseconds; - - /// The reason for the failure, if [success] is `false`. - final CallToolFailureReason? failureReason; - - /// Creates a new instance of [CallToolMetrics]. - CallToolMetrics({ - required this.tool, - required this.success, - required this.elapsedMilliseconds, - required this.failureReason, - }); - - @override - Map toMap() => { - _tool: tool, - _success: success, - _elapsedMilliseconds: elapsedMilliseconds, - if (failureReason != null) _failureReason: failureReason!.name, - }; -} - -/// The kind of resource that was read. -enum ResourceKind { - /// The runtime errors of the application. - runtimeErrors, -} - -/// An extension for attaching failure reasons to [CallToolResult] objects. -extension WithFailureReason on CallToolResult { - static final _expando = Expando(); - - /// Gets the failure reason for this [CallToolResult]. - CallToolFailureReason? get failureReason => _expando[this as Object]; - - /// Sets the failure reason for this [CallToolResult]. - set failureReason(CallToolFailureReason? value) => - _expando[this as Object] = value; -} - -/// Known reasons for failed tool calls. -enum CallToolFailureReason { - /// An error occurred due to invalid arguments. - argumentError, - - /// The connected application's service is not supported. - connectedAppServiceNotSupported, - - /// A DTD connection was attempted when one was already established. - dtdAlreadyConnected, - - /// A DTD connection was required but not established. - dtdNotConnected, - - /// Flutter driver was required but not enabled. - flutterDriverNotEnabled, - - /// The provided path was invalid. - invalidPath, - - /// The provided root path was invalid. - invalidRootPath, - - /// The provided root URI scheme was invalid. - invalidRootScheme, - - /// There was no active debug session. - noActiveDebugSession, - - /// A required root was not provided. - noRootGiven, - - /// No project roots have been set. - noRootsSet, - - /// The requested command does not exist. - noSuchCommand, - - /// A WebSocket exception occurred. - webSocketException, -} - -const _elapsedMilliseconds = 'elapsedMilliseconds'; -const _failureReason = 'failureReason'; -const _kind = 'kind'; -const _length = 'length'; -const _name = 'name'; -const _success = 'success'; -const _tool = 'tool'; -const _withArguments = 'withArguments'; diff --git a/flutter_launcher_mcp/lib/src/utils/cli_utils.dart b/flutter_launcher_mcp/lib/src/utils/cli_utils.dart deleted file mode 100644 index 8911c97..0000000 --- a/flutter_launcher_mcp/lib/src/utils/cli_utils.dart +++ /dev/null @@ -1,454 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// A library of utility functions for command-line operations in the MCP -/// server. -library; - -import 'dart:async'; -import 'dart:io' as io; - -import 'package:collection/collection.dart'; -import 'package:dart_mcp/server.dart'; -import 'package:file/file.dart'; -import 'package:process/process.dart'; -import 'package:yaml/yaml.dart'; - -import 'analytics.dart'; -import 'constants.dart'; -import 'sdk.dart'; - -/// The supported kinds of projects. -enum ProjectKind { - /// A Flutter project. - flutter, - - /// A Dart project. - dart, - - /// An unknown project, this usually means there was no pubspec.yaml. - unknown, -} - -/// Infers the [ProjectKind] of a given project at [rootUri]. -/// -/// This is done by checking for the existence of a `pubspec.yaml` -/// file and whether it contains a Flutter SDK dependency. -Future inferProjectKind( - String rootUri, - FileSystem fileSystem, -) async { - final pubspecFile = fileSystem - .directory(Uri.parse(rootUri)) - .childFile('pubspec.yaml'); - if (!await pubspecFile.exists()) { - return ProjectKind.unknown; - } - final pubspec = loadYaml(await pubspecFile.readAsString()) as Pubspec; - - if (pubspec.flutter != null || - pubspec.environment?.containsKey('flutter') == true || - pubspec.dependencies - .followedBy(pubspec.devDependencies) - .any((dep) => dep.sdk == 'flutter')) { - return ProjectKind.flutter; - } - return ProjectKind.dart; -} - -/// Runs a command in each of the project roots specified in the [request]. -/// -/// The [commandForRoot] function determines the executable to run (e.g., `dart` -/// or `flutter`), and it is combined with [arguments] to form the full command. -/// This command is then passed directly to [ProcessManager.run]. -/// -/// The [commandDescription] is used in the output to describe the command -/// being run. For example, if the command is `['dart', 'fix', '--apply']`, the -/// command description might be `dart fix`. -/// -/// If no roots are provided in the [request], the command is run in all -/// [knownRoots]. Otherwise, all roots provided in the request must be -/// subdirectories of the [knownRoots]. -/// -/// [defaultPaths] can be specified for commands that require path arguments -/// (e.g., `dart format .`). These paths are used if the root configuration in -/// the [request] does not specify its own paths. -Future runCommandInRoots( - CallToolRequest request, { - FutureOr Function(String, FileSystem, Sdk) commandForRoot = - defaultCommandForRoot, - List arguments = const [], - required String commandDescription, - required FileSystem fileSystem, - required ProcessManager processManager, - required List knownRoots, - List defaultPaths = const [], - required Sdk sdk, -}) async { - var rootConfigs = (request.arguments?[ParameterNames.roots] as List?) - ?.cast>(); - - // Default to use the known roots if none were specified. - if (rootConfigs == null || rootConfigs.isEmpty) { - rootConfigs = [ - for (final root in knownRoots) {ParameterNames.root: root.uri}, - ]; - } - - final outputs = []; - var isError = false; - for (var rootConfig in rootConfigs) { - final result = await runCommandInRoot( - request, - rootConfig: rootConfig, - commandForRoot: commandForRoot, - arguments: arguments, - commandDescription: commandDescription, - fileSystem: fileSystem, - processManager: processManager, - knownRoots: knownRoots, - defaultPaths: defaultPaths, - sdk: sdk, - ); - isError = isError || result.isError == true; - outputs.addAll(result.content); - } - return CallToolResult(content: outputs, isError: isError); -} - -/// Runs a command in a single project root specified in the [request]. -/// -/// If [rootConfig] is provided, it is used to read the root configuration; -/// otherwise, the configuration is read directly from `request.arguments`. -/// -/// The [commandForRoot] function determines the executable to run, which is -/// combined with [arguments] and passed to [ProcessManager.run]. -/// -/// The [commandDescription] is used in the output to describe the command being -/// run. -/// -/// [defaultPaths] can be specified for commands that require path arguments and -/// are used if the root configuration does not provide its own paths. -Future runCommandInRoot( - CallToolRequest request, { - Map? rootConfig, - FutureOr Function(String, FileSystem, Sdk) commandForRoot = - defaultCommandForRoot, - List arguments = const [], - required String commandDescription, - required FileSystem fileSystem, - required ProcessManager processManager, - required List knownRoots, - List defaultPaths = const [], - required Sdk sdk, -}) async { - rootConfig ??= request.arguments; - final rootUriString = rootConfig?[ParameterNames.root] as String?; - if (rootUriString == null) { - // This shouldn't happen based on the schema, but handle defensively. - return CallToolResult( - content: [ - TextContent(text: 'Invalid root configuration: missing `root` key.'), - ], - isError: true, - )..failureReason ??= CallToolFailureReason.noRootGiven; - } - - final root = knownRoots.firstWhereOrNull( - (root) => _isUnderRoot(root, rootUriString, fileSystem), - ); - if (root == null) { - return CallToolResult( - content: [ - TextContent( - text: - 'Invalid root $rootUriString, must be under one of the ' - 'registered project roots:\n\n${knownRoots.join('\n')}', - ), - ], - isError: true, - )..failureReason ??= CallToolFailureReason.invalidRootPath; - } - - final rootUri = Uri.parse(rootUriString); - if (rootUri.scheme != 'file') { - return CallToolResult( - content: [ - TextContent( - text: - 'Only file scheme uris are allowed for roots, but got ' - '$rootUri', - ), - ], - isError: true, - )..failureReason ??= CallToolFailureReason.invalidRootScheme; - } - final projectRoot = fileSystem.directory(rootUri); - - final command = await commandForRoot(rootUriString, fileSystem, sdk); - if (command == null) { - return CallToolResult( - content: [ - TextContent( - text: - 'Flutter executable not found. Please ensure the Flutter SDK is in your path and restart the MCP server.', - ), - ], - isError: true, - ); - } - - final commandWithPaths = [command, ...arguments]; - final paths = - (rootConfig?[ParameterNames.paths] as List?)?.cast() ?? - defaultPaths; - final invalidPaths = paths.where( - (path) => !_isUnderRoot(root, path, fileSystem), - ); - if (invalidPaths.isNotEmpty) { - return CallToolResult( - content: [ - TextContent( - text: - 'Paths are not allowed to escape their project root:\n' - '${invalidPaths.join('\n')}', - ), - ], - isError: true, - )..failureReason ??= CallToolFailureReason.invalidPath; - } - commandWithPaths.addAll(paths); - - final workingDir = fileSystem.directory(projectRoot.path); - await workingDir.create(recursive: true); - - final result = await processManager.run( - commandWithPaths, - workingDirectory: workingDir.path, - runInShell: - // Required when running .bat files on windows, but otherwise should - // be avoided due to escaping behavior. - io.Platform.isWindows && commandWithPaths.first.endsWith('.bat'), - ); - - final output = (result.stdout as String).trim(); - final errors = (result.stderr as String).trim(); - if (result.exitCode != 0) { - return CallToolResult( - content: [ - TextContent( - text: - '$commandDescription returned a non-zero exit code in ' - '${projectRoot.path}:\n' - '$output${errors.isEmpty ? '' : '\nErrors:\n$errors'}', - ), - // Returning a non-zero exit code is not considered an "error" in the - // "isError" sense. - ], - ); - } - return CallToolResult( - content: [ - TextContent(text: '$commandDescription in ${projectRoot.path}:\n$output'), - ], - ); -} - -/// Validates a root argument given via [rootConfig]. -/// -/// This function ensures that the root falls under one of the [knownRoots], and -/// that all `paths` arguments are also under the given root. -/// -/// On success, it returns a record containing the validated [Root] and a list -/// of paths. If no [ParameterNames.paths] are provided, [defaultPaths] is used. -/// -/// On failure, it returns a [CallToolResult] with an error message. -({Root? root, List? paths, CallToolResult? errorResult}) -validateRootConfig( - Map? rootConfig, { - List? defaultPaths, - required FileSystem fileSystem, - required List knownRoots, -}) { - final rootUriString = rootConfig?[ParameterNames.root] as String?; - if (rootUriString == null) { - // This shouldn't happen based on the schema, but handle defensively. - return ( - root: null, - paths: null, - errorResult: CallToolResult( - content: [ - TextContent(text: 'Invalid root configuration: missing `root` key.'), - ], - isError: true, - )..failureReason ??= CallToolFailureReason.noRootGiven, - ); - } - final rootUri = Uri.parse(rootUriString); - if (rootUri.scheme != 'file') { - return ( - root: null, - paths: null, - errorResult: CallToolResult( - content: [ - TextContent( - text: - 'Only file scheme uris are allowed for roots, but got ' - '$rootUri', - ), - ], - isError: true, - )..failureReason ??= CallToolFailureReason.invalidRootScheme, - ); - } - - final knownRoot = knownRoots.firstWhereOrNull( - (root) => _isUnderRoot(root, rootUriString, fileSystem), - ); - if (knownRoot == null) { - return ( - root: null, - paths: null, - errorResult: CallToolResult( - content: [ - TextContent( - text: - 'Invalid root $rootUriString, must be under one of the ' - 'registered project roots:\n\n${knownRoots.join('\n')}', - ), - ], - isError: true, - )..failureReason ??= CallToolFailureReason.invalidRootPath, - ); - } - final root = Root(uri: rootUriString); - - final paths = - (rootConfig?[ParameterNames.paths] as List?)?.cast() ?? - defaultPaths; - if (paths != null) { - final invalidPaths = paths.where( - (path) => !_isUnderRoot(root, path, fileSystem), - ); - if (invalidPaths.isNotEmpty) { - return ( - root: null, - paths: null, - errorResult: CallToolResult( - content: [ - TextContent( - text: - 'Paths are not allowed to escape their project root:\n' - '${invalidPaths.join('\n')}', - ), - ], - isError: true, - )..failureReason ??= CallToolFailureReason.invalidPath, - ); - } - } - return (root: root, paths: paths, errorResult: null); -} - -/// Returns 'dart' or 'flutter' based on the pubspec contents. -/// -/// Throws an [ArgumentError] if there is no pubspec. -Future defaultCommandForRoot( - String rootUri, - FileSystem fileSystem, - Sdk sdk, -) async => switch (await inferProjectKind(rootUri, fileSystem)) { - ProjectKind.dart => sdk.dartExecutablePath, - ProjectKind.flutter => sdk.flutterExecutablePath, - ProjectKind.unknown => throw ArgumentError.value( - rootUri, - 'rootUri', - 'Unknown project kind at root $rootUri. All projects must have a ' - 'pubspec.', - ), -}; - -/// Returns whether [uri] is under or exactly equal to [root]. -/// -/// Relative uris will always be under [root] unless they escape it with `../`. -bool _isUnderRoot(Root root, String uri, FileSystem fileSystem) { - // This normalizes the URI to ensure it is treated as a directory (for example - // ensures it ends with a trailing slash). - final rootUri = fileSystem.directory(Uri.parse(root.uri)).uri; - final resolvedUri = rootUri.resolve(uri); - // We don't care about queries or fragments, but the scheme/authority must - // match. - if (rootUri.scheme != resolvedUri.scheme || - rootUri.authority != resolvedUri.authority) { - return false; - } - // Canonicalizing the paths handles any `../` segments and also deals with - // trailing slashes versus no trailing slashes. - - final canonicalRootPath = fileSystem.path.canonicalize(rootUri.path); - final canonicalUriPath = fileSystem.path.canonicalize(resolvedUri.path); - return canonicalRootPath == canonicalUriPath || - fileSystem.path.isWithin(canonicalRootPath, canonicalUriPath); -} - -/// The schema for the `roots` parameter for any tool that accepts it. -ListSchema rootsSchema({bool supportsPaths = false}) => Schema.list( - title: 'The project roots to run this tool in.', - items: Schema.object( - properties: { - ParameterNames.root: rootSchema, - if (supportsPaths) - ParameterNames.paths: Schema.list( - title: - 'Paths to run this tool on. Must resolve to a path that is ' - 'within the "root".', - items: Schema.string(), - ), - }, - required: [ParameterNames.root], - ), -); - -/// The schema for a `root` parameter. -final rootSchema = Schema.string( - title: 'The file URI of the project root to run this tool in.', - description: - 'This must be equal to or a subdirectory of one of the roots ' - 'allowed by the client. Must be a URI with a `file:` ' - 'scheme (e.g. file:///absolute/path/to/root).', -); - -/// A wrapper for a `pubspec.yaml` file, providing access to specific fields. -/// -/// This extension type assumes a valid pubspec structure. -extension type Pubspec(Map _value) { - /// The `dependencies` section of the pubspec. - Iterable get dependencies => - (_value['dependencies'] as Map?)?.values - .cast() ?? - []; - - /// The `dev_dependencies` section of the pubspec. - Iterable get devDependencies => - (_value['dev_dependencies'] as Map?)?.values - .cast() ?? - []; - - /// The `environment` section of the pubspec. - Map? get environment => - _value['environment'] as Map?; - - /// The `flutter` section of the pubspec. - Map? get flutter => - _value['flutter'] as Map?; -} - -/// A dependency entry in a `pubspec.yaml` file. -/// -/// Dependencies can be represented as either a [String] (for version -/// constraints) or a [Map] (for more complex definitions like `sdk` or `path`). -extension type Dependency(Object? _value) { - /// If this is an `sdk` dependency, returns the SDK name; otherwise, `null`. - String? get sdk => _value is Map ? _value['sdk'] as String? : null; -} diff --git a/flutter_launcher_mcp/lib/src/utils/constants.dart b/flutter_launcher_mcp/lib/src/utils/constants.dart deleted file mode 100644 index 1ef1280..0000000 --- a/flutter_launcher_mcp/lib/src/utils/constants.dart +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// A library for shared constants used throughout the MCP server. -library; - -import 'package:dart_mcp/server.dart'; - -/// A namespace for all the parameter names used in MCP tools. -/// -/// This extension on `Never` provides a centralized place to define and access -/// the string constants for tool parameter names, reducing the risk of typos. -extension ParameterNames on Never { - /// The parameter name for a column number. - static const column = 'column'; - - /// The parameter name for a command. - static const command = 'command'; - - /// The parameter name for deleting conflicting outputs. - static const deleteConflictingOutputs = 'delete-conflicting-outputs'; - - /// The parameter name for a directory. - static const directory = 'directory'; - - /// The parameter name for the empty flag. - static const empty = 'empty'; - - /// The parameter name for a line number. - static const line = 'line'; - - /// The parameter name for a name identifier. - static const name = 'name'; - - /// The parameter name for a list of package names. - static const packageNames = 'packageNames'; - - /// The parameter name for a list of paths. - static const paths = 'paths'; - - /// The parameter name for a platform identifier. - static const platform = 'platform'; - - /// The parameter name for a position. - static const position = 'position'; - - /// The parameter name for a project type. - static const projectType = 'projectType'; - - /// The parameter name for a query string. - static const query = 'query'; - - /// The parameter name for a root directory. - static const root = 'root'; - - /// The parameter name for a list of root directories. - static const roots = 'roots'; - - /// The parameter name for a template identifier. - static const template = 'template'; - - /// The parameter name for test runner arguments. - static const testRunnerArgs = 'testRunnerArgs'; - - /// The parameter name for a URI. - static const uri = 'uri'; - - /// The parameter name for a list of URIs. - static const uris = 'uris'; - - /// The parameter name for a user journey. - static const userJourney = 'user_journey'; -} - -/// A shared success response for tools. -final success = CallToolResult(content: [Content.text(text: 'Success')]); diff --git a/flutter_launcher_mcp/lib/src/utils/file_system.dart b/flutter_launcher_mcp/lib/src/utils/file_system.dart deleted file mode 100644 index 21e35ad..0000000 --- a/flutter_launcher_mcp/lib/src/utils/file_system.dart +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// A library for file system access within the MCP server. -library; - -import 'package:file/file.dart'; - -/// An interface that provides access to a [FileSystem] instance. -/// -/// MCP server classes and mixins that interact with the file system should -/// implement this interface. This allows for the injection of a mock -/// [FileSystem] during testing, instead of interacting with the real file -/// system via `dart:io`. -abstract interface class FileSystemSupport { - /// The file system instance to use for all file operations. - FileSystem get fileSystem; -} diff --git a/flutter_launcher_mcp/lib/src/utils/process_manager.dart b/flutter_launcher_mcp/lib/src/utils/process_manager.dart deleted file mode 100644 index 9f05d84..0000000 --- a/flutter_launcher_mcp/lib/src/utils/process_manager.dart +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// A library for managing processes within the MCP server. -library; - -import 'package:process/process.dart'; - -/// An interface that provides access to a [ProcessManager] instance. -/// -/// MCP server classes and mixins that spawn processes should implement this -/// interface. This allows for the injection of a mock [ProcessManager] during -/// testing, instead of making direct calls to `dart:io.Process`. -abstract interface class ProcessManagerSupport { - /// The process manager to use for all process operations. - ProcessManager get processManager; -} diff --git a/flutter_launcher_mcp/lib/src/utils/sdk.dart b/flutter_launcher_mcp/lib/src/utils/sdk.dart deleted file mode 100644 index 70d25c6..0000000 --- a/flutter_launcher_mcp/lib/src/utils/sdk.dart +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// A library for locating and interacting with the Dart and Flutter SDKs. -library; - -import 'dart:convert'; -import 'dart:io'; - -import 'package:dart_mcp/server.dart'; -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:path/path.dart' as p; -import 'package:process/process.dart'; - -/// An interface that provides access to an [Sdk] instance. -/// -/// This provides information about the Dart and Flutter SDKs, if available. -abstract interface class SdkSupport { - /// The SDK instance containing path information. - Sdk get sdk; -} - -/// Information about the Dart and Flutter SDKs. -/// -/// This class provides the paths to the Dart and Flutter SDKs, as well as -/// convenience getters for the executable paths. -class Sdk { - /// The path to the root of the Dart SDK. - String? dartSdkPath; - - /// The path to the root of the Flutter SDK. - String? flutterSdkPath; - - /// Creates a new [Sdk] instance. - Sdk({this.dartSdkPath, this.flutterSdkPath}); - - /// Initializes the SDK paths by attempting to locate the SDKs. - /// - /// This method runs `flutter --version --machine` to find the Flutter SDK, - /// and from that it derives the Dart SDK path. If `flutter` is not in the - /// path or the command fails, the SDK paths will be null. - Future init({ - ProcessManager processManager = const LocalProcessManager(), - FileSystem fileSystem = const LocalFileSystem(), - void Function(LoggingLevel, String)? log, - }) async { - log?.call(LoggingLevel.debug, 'Finding SDKs...'); - try { - final result = await processManager.run([ - 'flutter', - '--version', - '--machine', - ]); - - if (result.exitCode != 0) { - log?.call( - LoggingLevel.warning, - 'Failed to find Flutter SDK: `flutter --version --machine` failed with exit code ${result.exitCode}. ' - 'Please ensure the Flutter SDK is in your path and restart the MCP server.', - ); - return; - } - - final json = jsonDecode(result.stdout as String); - final foundFlutterSdkPath = json['flutterRoot'] as String?; - if (foundFlutterSdkPath == null) { - log?.call( - LoggingLevel.warning, - 'Failed to find flutterRoot in `flutter --version --machine` output.', - ); - return; - } - log?.call( - LoggingLevel.debug, - 'Found Flutter SDK at: $foundFlutterSdkPath', - ); - flutterSdkPath = foundFlutterSdkPath; - - final foundDartSdkPath = p.join( - flutterSdkPath!, - 'bin', - 'cache', - 'dart-sdk', - ); - final versionFile = p.join(foundDartSdkPath, 'version'); - if (!fileSystem.file(versionFile).existsSync()) { - log?.call( - LoggingLevel.warning, - 'Invalid Dart SDK path, no version file found at ${p.join(foundDartSdkPath, 'version')}.', - ); - return; - } - log?.call(LoggingLevel.debug, 'Found Dart SDK at: $foundDartSdkPath'); - dartSdkPath = foundDartSdkPath; - } on ProcessException catch (e) { - log?.call( - LoggingLevel.warning, - 'Failed to find Flutter SDK. The "flutter" command is not in your path or failed to run. ' - 'Please ensure the Flutter SDK is in your path and restart the MCP server. Error: ${e.message}', - ); - } catch (e, s) { - log?.call( - LoggingLevel.warning, - 'Exception while trying to find Flutter SDK: $e\n$s', - ); - } - } - - /// The path to the `dart` executable. - String? get dartExecutablePath => dartSdkPath - ?.child('bin') - .child('dart${Platform.isWindows ? '.exe' : ''}'); - - /// The path to the `flutter` executable. - String? get flutterExecutablePath => flutterSdkPath - ?.child('bin') - .child('flutter${Platform.isWindows ? '.bat' : ''}'); -} - -extension on String { - String child(String path) => p.join(this, path); -} diff --git a/flutter_launcher_mcp/pubspec.lock b/flutter_launcher_mcp/pubspec.lock deleted file mode 100644 index cf5760f..0000000 --- a/flutter_launcher_mcp/pubspec.lock +++ /dev/null @@ -1,469 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - sha256: dd3d2ad434b9510001d089e8de7556d50c834481b9abc2891a0184a8493a19dc - url: "https://pub.dev" - source: hosted - version: "89.0.0" - analyzer: - dependency: transitive - description: - name: analyzer - sha256: c22b6e7726d1f9e5db58c7251606076a71ca0dbcf76116675edfadbec0c9e875 - url: "https://pub.dev" - source: hosted - version: "8.2.0" - args: - dependency: "direct main" - description: - name: args - sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 - url: "https://pub.dev" - source: hosted - version: "2.7.0" - async: - dependency: "direct main" - description: - name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" - source: hosted - version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - cli_config: - dependency: transitive - description: - name: cli_config - sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec - url: "https://pub.dev" - source: hosted - version: "0.2.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - collection: - dependency: "direct main" - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - convert: - dependency: transitive - description: - name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - coverage: - dependency: transitive - description: - name: coverage - sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" - url: "https://pub.dev" - source: hosted - version: "1.15.0" - crypto: - dependency: transitive - description: - name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.dev" - source: hosted - version: "3.0.6" - dart_mcp: - dependency: "direct main" - description: - name: dart_mcp - sha256: "2b34cbd60e3b64e7f770a453b84c5f569f096f6044ea343ddffc99f6b366de45" - url: "https://pub.dev" - source: hosted - version: "0.3.3" - fake_async: - dependency: "direct dev" - description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" - source: hosted - version: "1.3.3" - file: - dependency: "direct main" - description: - name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" - source: hosted - version: "7.0.1" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 - url: "https://pub.dev" - source: hosted - version: "4.0.0" - glob: - dependency: transitive - description: - name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de - url: "https://pub.dev" - source: hosted - version: "2.1.3" - http: - dependency: transitive - description: - name: http - sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 - url: "https://pub.dev" - source: hosted - version: "1.5.0" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - io: - dependency: transitive - description: - name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.dev" - source: hosted - version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" - json_rpc_2: - dependency: transitive - description: - name: json_rpc_2 - sha256: "3c46c2633aec07810c3d6a2eb08d575b5b4072980db08f1344e66aeb53d6e4a7" - url: "https://pub.dev" - source: hosted - version: "4.0.0" - lints: - dependency: "direct dev" - description: - name: lints - sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 - url: "https://pub.dev" - source: hosted - version: "6.0.0" - logging: - dependency: transitive - description: - name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" - source: hosted - version: "1.3.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - meta: - dependency: transitive - description: - name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" - source: hosted - version: "1.17.0" - mime: - dependency: transitive - description: - name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - node_preamble: - dependency: transitive - description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - package_config: - dependency: transitive - description: - name: package_config - sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc - url: "https://pub.dev" - source: hosted - version: "2.2.0" - path: - dependency: "direct main" - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - pool: - dependency: transitive - description: - name: pool - sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" - url: "https://pub.dev" - source: hosted - version: "1.5.2" - process: - dependency: "direct main" - description: - name: process - sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 - url: "https://pub.dev" - source: hosted - version: "5.0.5" - pub_semver: - dependency: transitive - description: - name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - shelf: - dependency: transitive - description: - name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 - url: "https://pub.dev" - source: hosted - version: "1.4.2" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 - url: "https://pub.dev" - source: hosted - version: "1.1.3" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b - url: "https://pub.dev" - source: hosted - version: "2.1.2" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" - url: "https://pub.dev" - source: hosted - version: "0.10.13" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: "direct main" - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - stream_transform: - dependency: transitive - description: - name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 - url: "https://pub.dev" - source: hosted - version: "2.1.1" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test: - dependency: "direct dev" - description: - name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" - url: "https://pub.dev" - source: hosted - version: "1.26.3" - test_api: - dependency: transitive - description: - name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.dev" - source: hosted - version: "0.7.7" - test_core: - dependency: transitive - description: - name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" - url: "https://pub.dev" - source: hosted - version: "0.6.12" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - unified_analytics: - dependency: "direct main" - description: - name: unified_analytics - sha256: "8d1429a4b27320a9c4fc854287d18c8fde1549bf622165c5837202a9f370b53d" - url: "https://pub.dev" - source: hosted - version: "8.0.5" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" - source: hosted - version: "15.0.2" - watcher: - dependency: transitive - description: - name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" - url: "https://pub.dev" - source: hosted - version: "1.1.4" - web: - dependency: transitive - description: - name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" - url: "https://pub.dev" - source: hosted - version: "1.1.1" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 - url: "https://pub.dev" - source: hosted - version: "3.0.3" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - yaml: - dependency: "direct main" - description: - name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.dev" - source: hosted - version: "3.1.3" -sdks: - dart: ">=3.9.0 <4.0.0" diff --git a/flutter_launcher_mcp/pubspec.yaml b/flutter_launcher_mcp/pubspec.yaml deleted file mode 100644 index 641c5d4..0000000 --- a/flutter_launcher_mcp/pubspec.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: flutter_launcher_mcp -description: An MCP server for launching and managing Flutter applications. -version: 0.2.2 -environment: - sdk: ^3.9.0 -dependencies: - args: ^2.7.0 - async: ^2.13.0 - collection: ^1.19.1 - dart_mcp: ^0.3.3 - file: ^7.0.1 - path: ^1.9.1 - process: ^5.0.5 - stream_channel: ^2.1.4 - unified_analytics: ^8.0.5 - yaml: ^3.1.3 -dev_dependencies: - fake_async: ^1.3.3 - lints: ^6.0.0 - test: ^1.25.6 diff --git a/flutter_launcher_mcp/test/sdk_test.dart b/flutter_launcher_mcp/test/sdk_test.dart deleted file mode 100644 index 6564ce5..0000000 --- a/flutter_launcher_mcp/test/sdk_test.dart +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_launcher_mcp/src/utils/sdk.dart'; -import 'package:process/process.dart'; -import 'package:test/test.dart' as test; - -void main() { - test.group('Sdk', () { - late MockProcessManager mockProcessManager; - late MemoryFileSystem fileSystem; - - test.setUp(() { - mockProcessManager = MockProcessManager(); - fileSystem = MemoryFileSystem(); - }); - - test.test('create returns Sdk with paths when flutter is found', () async { - mockProcessManager.addCommand( - Command([ - 'flutter', - '--version', - '--machine', - ], stdout: jsonEncode({'flutterRoot': '/path/to/flutter/sdk'})), - ); - fileSystem - .file('/path/to/flutter/sdk/bin/cache/dart-sdk/version') - .createSync(recursive: true); - final sdk = Sdk(); - await sdk.init( - processManager: mockProcessManager, - fileSystem: fileSystem, - ); - - test.expect(sdk.flutterSdkPath, '/path/to/flutter/sdk'); - test.expect(sdk.dartSdkPath, '/path/to/flutter/sdk/bin/cache/dart-sdk'); - test.expect( - sdk.flutterExecutablePath, - '/path/to/flutter/sdk/bin/flutter', - ); - test.expect( - sdk.dartExecutablePath, - '/path/to/flutter/sdk/bin/cache/dart-sdk/bin/dart', - ); - }); - - test.test( - 'create returns Sdk with null paths when flutter is not found', - () async { - mockProcessManager.addCommand( - Command([ - 'flutter', - '--version', - '--machine', - ], exitCode: Future.value(1)), - ); - final sdk = Sdk(); - await sdk.init( - processManager: mockProcessManager, - fileSystem: fileSystem, - ); - - test.expect(sdk.flutterSdkPath, test.isNull); - test.expect(sdk.dartSdkPath, test.isNull); - test.expect(sdk.flutterExecutablePath, test.isNull); - test.expect(sdk.dartExecutablePath, test.isNull); - }, - ); - - test.test( - 'create returns Sdk with null paths when flutterRoot is missing', - () async { - mockProcessManager.addCommand( - Command([ - 'flutter', - '--version', - '--machine', - ], stdout: jsonEncode({'someOtherKey': '/path/to/flutter/sdk'})), - ); - final sdk = Sdk(); - await sdk.init( - processManager: mockProcessManager, - fileSystem: fileSystem, - ); - - test.expect(sdk.flutterSdkPath, test.isNull); - test.expect(sdk.dartSdkPath, test.isNull); - }, - ); - - test.test( - 'create returns Sdk with null dartSdkPath when version file is missing', - () async { - mockProcessManager.addCommand( - Command([ - 'flutter', - '--version', - '--machine', - ], stdout: jsonEncode({'flutterRoot': '/path/to/flutter/sdk'})), - ); - // Do not create the version file in the mock file system. - - final sdk = Sdk(); - await sdk.init( - processManager: mockProcessManager, - fileSystem: fileSystem, - ); - - test.expect(sdk.flutterSdkPath, '/path/to/flutter/sdk'); - test.expect(sdk.dartSdkPath, test.isNull); - }, - ); - }); -} - -class Command { - final List command; - final String? stdout; - final String? stderr; - final Future? exitCode; - final int pid; - - Command( - this.command, { - this.stdout, - this.stderr, - this.exitCode, - this.pid = 12345, - }); -} - -class MockProcessManager implements ProcessManager { - final List _commands = []; - final List> commands = []; - final Map runningProcesses = {}; - bool shouldThrowOnStart = false; - bool killResult = true; - final killedPids = []; - int _pidCounter = 12345; - - void addCommand(Command command) { - _commands.add(command); - } - - void completeExitCodeForProcess(int pid, int exitCode) { - runningProcesses[pid]?.completeExitCode(exitCode); - } - - Command _findCommand(List command) { - for (final cmd in _commands) { - if (const ListEquality().equals(cmd.command, command)) { - return cmd; - } - } - throw Exception('Command not mocked: $command'); - } - - @override - Future start( - List command, { - String? workingDirectory, - Map? environment, - bool includeParentEnvironment = true, - bool runInShell = false, - ProcessStartMode mode = ProcessStartMode.normal, - }) async { - if (shouldThrowOnStart) { - throw Exception('Failed to start process'); - } - commands.add(command); - final mockCommand = _findCommand(command); - final pid = mockCommand.pid == 12345 ? _pidCounter++ : mockCommand.pid; - final process = MockProcess( - stdout: Stream.value(utf8.encode(mockCommand.stdout ?? '')), - stderr: Stream.value(utf8.encode(mockCommand.stderr ?? '')), - pid: pid, - exitCodeFuture: mockCommand.exitCode, - ); - runningProcesses[pid] = process; - return process; - } - - @override - bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]) { - killedPids.add(pid); - runningProcesses[pid]?.kill(); - return killResult; - } - - @override - Future run( - List command, { - String? workingDirectory, - Map? environment, - bool includeParentEnvironment = true, - bool runInShell = false, - Encoding? stdoutEncoding = systemEncoding, - Encoding? stderrEncoding = systemEncoding, - }) async { - commands.add(command); - final mockCommand = _findCommand(command); - return ProcessResult( - mockCommand.pid, - await (mockCommand.exitCode ?? Future.value(0)), - mockCommand.stdout ?? '', - mockCommand.stderr ?? '', - ); - } - - @override - bool canRun(executable, {String? workingDirectory}) => true; - - @override - ProcessResult runSync( - List command, { - String? workingDirectory, - Map? environment, - bool includeParentEnvironment = true, - bool runInShell = false, - Encoding? stdoutEncoding = systemEncoding, - Encoding? stderrEncoding = systemEncoding, - }) { - throw UnimplementedError(); - } -} - -class MockProcess implements Process { - @override - final Stream> stdout; - @override - final Stream> stderr; - @override - final int pid; - - @override - late final Future exitCode; - final Completer exitCodeCompleter = Completer(); - - bool killed = false; - - MockProcess({ - required this.stdout, - required this.stderr, - required this.pid, - Future? exitCodeFuture, - }) { - exitCode = exitCodeFuture ?? exitCodeCompleter.future; - } - - void completeExitCode(int code) { - if (!exitCodeCompleter.isCompleted) { - exitCodeCompleter.complete(code); - } - } - - @override - bool kill([ProcessSignal signal = ProcessSignal.sigterm]) { - killed = true; - if (!exitCodeCompleter.isCompleted) { - exitCodeCompleter.complete(-9); // SIGKILL - } - return true; - } - - @override - late final IOSink stdin = throw UnimplementedError(); -} diff --git a/flutter_launcher_mcp/test/server_test.dart b/flutter_launcher_mcp/test/server_test.dart deleted file mode 100644 index acc9fee..0000000 --- a/flutter_launcher_mcp/test/server_test.dart +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:dart_mcp/client.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_launcher_mcp/src/server.dart'; -import 'package:flutter_launcher_mcp/src/utils/sdk.dart'; -import 'package:process/process.dart'; -import 'package:stream_channel/stream_channel.dart'; -import 'package:test/test.dart' as test; - -import 'sdk_test.dart'; - -void main() { - test.group('FlutterLauncherMCPServer', () { - late MemoryFileSystem fileSystem; - - Future<({FlutterLauncherMCPServer server, ServerConnection client})> - createServerAndClient({ - required ProcessManager processManager, - required MemoryFileSystem fileSystem, - }) async { - final channel = StreamChannelController(); - final server = FlutterLauncherMCPServer( - channel.local, - sdk: Sdk( - flutterSdkPath: '/path/to/flutter/sdk', - dartSdkPath: '/path/to/flutter/sdk/bin/cache/dart-sdk', - ), - processManager: processManager, - fileSystem: fileSystem, - ); - final client = ServerConnection.fromStreamChannel(channel.foreign); - return (server: server, client: client); - } - - test.setUp(() { - fileSystem = MemoryFileSystem(); - }); - - test.test('launch_app tool returns DTD URI and PID on success', () async { - final dtdUri = 'ws://127.0.0.1:12345/abcdefg='; - final processPid = 54321; - final mockProcessManager = MockProcessManager(); - mockProcessManager.addCommand( - Command( - [ - '/path/to/flutter/sdk/bin/flutter', - 'run', - '--print-dtd', - '--device-id', - 'test-device', - ], - stdout: 'The Dart Tooling Daemon is available at: $dtdUri\n', - pid: processPid, - ), - ); - final serverAndClient = await createServerAndClient( - processManager: mockProcessManager, - fileSystem: fileSystem, - ); - final server = serverAndClient.server; - final client = serverAndClient.client; - - // Initialize - final initResult = await client.initialize( - InitializeRequest( - protocolVersion: ProtocolVersion.latestSupported, - capabilities: ClientCapabilities(), - clientInfo: Implementation(name: 'test_client', version: '1.0.0'), - ), - ); - test.expect(initResult.serverInfo.name, 'Flutter Launcher MCP Server'); - client.notifyInitialized(); - - // Call the tool - final result = await client.callTool( - CallToolRequest( - name: 'launch_app', - arguments: { - 'root': - '/Users/gspencer/code/gemini-cli-extension/flutter_launcher_mcp', - 'device': 'test-device', - }, - ), - ); - - test.expect(result.isError, test.isNot(true)); - test.expect(result.structuredContent, { - 'dtdUri': dtdUri, - 'pid': processPid, - }); - await server.shutdown(); - await client.shutdown(); - }); - - test.test( - 'launch_app tool returns DTD URI and PID on success from stderr', - () async { - final dtdUri = 'ws://127.0.0.1:12345/abcdefg='; - final processPid = 54321; - final mockProcessManager = MockProcessManager(); - mockProcessManager.addCommand( - Command( - [ - '/path/to/flutter/sdk/bin/flutter', - 'run', - '--print-dtd', - '--device-id', - 'test-device', - ], - stderr: 'The Dart Tooling Daemon is available at: $dtdUri\n', - pid: processPid, - ), - ); - final serverAndClient = await createServerAndClient( - processManager: mockProcessManager, - fileSystem: fileSystem, - ); - final server = serverAndClient.server; - final client = serverAndClient.client; - - // Initialize - final initResult = await client.initialize( - InitializeRequest( - protocolVersion: ProtocolVersion.latestSupported, - capabilities: ClientCapabilities(), - clientInfo: Implementation(name: 'test_client', version: '1.0.0'), - ), - ); - test.expect(initResult.serverInfo.name, 'Flutter Launcher MCP Server'); - client.notifyInitialized(); - - // Call the tool - final result = await client.callTool( - CallToolRequest( - name: 'launch_app', - arguments: { - 'root': - '/Users/gspencer/code/gemini-cli-extension/flutter_launcher_mcp', - 'device': 'test-device', - }, - ), - ); - - test.expect(result.isError, test.isNot(true)); - test.expect(result.structuredContent, { - 'dtdUri': dtdUri, - 'pid': processPid, - }); - await server.shutdown(); - await client.shutdown(); - }, - ); - }); -} diff --git a/gemini-extension.json b/gemini-extension.json index 12dcb67..28f7204 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,7 +1,7 @@ { "name": "flutter", "description": "Enables several Flutter and Dart-related commands and context.", - "version": "0.2.2", + "version": "0.3.0", "contextFileName": "flutter.md", "mcpServers": { "dart": { @@ -9,10 +9,6 @@ "args": [ "mcp-server" ] - }, - "flutter_launcher": { - "command": "${extensionPath}${/}flutter_launcher_mcp.exe", - "args": [] } } } diff --git a/scripts/build_release.ps1 b/scripts/build_release.ps1 index 2d04b9d..bc83b48 100644 --- a/scripts/build_release.ps1 +++ b/scripts/build_release.ps1 @@ -1,23 +1,7 @@ if (-not $env:GITHUB_REF) { $env:GITHUB_REF = "refs/tags/HEAD" } $tagName = $env:GITHUB_REF.Substring($env:GITHUB_REF.LastIndexOf("/") + 1) $archiveName = "win32.flutter.zip" -$exeFile = "flutter_launcher_mcp.exe" -Push-Location flutter_launcher_mcp -dart pub get -$version = (Get-Content pubspec.yaml | Select-String -Pattern "version:\s*(\S+)" | ForEach-Object { $_.Matches[0].Groups[1].Value }) -dart compile exe bin/flutter_launcher_mcp.dart -o "../$exeFile" --define=FLUTTER_LAUNCHER_VERSION=$version -Pop-Location - -$tempDir = "temp_archive" -New-Item -ItemType Directory -Path $tempDir - -git archive --format=zip -o temp.zip $tagName gemini-extension.json commands/ LICENSE README.md flutter.md -Expand-Archive -Path temp.zip -DestinationPath $tempDir -Move-Item $exeFile $tempDir -Compress-Archive -Path "$tempDir\*" -DestinationPath $archiveName - -Remove-Item -Path "temp.zip" -Force -Remove-Item -Path $tempDir -Recurse -Force +git archive --format=zip -o $archiveName $tagName gemini-extension.json commands/ LICENSE README.md flutter.md echo "ARCHIVE_NAME=$archiveName" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append diff --git a/scripts/build_release.sh b/scripts/build_release.sh index 9bd43af..da1e019 100755 --- a/scripts/build_release.sh +++ b/scripts/build_release.sh @@ -23,24 +23,7 @@ else fi archive_name="$os.$arch.flutter.tar" -exe_file="flutter_launcher_mcp.exe" -trap 'rm -f "$compile_log"' EXIT -compile_log="$(mktemp --tmpdir compile_log_XXXX)" - -function build_exe() ( - CDPATH= cd flutter_launcher_mcp && \ - dart pub get && \ - version=$(yq -r '.version' pubspec.yaml) && \ - dart compile exe bin/flutter_launcher_mcp.dart -o "../$exe_file" --define=FLUTTER_LAUNCHER_VERSION=$version 2>&1 > "$compile_log" -) - -build_exe || \ - (echo "Failed to compile $exe_file"; \ - cat "$compile_log"; \ - rm -f "$compile_log"; \ - exit 1) - -rm -f "$compile_log" "$archive_name" +rm -f "$archive_name" # Create the archive of the extension sources that are in the git ref. git archive --format=tar -o "$archive_name" "$tag_name" \ @@ -50,9 +33,6 @@ git archive --format=tar -o "$archive_name" "$tag_name" \ README.md \ flutter.md -# Append the compiled kernel file to the archive. -tar --append --file="$archive_name" "$exe_file" -rm -f "$exe_file" gzip --force "$archive_name" archive_name="${archive_name}.gz" diff --git a/scripts/bump_version.sh b/scripts/bump_version.sh index 2b0998b..904c58f 100755 --- a/scripts/bump_version.sh +++ b/scripts/bump_version.sh @@ -18,9 +18,6 @@ REPO_ROOT="$(dirname "$SCRIPT_DIR")" # Update gemini-extension.json jq --arg version "$NEW_VERSION" '.version = $version' "$REPO_ROOT/gemini-extension.json" > "$REPO_ROOT/gemini-extension.json.tmp" && command mv -f "$REPO_ROOT/gemini-extension.json.tmp" "$REPO_ROOT/gemini-extension.json" -# Update pubspec.yaml -yq -y ".version = \"$NEW_VERSION\"" "$REPO_ROOT/flutter_launcher_mcp/pubspec.yaml" > "$REPO_ROOT/flutter_launcher_mcp/pubspec.yaml.tmp" && mv "$REPO_ROOT/flutter_launcher_mcp/pubspec.yaml.tmp" "$REPO_ROOT/flutter_launcher_mcp/pubspec.yaml" - # Check and update CHANGELOG.md CHANGELOG_FILE="$REPO_ROOT/CHANGELOG.md" if ! grep -q "## $NEW_VERSION" "$CHANGELOG_FILE"; then @@ -36,8 +33,4 @@ if ! grep -q "## $NEW_VERSION" "$CHANGELOG_FILE"; then mv "$TEMP_FILE" "$CHANGELOG_FILE" fi -# Update README.md -sed -i.bak 's/ flutter_launcher_mcp: \^.*/ flutter_launcher_mcp: ^'"$NEW_VERSION/g" "$REPO_ROOT/flutter_launcher_mcp/README.md" && \ - rm "$REPO_ROOT/flutter_launcher_mcp/README.md.bak" - echo "Version bumped to $NEW_VERSION" \ No newline at end of file