generate static method

String generate(
  1. List<Map<String, dynamic>> tools,
  2. int port,
  3. String address, {
  4. bool codeMode = false,
  5. int codeModeTimeout = 30,
  6. bool logErrors = false,
  7. List<Map<String, dynamic>> prompts = const [],
  8. List<String> corsOrigins = const ['*'],
})

Generates the HTTP server Dart code.

tools - List of tool definitions with parameters and metadata port - The network port to listen on (e.g., 3000, 8080) address - The bind address (e.g., '127.0.0.1', '0.0.0.0') codeMode - Enable code mode with JavaScript sandbox (default: false) codeModeTimeout - Timeout for code mode execution in seconds (default: 30) corsOrigins - Allowed CORS origins (default: '*' for backward compatibility)

Returns the complete server code as a Dart string.

Implementation

static String generate(
  List<Map<String, dynamic>> tools,
  int port,
  String address, {
  bool codeMode = false,
  int codeModeTimeout = 30,
  bool logErrors = false,
  List<Map<String, dynamic>> prompts = const [],
  List<String> corsOrigins = const ['*'],
}) {
  // Collect unique imports for custom List inner types
  final listInnerImports = _collectListInnerImports(tools);
  final listInnerImportStatements = listInnerImports
      .map((uri) => "import '$uri';")
      .join('\n');

  // Determine the address expression to use
  final addressExpression = address == '127.0.0.1'
      ? 'io.InternetAddress.loopbackIPv4'
      : "'$address'";

  // Collect unique per-tool source imports with aliases
  final sourceImports = <String, String>{};
  for (final tool in tools) {
    final sourceImport = tool['sourceImport'] as String?;
    final sourceAlias = tool['sourceAlias'] as String?;
    if (sourceImport != null && sourceAlias != null) {
      sourceImports[sourceImport] = sourceAlias;
    }
  }

  // Also collect source imports from prompts
  for (final prompt in prompts) {
    final sourceImport = prompt['sourceImport'] as String?;
    final sourceAlias = prompt['sourceAlias'] as String?;
    if (sourceImport != null && sourceAlias != null) {
      sourceImports[sourceImport] = sourceAlias;
    }
  }

  final sourceImportStatements = sourceImports.entries
      .map((e) => "import '${e.key}' as ${e.value};")
      .join('\n');

  // Filter code mode tools
  final codeModeTools = codeMode
      ? _filterCodeModeTools(tools)
      : <Map<String, dynamic>>[];

  // When code mode is enabled, only tools explicitly marked visible
  // (`@Tool(codeModeVisible: true)`) remain in the standard `tools/list`
  // response. The `search` and `execute` tools are always registered.
  final listedTools = codeMode ? _filterCodeModeVisibleTools(tools) : tools;

  // When code mode is enabled, only generate handlers for tools that are
  // referenced somewhere (tools/list or sandbox dispatch).
  final handlerTools = codeMode
      ? _filterHandlerToolsForCodeMode(tools)
      : tools;

  final toolRegistrations = listedTools
      .map((t) {
        final name = t['name'] as String;
        final description = (t['description'] as String?) ?? 'Tool $name';
        final schema = SchemaBuilder.buildObjectSchema(
          (t['parameters'] as List<Map<String, dynamic>>? ?? []),
        );
        final annotationsExpr = _generateAnnotationsExpression(t);
        // The Dart member reference `_$name` is safe because tool names are
        // validated against `^[a-zA-Z_][a-zA-Z0-9_]*$` at extraction time;
        // the string-literal interpolations still go through
        // _escapeDartString so doc comments containing apostrophes, dollar
        // signs, or backslashes never break the generated source.
        return '''
  registerTool(
    Tool(
      name: '${_escapeDartString(name)}',
      description: '${_escapeDartString(description)}',
      inputSchema: $schema,$annotationsExpr
    ),
    _$name,
  );''';
      })
      .join('\n');

  final codeModeRegistrations = codeMode
      ? _generateCodeModeToolRegistrations()
      : '';

  final toolHandlers = handlerTools
      .map((t) {
        final name = t['name'] as String;
        final params = t['parameters'] as List<Map<String, dynamic>>? ?? [];
        final paramExtractions = params
            .map((p) {
              final paramType = p['type'] as String;
              final dartType = _dartType(paramType);
              return _renderMcpParamExtraction(p, dartType);
            })
            .join('\n');

        // Generate validation code for parameters
        final paramValidations = params
            .map((p) => _renderParamValidation(p))
            .where((v) => v.isNotEmpty)
            .join('\n');

        // Generate conversion code for List parameters with custom inner types
        final paramConversions = params
            .where((p) => _needsListConversion(p['type'] as String))
            .map((p) {
              final paramName = p['name'] as String;
              final paramType = p['type'] as String;
              final innerType = _extractListInnerType(paramType);
              final isOptional = p['isOptional'] == true;
              if (isOptional) {
                return '    final ${paramName}Converted = $paramName?.map((e) => $innerType.fromJson(e as Map<String, dynamic>)).toList();';
              }
              return '    final ${paramName}Converted = $paramName.map((e) => $innerType.fromJson(e as Map<String, dynamic>)).toList();';
            })
            .join('\n');

        final isAsync = t['isAsync'] == true;
        final className = t['className'] as String?;
        final isStatic = t['isStatic'] == true;
        final sourceAlias = t['sourceAlias'] as String? ?? 'lib';

        String call;
        final methodName = t['methodName'] as String? ?? name;
        if (className != null && isStatic) {
          call = isAsync
              ? 'await $sourceAlias.$className.$methodName(${_callArgsWithConversion(params)})'
              : '$sourceAlias.$className.$methodName(${_callArgsWithConversion(params)})';
        } else if (className != null) {
          call = isAsync
              ? 'await $sourceAlias.$className().$methodName(${_callArgsWithConversion(params)})'
              : '$sourceAlias.$className().$methodName(${_callArgsWithConversion(params)})';
        } else {
          call = isAsync
              ? 'await $sourceAlias.$methodName(${_callArgsWithConversion(params)})'
              : '$sourceAlias.$methodName(${_callArgsWithConversion(params)})';
        }

        return '''
FutureOr<CallToolResult> _$name(CallToolRequest request) async {
  try {
$paramExtractions
$paramValidations
$paramConversions
    final result = $call;
    return CallToolResult(
      content: [TextContent(text: _serializeResult(result))],
    );
  } catch (e, st) {
    if (_logErrors) {
      io.stderr.writeln('[easy_api] $name: \$e');
      io.stderr.writeln(st);
      await io.stderr.flush();
    }
    return CallToolResult(
      content: [TextContent(text: 'An error occurred while processing the request.')],
      isError: true,
    );
  }
}''';
      })
      .join('\n');

  // dart:io is needed for default address (InternetAddress) or code mode
  final ioImport = (address == '127.0.0.1' || codeMode)
      ? "import 'dart:io' as io;"
      : '';

  final codeModeSpecRegistry = codeMode
      ? _generateToolSpecRegistry(codeModeTools)
      : '';
  final codeModeHandlers = codeMode
      ? _generateCodeModeHandlers(codeModeTools, codeModeTimeout)
      : '';
  final logErrorsConstant = '  static const bool _logErrors = $logErrors;';

  // Generate prompt support
  final hasPrompts = prompts.isNotEmpty;
  final promptAddCalls = hasPrompts
      ? prompts
            .map((p) {
              final name = p['name'] as String;
              return '    addPrompt(_prompt${name}Spec, _prompt${name}Impl);';
            })
            .join('\n')
      : '';
  final promptRegistrations = hasPrompts ? '\n$promptAddCalls' : '';
  final promptSpecs = hasPrompts ? _generatePromptSpecs(prompts) : '';
  final promptHandlers = hasPrompts ? _generatePromptHandlers(prompts) : '';

  // Generate CORS headers based on configuration
  final corsOriginHeader = corsOrigins.contains('*')
      ? "<String>['*']"
      : "<String>[${corsOrigins.map((o) => "'${_escapeDartString(o)}'").join(', ')}]";

  return '''
// Generated MCP HTTP server
// DO NOT EDIT - automatically generated by mcp_generator

import 'dart:async';
import 'dart:convert';
$ioImport
import 'dart:math' as math;

import 'package:dart_mcp/server.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:stream_channel/stream_channel.dart';
import 'package:easy_api_annotations/easy_api_annotations.dart' as easy_api;

$listInnerImportStatements
$sourceImportStatements

/// Allowed CORS origins for this server.
/// Configured via @Server annotation. Defaults to ['*'] for backward compatibility.
/// For production use, restrict to specific origins to prevent CSRF attacks.
const _corsOrigins = $corsOriginHeader;

Future<void> main() async {
// Create stream controllers for bidirectional communication
final clientToServer = StreamController<String>();
final serverToClient = StreamController<String>.broadcast();

// Create the StreamChannel that MCPServer expects
final channel = StreamChannel<String>(
  clientToServer.stream,
  serverToClient.sink,
);

final server = MCPServerWithTools(channel);

// FIFO of completers awaiting responses for in-flight POST requests.
final responseQueue = <Completer<String>>[];
// Active SSE subscribers (Streamable-HTTP GET streams).
final sseSinks = <StreamController<List<int>>>{};

serverToClient.stream.listen((response) {
  if (responseQueue.isNotEmpty) {
    responseQueue.removeAt(0).complete(response);
    return;
  }
  // Fan out unsolicited server→client messages to any open SSE streams.
  final bytes = utf8.encode('event: message\\ndata: \$response\\n\\n');
  for (final sink in sseSinks) {
    if (!sink.isClosed) sink.add(bytes);
  }
});

// Pre-compute the CORS origin header value
final corsOriginValue = _corsOrigins.length == 1 ? _corsOrigins.first : _corsOrigins.join(', ');

final corsHeaders = <String, String>{
  'Access-Control-Allow-Origin': corsOriginValue,
  'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
  'Access-Control-Allow-Headers':
      'Content-Type, Accept, Mcp-Session-Id, Authorization',
  'Access-Control-Expose-Headers': 'Mcp-Session-Id',
};

bool containsRequest(dynamic m) {
  if (m is List) return m.any(containsRequest);
  if (m is Map) return m.containsKey('id') && m.containsKey('method');
  return false;
}

Future<shelf.Response> handleRequest(shelf.Request request) async {
  final method = request.method;

  // CORS preflight.
  if (method == 'OPTIONS') {
    return shelf.Response(204, headers: corsHeaders);
  }

  // Streamable-HTTP server→client SSE stream.
  if (method == 'GET') {
    final controller = StreamController<List<int>>();
    sseSinks.add(controller);
    controller.add(utf8.encode(': ok\\n\\n'));
    final keepalive = Timer.periodic(const Duration(seconds: 15), (t) {
      if (controller.isClosed) {
        t.cancel();
      } else {
        controller.add(utf8.encode(': keepalive\\n\\n'));
      }
    });
    controller.onCancel = () {
      keepalive.cancel();
      sseSinks.remove(controller);
      controller.close();
    };
    return shelf.Response.ok(
      controller.stream,
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'X-Accel-Buffering': 'no',
        ...corsHeaders,
      },
    );
  }

  // Session termination.
  if (method == 'DELETE') {
    return shelf.Response.ok('', headers: corsHeaders);
  }

  if (method != 'POST') {
    return shelf.Response(
      405,
      body: 'Method not allowed',
      headers: corsHeaders,
    );
  }

  final body = await request.readAsString();

  dynamic parsed;
  try {
    parsed = jsonDecode(body);
  } catch (_) {
    return shelf.Response(
      400,
      body:
          '{"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"},"id":null}',
      headers: {'Content-Type': 'application/json', ...corsHeaders},
    );
  }

  // Notification / response-only batches: accept and acknowledge.
  if (!containsRequest(parsed)) {
    clientToServer.add(body);
    return shelf.Response(202, headers: corsHeaders);
  }

  final completer = Completer<String>();
  responseQueue.add(completer);
  clientToServer.add(body);

  final response = await completer.future;
  return shelf.Response.ok(
    response,
    headers: {'Content-Type': 'application/json', ...corsHeaders},
  );
}

// Use PORT env var (Cloud Run) or fall back to configured port
final portEnv = io.Platform.environment['PORT'];
final serverPort = portEnv != null
    ? int.tryParse(portEnv) ?? $port
    : $port;

final httpServer = await shelf_io.serve(
  handleRequest,
  $addressExpression,
  serverPort,
);

print('MCP HTTP server listening on port \${httpServer.port}');

// Wait for server to complete and then clean up
await server.done;
await httpServer.close();
await clientToServer.close();
await serverToClient.close();
for (final sink in sseSinks) {
  await sink.close();
}
}

base class MCPServerWithTools extends MCPServer with ToolsSupport${hasPrompts ? ', PromptsSupport' : ''} {
$logErrorsConstant

MCPServerWithTools(super.channel)
  : super.fromStreamChannel(
      implementation: Implementation(
        name: 'mcp-server',
        version: '1.0.0',
      ),
      instructions: 'Auto-generated MCP server on port $port',
    ) {
$toolRegistrations$codeModeRegistrations$promptRegistrations
}

/// Guards against duplicate initialization requests (e.g. from MCP Inspector
/// which may send `initialize` more than once for HTTP endpoints).
bool _isInitialized = false;
InitializeResult? _initializeResult;

@override
FutureOr<InitializeResult> initialize(InitializeRequest request) async {
  if (_isInitialized) return _initializeResult!;
  _isInitialized = true;
  final result = await super.initialize(request);
  _initializeResult = result;
  return result;
}

$toolHandlers
$codeModeSpecRegistry
$codeModeHandlers
$promptSpecs
$promptHandlers

String _serializeResult(dynamic result) {
  if (result == null) return 'null';
  try {
    if (result is Map) return jsonEncode(result);
    if (result is List) {
      final items = result.map((e) {
        if (e == null) return null;
        if (e is Map) return e;
        final toJson = e.toJson;
        if (toJson != null && toJson is Function) return toJson();
        return e.toString();
      }).where((e) => e != null).toList();
      return jsonEncode(items);
    }
    final toJson = result.toJson;
    if (toJson != null && toJson is Function) return jsonEncode(toJson());
    return result.toString();
  } catch (_) {
    return result.toString();
  }
}
}
''';
}