initialize method

  1. @override
FutureOr<InitializeResult> initialize(
  1. InitializeRequest request
)

Mixins should register their methods in this method, as well as editing the InitializeResult.capabilities as needed.

Implementation

@override
FutureOr<InitializeResult> initialize(InitializeRequest request) async {
  // 1. Let ToolsSupport register its request handlers and seed the result.
  //    DO NOT mutate `result.capabilities.tools.listChanged`; the mixin
  //    has already set it to true and our V1 contract honors that.
  final result = await super.initialize(request);

  // 2. Discover the running Flutter app via the state file written by
  //    `artisan start`. State file absence is NOT fatal: the MCP server
  //    stays online, registers every tool, and individual tool calls
  //    soft-fail at dispatch time with an actionable "run artisan start"
  //    message. This lets MCP clients (Claude Code, Cursor, Windsurf) stay
  //    connected across the natural dev cycle of starting / stopping the
  //    Flutter app without having to reconnect the server every time.
  final state = await _stateReader();
  final wsUri = state?['vmServiceUri'] as String?;
  if (wsUri != null && wsUri.isNotEmpty) {
    try {
      final vmClient = _vmClientFactory(wsUri);
      await vmClient.connect();
      final isolateId = await vmClient.getMainIsolateId();
      _vmClient = vmClient;
      _isolateId = isolateId;
      // BUG #11 fix: when callServiceExtension's internal retry-on-sentinel
      // discovers the cached isolate id is stale (hot-restart minted a new
      // isolate), it refreshes via getMainIsolateId() and broadcasts the
      // new id on onIsolateRefreshed. Update our cached _isolateId in
      // place so subsequent dispatches use the fresh value without paying
      // a getVM RPC per call.
      vmClient.onIsolateRefreshed.listen((fresh) => _isolateId = fresh);
    } catch (e) {
      stderr.writeln(
        '[fluttersdk_artisan_mcp] VM Service connect failed: $e. '
        'Tool calls requiring VM connection will return an error; '
        'restart the Flutter app and the next call will reconnect.',
      );
    }
  } else {
    stderr.writeln(
      '[fluttersdk_artisan_mcp] no running Flutter app detected '
      '(~/.artisan/state.json missing or empty). Tools register but VM '
      'Service calls will fail until you run `artisan start`.',
    );
  }

  // 3. Apply the filter once; survivors get registered, denied tools never
  //    reach `registerTool` so dart_mcp's native "no tool" error covers
  //    denied call attempts. The plugin-side tools (registry.mcpTools) AND
  //    the substrate-side tools (artisan command adapter below) flow
  //    through the same filter so a deny rule like `tools.deny:
  //    [artisan_start]` works the same as `tools.deny: [dusk_snap]`.
  final pluginTools = registry.mcpTools;
  final substrateTools = _artisanCommandTools();
  final allTools = <McpToolDescriptor>[...pluginTools, ...substrateTools];
  final filtered = filter.apply(
    allTools,
    (tool) {
      // Substrate tools belong to the synthetic provider 'fluttersdk_artisan';
      // plugin tools defer to the registry's provider lookup.
      if (tool.extensionMethod.startsWith(_artisanDispatchPrefix)) {
        return 'fluttersdk_artisan';
      }
      return registry.providerNameFor(tool.name) ?? '';
    },
  );

  // 4. Register each surviving tool with its dispatch handler. dart_mcp
  //    auto-validates arguments against the wrapped ObjectSchema. The
  //    handler routes by `extensionMethod` prefix: substrate commands
  //    (prefix `artisan:`) run in-process via the registry; everything
  //    else dispatches through the VM Service.
  for (final descriptor in filtered) {
    registerTool(
      Tool(
        name: descriptor.name,
        description: descriptor.description,
        inputSchema: ObjectSchema.fromMap(descriptor.inputSchema),
      ),
      (request) => _dispatch(request, descriptor.extensionMethod),
    );
  }

  stderr.writeln(
    '[fluttersdk_artisan_mcp] initialized with ${filtered.length} tools '
    '(${allTools.length - filtered.length} filtered; '
    '${pluginTools.length} plugin + ${substrateTools.length} substrate)',
  );

  return result;
}