initialize method
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;
}