| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async' show Completer, FutureOr; |
| import 'dart:convert' show LineSplitter, utf8; |
| import 'dart:io' show Process; |
| |
| import 'package:dart_mcp/server.dart'; |
| import 'package:process_runner/process_runner.dart'; |
| |
| /// An [MCPServer] that provides tools an agent would need to develop the |
| /// Flutter engine. |
| final class EngineServer extends MCPServer with ToolsSupport { |
| /// Create an [EngineServer] that is communicating over [stream]. |
| /// [processRunner] can be supplied for testing, to mock out subprocesses. |
| EngineServer.fromStreamChannel(super.stream, {ProcessRunner? processRunner}) |
| : _processRunner = processRunner ?? ProcessRunner(), |
| super.fromStreamChannel( |
| implementation: Implementation(name: 'engine mcp', version: '0.0.1'), |
| instructions: '', |
| ); |
| |
| final ProcessRunner _processRunner; |
| |
| final _engineBuildHelp = Tool( |
| name: 'engine_build_help', |
| description: 'Get help for the building tool and a list of configs.', |
| inputSchema: Schema.object(), |
| annotations: ToolAnnotations(readOnlyHint: true), |
| ); |
| |
| final _engineBuild = Tool( |
| name: 'engine_build', |
| description: 'Build an engine target. This is potentially a long running process.', |
| inputSchema: Schema.object( |
| properties: { |
| 'config': Schema.string(description: 'The config to build.'), |
| 'target': Schema.string(description: 'The specific target to build (optional).'), |
| }, |
| ), |
| annotations: ToolAnnotations(readOnlyHint: true), |
| ); |
| |
| final _engineListTargets = Tool( |
| name: 'engine_list_targets', |
| description: 'Lists build targets for a given config.', |
| inputSchema: Schema.object( |
| properties: {'config': Schema.string(description: 'The build config to query.')}, |
| ), |
| annotations: ToolAnnotations(readOnlyHint: true), |
| ); |
| |
| Future<CallToolResult> _doEngineListTargets(CallToolRequest request) async { |
| try { |
| final config = request.arguments!['config'] as String?; |
| final arguments = <String>['./third_party/gn/gn', 'ls', '../out/$config']; |
| |
| final ProcessRunnerResult result = await _processRunner.runProcess(arguments); |
| final String output = result.stdout; |
| |
| return CallToolResult(content: [TextContent(text: output)]); |
| } catch (err) { |
| return CallToolResult(isError: true, content: [TextContent(text: err.toString())]); |
| } |
| } |
| |
| Future<CallToolResult> _doEngineBuildHelp(CallToolRequest request) async { |
| try { |
| final arguments = <String>['./bin/et', 'build', '--help']; |
| final ProcessRunnerResult result = await _processRunner.runProcess(arguments); |
| final String output = result.stdout; |
| |
| return CallToolResult(content: [TextContent(text: output)]); |
| } catch (ex) { |
| return CallToolResult(isError: true, content: [TextContent(text: ex.toString())]); |
| } |
| } |
| |
| Future<CallToolResult> _doEngineBuild(CallToolRequest request) async { |
| try { |
| final config = request.arguments!['config'] as String?; |
| final target = request.arguments!['target'] as String?; |
| final List<String> arguments = ['./bin/et', 'build', '-c', config!]; |
| if (target != null) { |
| arguments.add(target); |
| } |
| |
| final completer = Completer<void>(); |
| void streamDone() { |
| completer.complete(); |
| } |
| |
| final Process process = await _processRunner.processManager.start(arguments); |
| |
| process.stdout |
| .transform(utf8.decoder) |
| .transform(const LineSplitter()) |
| .listen( |
| (String line) { |
| // TODO(gaaclarke): We should be sending progress notifications |
| // here. See https://modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress. |
| }, |
| onDone: streamDone, |
| onError: (error) { |
| streamDone(); |
| }, |
| ); |
| |
| final int exitCode = await process.exitCode; |
| await completer.future; |
| |
| if (exitCode == 0) { |
| return CallToolResult(content: [TextContent(text: 'Build succeeded.')]); |
| } else { |
| return CallToolResult(content: [TextContent(text: 'Build failed.')]); |
| } |
| } catch (ex) { |
| return CallToolResult(isError: true, content: [TextContent(text: ex.toString())]); |
| } |
| } |
| |
| @override |
| FutureOr<InitializeResult> initialize(InitializeRequest request) { |
| registerTool(_engineBuildHelp, _doEngineBuildHelp); |
| registerTool(_engineBuild, _doEngineBuild); |
| registerTool(_engineListTargets, _doEngineListTargets); |
| return super.initialize(request); |
| } |
| } |