generate static method
Generates the CLI application Dart source for the given tools.
tools - List of tool definitions produced by McpBuilder.
appName - Display name of the CLI binary (used in help text).
logErrors - When true, full error messages and stack traces are
written to stderr before the generic error response.
Implementation
static String generate(
List<Map<String, dynamic>> tools, {
required String appName,
bool logErrors = false,
}) {
final escapedAppName = escapeDartString(appName);
// Group tools by containing class. Tools with no className are top-level.
final byClass = <String?, List<Map<String, dynamic>>>{};
for (final tool in tools) {
final className = tool['className'] as String?;
byClass.putIfAbsent(className, () => []).add(tool);
}
// Collect imports the same way the MCP template does so we can call into
// the user's source classes.
final sourceImports = <String, String>{};
final listInnerImports = <String>{};
for (final tool in tools) {
final src = tool['sourceImport'] as String?;
final alias = tool['sourceAlias'] as String?;
if (src != null && alias != null) sourceImports[src] = alias;
final params = tool['parameters'] as List<Map<String, dynamic>>? ?? [];
for (final p in params) {
final imp = p['listInnerTypeImport'] as String?;
if (imp != null) listInnerImports.add(imp);
}
}
final listInnerImportStatements = listInnerImports
.map((u) => "import '$u';")
.join('\n');
final sourceImportStatements = sourceImports.entries
.map((e) => "import '${e.key}' as ${e.value};")
.join('\n');
// Generate the Command class for each tool.
final toolCommandClasses = <String>[];
// Class-group commands and their registrations.
final classGroupClasses = <String>[];
final mainRegistrations = <String>[];
byClass.forEach((className, classTools) {
if (className == null) {
// Top-level functions become top-level commands.
for (final tool in classTools) {
toolCommandClasses.add(_renderToolCommand(tool, logErrors));
mainRegistrations.add(
' runner.addCommand(${_toolCommandClassName(tool)}());',
);
}
return;
}
final groupName = kebabCase(className);
final groupClassName = '_${className}GroupCommand';
final subRegs = <String>[];
for (final tool in classTools) {
toolCommandClasses.add(_renderToolCommand(tool, logErrors));
subRegs.add(' addSubcommand(${_toolCommandClassName(tool)}());');
}
classGroupClasses.add('''
class $groupClassName extends Command<int> {
@override
String get name => '${escapeDartString(groupName)}';
@override
String get description => 'Commands for $className.';
$groupClassName() {
${subRegs.join('\n')}
}
}
''');
mainRegistrations.add(' runner.addCommand($groupClassName());');
});
final logErrorsLine = 'const bool _logErrors = $logErrors;';
// Determine whether any parameter requires JSON decoding so we can
// conditionally include the _readJsonValue helper without triggering
// unused-element warnings.
var needsJsonHelper = false;
for (final tool in tools) {
final params = tool['parameters'] as List<Map<String, dynamic>>? ?? [];
for (final p in params) {
final type = p['type'] as String;
final base = baseType(type);
if (isCustomList(type) ||
(!isPrimitive(type) && !isPrimitiveList(type) && base != 'bool')) {
needsJsonHelper = true;
break;
}
}
if (needsJsonHelper) break;
}
final readJsonHelper = needsJsonHelper
? '''
/// Reads a JSON value from a CLI option. Accepts either a literal JSON
/// string or a curl-style `@path/to/file.json` reference.
dynamic _readJsonValue(String? raw) {
if (raw == null || raw.isEmpty) return null;
final src = raw.startsWith('@')
? io.File(raw.substring(1)).readAsStringSync()
: raw;
return jsonDecode(src);
}
'''
: '';
return '''
// Generated CLI app
// DO NOT EDIT - automatically generated by mcp_generator
import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
$listInnerImportStatements
$sourceImportStatements
$logErrorsLine
Future<void> main(List<String> argv) async {
final runner = CommandRunner<int>(
'$escapedAppName',
'Generated CLI for annotated easy_api tools.',
);
runner.argParser.addFlag(
'compact',
negatable: false,
help: 'Emit results as compact (single-line) JSON instead of pretty-printed.',
);
${mainRegistrations.join('\n')}
try {
final code = await runner.run(argv) ?? 0;
io.exit(code);
} on UsageException catch (e) {
io.stderr.writeln(e);
io.exit(64);
}
}
$readJsonHelper
/// Serializes [result] for stdout. Defaults to pretty-printed JSON; with
/// the global `--compact` flag, emits single-line JSON.
void _emitResult(Object? result, ArgResults? globalResults) {
if (result == null) return;
Object? toEmit;
try {
if (result is num || result is bool || result is String) {
toEmit = result;
} else if (result is Map) {
toEmit = result;
} else if (result is List) {
toEmit = result.map(_toJsonish).toList();
} else {
toEmit = _toJsonish(result);
}
} catch (_) {
toEmit = result.toString();
}
final compact = globalResults != null && globalResults['compact'] == true;
final encoded = compact
? jsonEncode(toEmit)
: const JsonEncoder.withIndent(' ').convert(toEmit);
io.stdout.writeln(encoded);
}
Object? _toJsonish(Object? e) {
if (e == null || e is num || e is bool || e is String || e is Map) return e;
if (e is List) return e.map(_toJsonish).toList();
try {
final tj = (e as dynamic).toJson;
if (tj != null && tj is Function) return tj();
} catch (_) {}
return e.toString();
}
/// Reports a CLI usage / validation error and returns exit code 64.
int _usageError(String message) {
io.stderr.writeln(message);
return 64;
}
/// Reports an internal error and returns exit code 1.
int _internalError(Object e, StackTrace st, String name) {
if (_logErrors) {
io.stderr.writeln('[easy_api] \$name: \$e');
io.stderr.writeln(st);
}
io.stderr.writeln('An error occurred while processing the request.');
return 1;
}
${classGroupClasses.join('\n')}
${toolCommandClasses.join('\n')}
''';
}