generate static method

String generate(
  1. List<Map<String, dynamic>> tools, {
  2. required String appName,
  3. bool logErrors = false,
})

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')}
''';
}