@@ -160,6 +160,10 @@ class FlutterMcpServer {
160160 String ? _autoConnectUrl;
161161 int ? _autoConnectCdpPort;
162162
163+ // Plugin system
164+ String _pluginsDir = '${Platform .environment ['HOME' ] ?? '.' }/.flutter-skill/plugins' ;
165+ final List <Map <String , dynamic >> _pluginTools = [];
166+
163167 // Last known connection info for auto-reconnect
164168 String ? _lastConnectionUri;
165169 int ? _lastConnectionPort;
@@ -2098,6 +2102,72 @@ Detailed diagnostic report with:
20982102 "inputSchema" : {"type" : "object" , "properties" : {}},
20992103 },
21002104
2105+ // AI Visual Verification
2106+ {
2107+ "name" : "visual_verify" ,
2108+ "description" : """Take a screenshot AND text snapshot for AI visual verification.
2109+
2110+ Returns both a screenshot file and structured text snapshot so the calling AI can verify
2111+ the UI matches the expected description. Optionally checks for specific elements.
2112+
2113+ [USE WHEN]
2114+ • Verifying UI looks correct after a series of actions
2115+ • Checking that expected elements are present on screen
2116+ • Visual QA of a screen against a description
2117+
2118+ [RETURNS]
2119+ Combined result with screenshot path, text snapshot, element matching results, and a hint
2120+ for the AI to compare against the provided description.""" ,
2121+ "inputSchema" : {
2122+ "type" : "object" ,
2123+ "properties" : {
2124+ "description" : {
2125+ "type" : "string" ,
2126+ "description" : "What the UI should look like (e.g., 'login form with email and password fields')"
2127+ },
2128+ "check_elements" : {
2129+ "type" : "array" ,
2130+ "items" : {"type" : "string" },
2131+ "description" : "Specific elements that should be visible (matched against snapshot refs and text)"
2132+ },
2133+ "quality" : {
2134+ "type" : "number" ,
2135+ "description" : "Screenshot quality 0-1 (default 0.5)"
2136+ },
2137+ },
2138+ },
2139+ },
2140+ {
2141+ "name" : "visual_diff" ,
2142+ "description" : """Compare current screen against a baseline screenshot.
2143+
2144+ Takes a new screenshot and returns both the current and baseline paths so the calling AI
2145+ can visually compare them. Also returns text snapshots for structural comparison.
2146+
2147+ [USE WHEN]
2148+ • Visual regression testing
2149+ • Comparing before/after states
2150+ • Verifying no unintended UI changes""" ,
2151+ "inputSchema" : {
2152+ "type" : "object" ,
2153+ "properties" : {
2154+ "baseline_path" : {
2155+ "type" : "string" ,
2156+ "description" : "Path to baseline screenshot file"
2157+ },
2158+ "description" : {
2159+ "type" : "string" ,
2160+ "description" : "What to focus on when comparing (optional)"
2161+ },
2162+ "quality" : {
2163+ "type" : "number" ,
2164+ "description" : "Screenshot quality 0-1 (default 0.5)"
2165+ },
2166+ },
2167+ "required" : ["baseline_path" ],
2168+ },
2169+ },
2170+
21012171 // Parallel Multi-Device
21022172 {
21032173 "name" : "parallel_snapshot" ,
@@ -2122,6 +2192,52 @@ Detailed diagnostic report with:
21222192 },
21232193 },
21242194 },
2195+
2196+ // Cross-Platform Test Orchestration
2197+ {
2198+ "name" : "multi_platform_test" ,
2199+ "description" : "Run the same test steps across all connected platforms simultaneously. Great for cross-platform verification." ,
2200+ "inputSchema" : {
2201+ "type" : "object" ,
2202+ "properties" : {
2203+ "actions" : {
2204+ "type" : "array" ,
2205+ "items" : {
2206+ "type" : "object" ,
2207+ "properties" : {
2208+ "tool" : {"type" : "string" },
2209+ "args" : {"type" : "object" },
2210+ },
2211+ },
2212+ "description" : "Sequence of tool calls to execute on each platform"
2213+ },
2214+ "session_ids" : {
2215+ "type" : "array" ,
2216+ "items" : {"type" : "string" },
2217+ "description" : "Specific sessions to test (default: all connected)"
2218+ },
2219+ "stop_on_failure" : {
2220+ "type" : "boolean" ,
2221+ "description" : "Stop all platforms on first failure (default: false)"
2222+ },
2223+ },
2224+ "required" : ["actions" ],
2225+ },
2226+ },
2227+ {
2228+ "name" : "compare_platforms" ,
2229+ "description" : "Take snapshots from all connected platforms and compare element presence. Identifies cross-platform inconsistencies." ,
2230+ "inputSchema" : {
2231+ "type" : "object" ,
2232+ "properties" : {
2233+ "session_ids" : {
2234+ "type" : "array" ,
2235+ "items" : {"type" : "string" },
2236+ "description" : "Specific sessions to compare (default: all connected)"
2237+ },
2238+ },
2239+ },
2240+ },
21252241 ];
21262242
21272243 // Smart filtering: when connected, only return relevant tools
@@ -3547,6 +3663,149 @@ Detailed diagnostic report with:
35473663 return {"results" : results};
35483664 }
35493665
3666+ if (name == 'multi_platform_test' ) {
3667+ final actions = (args['actions' ] as List <dynamic >? ) ?? [];
3668+ final sessionIds = (args['session_ids' ] as List <dynamic >? )? .cast <String >() ?? _sessions.keys.toList ();
3669+ final stopOnFailure = args['stop_on_failure' ] as bool ? ?? false ;
3670+ final savedSessionId = _activeSessionId;
3671+
3672+ final futures = sessionIds.map ((sid) async {
3673+ final platform = _sessions[sid]? .deviceId ?? 'unknown' ;
3674+ final steps = < Map <String , dynamic >> [];
3675+ int passed = 0 ;
3676+ int failed = 0 ;
3677+ bool stopped = false ;
3678+
3679+ for (final action in actions) {
3680+ if (stopped) break ;
3681+ final toolName = (action as Map <String , dynamic >)['tool' ] as String ? ?? '' ;
3682+ final toolArgs = Map <String , dynamic >.from (
3683+ (action['args' ] as Map <String , dynamic >? ) ?? {},
3684+ );
3685+ toolArgs['session_id' ] = sid;
3686+
3687+ final sw = Stopwatch ()..start ();
3688+ try {
3689+ // Temporarily switch active session for tools that rely on it
3690+ _activeSessionId = sid;
3691+ final result = await _executeToolInner (toolName, toolArgs);
3692+ sw.stop ();
3693+ final success = result is Map ? (result['error' ] == null ) : true ;
3694+ steps.add ({'tool' : toolName, 'success' : success, 'time_ms' : sw.elapsedMilliseconds});
3695+ if (success) {
3696+ passed++ ;
3697+ } else {
3698+ failed++ ;
3699+ if (stopOnFailure) stopped = true ;
3700+ }
3701+ } catch (e) {
3702+ sw.stop ();
3703+ steps.add ({'tool' : toolName, 'success' : false , 'time_ms' : sw.elapsedMilliseconds, 'error' : e.toString ()});
3704+ failed++ ;
3705+ if (stopOnFailure) stopped = true ;
3706+ }
3707+ }
3708+
3709+ return MapEntry (sid, {
3710+ 'platform' : platform,
3711+ 'steps' : steps,
3712+ 'passed' : passed,
3713+ 'failed' : failed,
3714+ });
3715+ });
3716+
3717+ final entries = await Future .wait (futures);
3718+ _activeSessionId = savedSessionId;
3719+
3720+ final results = Map .fromEntries (entries);
3721+ final allPassed = results.values.where ((r) => (r['failed' ] as int ) == 0 ).length;
3722+ final someFailed = results.values.where ((r) => (r['failed' ] as int ) > 0 ).length;
3723+
3724+ return {
3725+ 'platforms_tested' : sessionIds.length,
3726+ 'results' : results,
3727+ 'summary' : {
3728+ 'total_platforms' : sessionIds.length,
3729+ 'all_passed' : allPassed,
3730+ 'some_failed' : someFailed,
3731+ },
3732+ };
3733+ }
3734+
3735+ if (name == 'compare_platforms' ) {
3736+ final sessionIds = (args['session_ids' ] as List <dynamic >? )? .cast <String >() ?? _sessions.keys.toList ();
3737+
3738+ // Take snapshots from all platforms in parallel
3739+ final futures = sessionIds.map ((sid) async {
3740+ try {
3741+ final c = _clients[sid];
3742+ if (c == null ) return MapEntry (sid, < String , dynamic > {'error' : 'Not connected' });
3743+ if (c is FlutterSkillClient ) {
3744+ final structured = await c.getInteractiveElementsStructured ();
3745+ final elements = (structured is Map && structured['elements' ] is List )
3746+ ? (structured['elements' ] as List )
3747+ : < dynamic > [];
3748+ final elementKeys = < String > {};
3749+ for (final el in elements) {
3750+ if (el is Map ) {
3751+ final type = el['type' ] as String ? ?? '' ;
3752+ final text = el['text' ] as String ? ?? el['label' ] as String ? ?? '' ;
3753+ elementKeys.add ('$type :$text ' );
3754+ }
3755+ }
3756+ return MapEntry (sid, < String , dynamic > {
3757+ 'platform' : _sessions[sid]? .deviceId ?? 'unknown' ,
3758+ 'element_count' : elements.length,
3759+ 'elements' : elementKeys.toList (),
3760+ });
3761+ }
3762+ return MapEntry (sid, < String , dynamic > {'error' : 'Not a Flutter client' });
3763+ } catch (e) {
3764+ return MapEntry (sid, < String , dynamic > {'error' : e.toString ()});
3765+ }
3766+ });
3767+
3768+ final entries = await Future .wait (futures);
3769+ final platformData = Map .fromEntries (entries);
3770+
3771+ // Find all unique element keys across platforms
3772+ final allElements = < String > {};
3773+ final platformElements = < String , Set <String >> {};
3774+ for (final entry in platformData.entries) {
3775+ if (entry.value.containsKey ('elements' )) {
3776+ final elems = (entry.value['elements' ] as List ).cast <String >().toSet ();
3777+ platformElements[entry.key] = elems;
3778+ allElements.addAll (elems);
3779+ }
3780+ }
3781+
3782+ // Build presence matrix and find inconsistencies
3783+ final inconsistencies = < Map <String , dynamic >> [];
3784+ final presenceMatrix = < String , Map <String , bool >> {};
3785+ for (final element in allElements) {
3786+ final presence = < String , bool > {};
3787+ for (final sid in platformElements.keys) {
3788+ presence[sid] = platformElements[sid]! .contains (element);
3789+ }
3790+ presenceMatrix[element] = presence;
3791+ // If not present on all platforms, it's an inconsistency
3792+ if (presence.values.any ((v) => ! v)) {
3793+ inconsistencies.add ({
3794+ 'element' : element,
3795+ 'present_on' : presence.entries.where ((e) => e.value).map ((e) => e.key).toList (),
3796+ 'missing_on' : presence.entries.where ((e) => ! e.value).map ((e) => e.key).toList (),
3797+ });
3798+ }
3799+ }
3800+
3801+ return {
3802+ 'platforms' : platformData,
3803+ 'total_unique_elements' : allElements.length,
3804+ 'inconsistencies' : inconsistencies,
3805+ 'consistent' : inconsistencies.isEmpty,
3806+ };
3807+ }
3808+
35503809 // Auth inject session
35513810 if (name == 'auth_inject_session' ) {
35523811 final token = args['token' ] as String ;
0 commit comments