Skip to content

Commit a29274b

Browse files
authored
Provide output logs when the process launch fails. (#293)
1 parent d33ed02 commit a29274b

File tree

3 files changed

+310
-3
lines changed

3 files changed

+310
-3
lines changed

pkgs/dart_mcp_server/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
* Add the `list_devices`, `launch_app`, `get_app_logs`, and `list_running_apps`
2020
tools for running Flutter apps.
2121
* Add the `hot_restart` tool for restarting running Flutter apps.
22+
* Add extra log output to failed launches, and allow AI to specify the maxLines
23+
of log output.
2224

2325
# 0.1.0 (Dart SDK 3.9.0)
2426

pkgs/dart_mcp_server/lib/src/mixins/flutter_launcher.dart

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ library;
88
import 'dart:async';
99
import 'dart:convert';
1010
import 'dart:io';
11+
import 'dart:math' as math;
1112

1213
import 'package:dart_mcp/server.dart';
1314

@@ -170,13 +171,21 @@ base mixin FlutterLauncherSupport
170171
LoggingLevel.info,
171172
'Flutter application ${process!.pid} exited with code $exitCode.',
172173
);
173-
_runningApps.remove(process.pid);
174174
if (!completer.isCompleted) {
175+
final logs = _runningApps[process.pid]?.logs ?? [];
176+
// Only output the last 500 lines of logs.
177+
final startLine = math.max(0, logs.length - 500);
178+
final logOutput = [
179+
if (startLine > 0) '[skipping $startLine log lines]...',
180+
...logs.sublist(startLine),
181+
];
175182
completer.completeError(
176183
'Flutter application exited with code $exitCode before the DTD '
177-
'URI was found.',
184+
'URI was found, with log output:\n${logOutput.join('\n')}',
178185
);
179186
}
187+
_runningApps.remove(process.pid);
188+
180189
// Cancel subscriptions after all processing is done.
181190
await stdoutSubscription.cancel();
182191
await stderrSubscription.cancel();
@@ -360,6 +369,11 @@ base mixin FlutterLauncherSupport
360369
'The process ID of the flutter run process running the '
361370
'application.',
362371
),
372+
'maxLines': Schema.int(
373+
description:
374+
'The maximum number of log lines to return from the end of the '
375+
'logs. Defaults to 500. If set to -1, all logs will be returned.',
376+
),
363377
},
364378
required: ['pid'],
365379
),
@@ -376,8 +390,9 @@ base mixin FlutterLauncherSupport
376390

377391
Future<CallToolResult> _getAppLogs(CallToolRequest request) async {
378392
final pid = request.arguments!['pid'] as int;
393+
var maxLines = request.arguments!['maxLines'] as int? ?? 500;
379394
log(LoggingLevel.info, 'Getting logs for application with PID: $pid');
380-
final logs = _runningApps[pid]?.logs;
395+
var logs = _runningApps[pid]?.logs;
381396

382397
if (logs == null) {
383398
log(
@@ -394,6 +409,17 @@ base mixin FlutterLauncherSupport
394409
);
395410
}
396411

412+
if (maxLines == -1) {
413+
maxLines = logs.length;
414+
}
415+
if (maxLines > 0 && maxLines <= logs.length) {
416+
final startLine = logs.length - maxLines;
417+
logs = [
418+
if (startLine > 0) '[skipping $startLine log lines]...',
419+
...logs.sublist(startLine),
420+
];
421+
}
422+
397423
return CallToolResult(
398424
content: [TextContent(text: logs.join('\n'))],
399425
structuredContent: {'logs': logs},

pkgs/dart_mcp_server/test/tools/flutter_launcher_test.dart

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,285 @@ void main() {
197197
await client.shutdown();
198198
},
199199
);
200+
201+
test.test('launch_app tool fails when process exits early', () async {
202+
final mockProcessManager = MockProcessManager();
203+
mockProcessManager.addCommand(
204+
Command(
205+
[
206+
Platform.isWindows
207+
? r'C:\path\to\flutter\sdk\bin\cache\dart-sdk\bin\dart.exe'
208+
: '/path/to/flutter/sdk/bin/cache/dart-sdk/bin/dart',
209+
'language-server',
210+
'--protocol',
211+
'lsp',
212+
],
213+
stdout:
214+
'''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''',
215+
),
216+
);
217+
mockProcessManager.addCommand(
218+
Command(
219+
[
220+
Platform.isWindows
221+
? r'C:\path\to\flutter\sdk\bin\flutter.bat'
222+
: '/path/to/flutter/sdk/bin/flutter',
223+
'run',
224+
'--print-dtd',
225+
'--device-id',
226+
'test-device',
227+
],
228+
stderr: 'Something went wrong',
229+
exitCode: Future.value(1),
230+
),
231+
);
232+
final serverAndClient = await createServerAndClient(
233+
processManager: mockProcessManager,
234+
fileSystem: fileSystem,
235+
);
236+
final server = serverAndClient.server;
237+
final client = serverAndClient.client;
238+
239+
// Initialize
240+
await client.initialize(
241+
InitializeRequest(
242+
protocolVersion: ProtocolVersion.latestSupported,
243+
capabilities: ClientCapabilities(),
244+
clientInfo: Implementation(name: 'test_client', version: '1.0.0'),
245+
),
246+
);
247+
client.notifyInitialized();
248+
249+
// Call the tool
250+
final result = await client.callTool(
251+
CallToolRequest(
252+
name: 'launch_app',
253+
arguments: {'root': projectRoot, 'device': 'test-device'},
254+
),
255+
);
256+
257+
test.expect(result.isError, true);
258+
final textOutput = result.content as List<TextContent>;
259+
test.expect(
260+
textOutput.map((context) => context.text).toList().join('\n'),
261+
test.stringContainsInOrder([
262+
'Flutter application exited with code 1 before the DTD URI was found',
263+
'with log output',
264+
'Something went wrong',
265+
]),
266+
);
267+
await server.shutdown();
268+
await client.shutdown();
269+
});
270+
271+
test.test('stop_app tool stops a running app', () async {
272+
final dtdUri = 'ws://127.0.0.1:12345/abcdefg=';
273+
final processPid = 54321;
274+
final mockProcessManager = MockProcessManager();
275+
mockProcessManager.addCommand(
276+
Command(
277+
[
278+
Platform.isWindows
279+
? r'C:\path\to\flutter\sdk\bin\cache\dart-sdk\bin\dart.exe'
280+
: '/path/to/flutter/sdk/bin/cache/dart-sdk/bin/dart',
281+
'language-server',
282+
'--protocol',
283+
'lsp',
284+
],
285+
stdout:
286+
'''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}
287+
''',
288+
),
289+
);
290+
mockProcessManager.addCommand(
291+
Command(
292+
[
293+
Platform.isWindows
294+
? r'C:\path\to\flutter\sdk\bin\flutter.bat'
295+
: '/path/to/flutter/sdk/bin/flutter',
296+
'run',
297+
'--print-dtd',
298+
'--device-id',
299+
'test-device',
300+
],
301+
stdout: 'The Dart Tooling Daemon is available at: $dtdUri\n',
302+
pid: processPid,
303+
),
304+
);
305+
final serverAndClient = await createServerAndClient(
306+
processManager: mockProcessManager,
307+
fileSystem: fileSystem,
308+
);
309+
final server = serverAndClient.server;
310+
final client = serverAndClient.client;
311+
312+
// Initialize and launch the app
313+
await client.initialize(
314+
InitializeRequest(
315+
protocolVersion: ProtocolVersion.latestSupported,
316+
capabilities: ClientCapabilities(),
317+
clientInfo: Implementation(name: 'test_client', version: '1.0.0'),
318+
),
319+
);
320+
client.notifyInitialized();
321+
await client.callTool(
322+
CallToolRequest(
323+
name: 'launch_app',
324+
arguments: {'root': projectRoot, 'device': 'test-device'},
325+
),
326+
);
327+
328+
// Stop the app
329+
final result = await client.callTool(
330+
CallToolRequest(name: 'stop_app', arguments: {'pid': processPid}),
331+
);
332+
333+
test.expect(result.isError, test.isNot(true));
334+
test.expect(result.structuredContent, {'success': true});
335+
test.expect(mockProcessManager.killedPids, [processPid]);
336+
await server.shutdown();
337+
await client.shutdown();
338+
});
339+
340+
test.test('get_app_logs tool respects maxLines', () async {
341+
final dtdUri = 'ws://127.0.0.1:12345/abcdefg=';
342+
final processPid = 54321;
343+
final mockProcessManager = MockProcessManager();
344+
mockProcessManager.addCommand(
345+
Command(
346+
[
347+
Platform.isWindows
348+
? r'C:\path\to\flutter\sdk\bin\cache\dart-sdk\bin\dart.exe'
349+
: '/path/to/flutter/sdk/bin/cache/dart-sdk/bin/dart',
350+
'language-server',
351+
'--protocol',
352+
'lsp',
353+
],
354+
stdout:
355+
'''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}''',
356+
),
357+
);
358+
mockProcessManager.addCommand(
359+
Command(
360+
[
361+
Platform.isWindows
362+
? r'C:\path\to\flutter\sdk\bin\flutter.bat'
363+
: '/path/to/flutter/sdk/bin/flutter',
364+
'run',
365+
'--print-dtd',
366+
'--device-id',
367+
'test-device',
368+
],
369+
stdout:
370+
'line 1\nline 2\nline 3\n'
371+
'The Dart Tooling Daemon is available at: $dtdUri\n',
372+
pid: processPid,
373+
),
374+
);
375+
final serverAndClient = await createServerAndClient(
376+
processManager: mockProcessManager,
377+
fileSystem: fileSystem,
378+
);
379+
final server = serverAndClient.server;
380+
final client = serverAndClient.client;
381+
382+
// Initialize and launch the app
383+
await client.initialize(
384+
InitializeRequest(
385+
protocolVersion: ProtocolVersion.latestSupported,
386+
capabilities: ClientCapabilities(),
387+
clientInfo: Implementation(name: 'test_client', version: '1.0.0'),
388+
),
389+
);
390+
client.notifyInitialized();
391+
await client.callTool(
392+
CallToolRequest(
393+
name: 'launch_app',
394+
arguments: {'root': projectRoot, 'device': 'test-device'},
395+
),
396+
);
397+
398+
// Get the logs
399+
final result = await client.callTool(
400+
CallToolRequest(
401+
name: 'get_app_logs',
402+
arguments: {'pid': processPid, 'maxLines': 2},
403+
),
404+
);
405+
406+
test.expect(result.isError, test.isNot(true));
407+
test.expect(result.structuredContent, {
408+
'logs': [
409+
'[skipping 2 log lines]...',
410+
'[stdout] line 3',
411+
'[stdout] The Dart Tooling Daemon is available at: ws://127.0.0.1:12345/abcdefg=',
412+
],
413+
});
414+
await server.shutdown();
415+
await client.shutdown();
416+
});
417+
418+
test.test('list_devices tool returns available devices', () async {
419+
final mockProcessManager = MockProcessManager();
420+
mockProcessManager.addCommand(
421+
Command(
422+
[
423+
Platform.isWindows
424+
? r'C:\path\to\flutter\sdk\bin\cache\dart-sdk\bin\dart.exe'
425+
: '/path/to/flutter/sdk/bin/cache/dart-sdk/bin/dart',
426+
'language-server',
427+
'--protocol',
428+
'lsp',
429+
],
430+
stdout:
431+
'''Content-Length: 145\r\n\r\n{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"workspace":{"workspaceFolders":{"supported":true,"changeNotifications":true}},"workspaceSymbolProvider":true}}}
432+
''',
433+
),
434+
);
435+
mockProcessManager.addCommand(
436+
Command(
437+
[
438+
Platform.isWindows
439+
? r'C:\path\to\flutter\sdk\bin\flutter.bat'
440+
: '/path/to/flutter/sdk/bin/flutter',
441+
'devices',
442+
'--machine',
443+
],
444+
stdout: jsonEncode([
445+
{'id': 'test-device-1'},
446+
{'id': 'test-device-2'},
447+
]),
448+
),
449+
);
450+
final serverAndClient = await createServerAndClient(
451+
processManager: mockProcessManager,
452+
fileSystem: fileSystem,
453+
);
454+
final server = serverAndClient.server;
455+
final client = serverAndClient.client;
456+
457+
// Initialize
458+
await client.initialize(
459+
InitializeRequest(
460+
protocolVersion: ProtocolVersion.latestSupported,
461+
capabilities: ClientCapabilities(),
462+
clientInfo: Implementation(name: 'test_client', version: '1.0.0'),
463+
),
464+
);
465+
client.notifyInitialized();
466+
467+
// List devices
468+
final result = await client.callTool(
469+
CallToolRequest(name: 'list_devices', arguments: {}),
470+
);
471+
472+
test.expect(result.isError, test.isNot(true));
473+
test.expect(result.structuredContent, {
474+
'devices': ['test-device-1', 'test-device-2'],
475+
});
476+
await server.shutdown();
477+
await client.shutdown();
478+
});
200479
});
201480
}
202481

0 commit comments

Comments
 (0)