generate static method
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)
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,
}) {
// 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;
}
}
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 schema = SchemaBuilder.buildObjectSchema(
(t['parameters'] as List<Map<String, dynamic>>? ?? []),
);
return '''
registerTool(
Tool(
name: '$name',
description: '${t['description'] ?? 'Tool $name'}',
inputSchema: $schema,
),
_$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 paramName = p['name'] as String;
final paramType = p['type'] as String;
final isOptional = p['isOptional'] == true;
final dartType = _dartType(paramType);
// Use alias as external name when present
final externalName =
(p['parameterMetadata']?['alias'] as String?) ?? paramName;
if (isOptional) {
final nullableType = dartType.endsWith('?')
? dartType
: '$dartType?';
return " final $paramName = request.arguments?['$externalName'] as $nullableType;";
}
return " final $paramName = request.arguments!['$externalName'] as $dartType;";
})
.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
$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;';
return '''
// Generated MCP HTTP server
// DO NOT EDIT - automatically generated by mcp_generator
import 'dart:async';
import 'dart:convert';
$ioImport
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';
$listInnerImportStatements
$sourceImportStatements
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);
}
});
const corsHeaders = <String, String>{
'Access-Control-Allow-Origin': '*',
'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},
);
}
final httpServer = await shelf_io.serve(
handleRequest,
$addressExpression,
$port,
);
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 {
$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
}
$toolHandlers
$codeModeSpecRegistry
$codeModeHandlers
String _serializeResult(dynamic result) {
if (result == null) return 'null';
try {
if (result is List) {
final items = result.map((e) {
if (e == null) return null;
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();
}
}
}
''';
}