@@ -134,7 +134,7 @@ public object CallTool(JsonElement parameters)
134134 { Name : "g4.ConvertToRule" } => new { options . Rule } ,
135135
136136 // Built-in: Finds and returns relevant examples based on the provided intent and tool filters.
137- { Name : "g4.FindExamples" } => FindExamples ( options ) ,
137+ { Name : "g4.FindExamples" } => FindExamples ( options . Arguments ) ,
138138
139139 // Built-in: Finds and returns metadata about a tool by its name.
140140 { Name : "g4.FindTool" } => FindTool ( options ) ,
@@ -162,6 +162,9 @@ public object CallTool(JsonElement parameters)
162162 // Built-in: Retrieves the locator for a specific element on the page.
163163 { Name : "g4.ResolveLocator" } => ResolveLocator ( options ) ,
164164
165+ // Built-in: Executes an ordered collection of rules as a single job in one driver session.
166+ { Name : "g4.SendRules" } => SendRules ( options ) ,
167+
165168 // Built-in: Starts a new G4 browser automation session.
166169 { Name : "g4.StartSession" } => StartSession ( options ) ,
167170
@@ -219,15 +222,6 @@ public object FindExamples(ExamplesQueryModel query)
219222 } ;
220223 }
221224
222- // Unwraps the raw "arguments" payload from an MCP tool-call invocation and delegates
223- // to the public, strongly-typed FindExamples(JsonElement) overload. This gives the
224- // g4.FindExamples switch arm in CallTool the same "Xxx(options)" call shape as every
225- // other built-in tool handler, instead of reaching into options.Arguments directly.
226- private object FindExamples ( InvokeOptions options )
227- {
228- return FindExamples ( options . Arguments ) ;
229- }
230-
231225 /// <inheritdoc />
232226 public McpToolModel FindTool ( string intent , string toolName )
233227 {
@@ -488,6 +482,42 @@ public object SendRule(SendRuleInputSchema schema)
488482 return SendRule ( options ) ;
489483 }
490484
485+ /// <inheritdoc />
486+ public object SendRules ( SendRulesInputSchema schema )
487+ {
488+ // Wrap the incoming rule collection in an anonymous arguments object so it can be
489+ // serialized and converted through the standard rule conversion pipeline.
490+ var arguments = new
491+ {
492+ schema . Rules
493+ } ;
494+
495+ // Serialize the arguments payload using the shared JSON settings.
496+ var json = JsonSerializer . Serialize ( value : arguments , AppSettings . JsonOptions ) ;
497+
498+ // Parse the serialized payload into a JsonElement so it can be passed
499+ // into the rule conversion routine.
500+ var jsonElement = JsonDocument . Parse ( json ) . RootElement ;
501+
502+ // Build the invocation options using the driver session, token,
503+ // tool catalog, and shared runtime services required by the G4 engine.
504+ var options = new InvokeOptions
505+ {
506+ Arguments = jsonElement ,
507+ Buffer = _buffer ,
508+ DriverSession = schema . DriverSession ,
509+ G4Client = _client ,
510+ HttpClient = _httpClient ,
511+ Intent = schema . Intent ,
512+ Sessions = _sessions ,
513+ Token = schema . Token ,
514+ Tools = _tools
515+ } ;
516+
517+ // Delegate the rule collection execution to the G4 engine and return its result.
518+ return SendRules ( options ) ;
519+ }
520+
491521 /// <inheritdoc />
492522 public void SyncTools ( )
493523 {
@@ -538,6 +568,18 @@ private static ActionRuleModel ConvertToRule(
538568 ? ruleOut
539569 : JsonDocument . Parse ( "{}" ) . RootElement ;
540570
571+ // Delegate the actual conversion to the shared single-rule routine.
572+ return ConvertRuleData ( ruleData , intent , tools ) ;
573+ }
574+
575+ // Converts a single rule-data JSON object (tool name, parameters, and properties)
576+ // into a G4 action rule model, resolving the plugin name from the tool registry and
577+ // attaching the supplied intent to the rule's capabilities.
578+ private static ActionRuleModel ConvertRuleData (
579+ JsonElement ruleData ,
580+ IntentModel intent ,
581+ ConcurrentDictionary < string , McpToolModel > tools )
582+ {
541583 // Retrieve the "name" of the tool to be invoked. Defaults to null if not provided.
542584 var includes = new [ ] { "tool_name" , "toolName" , "name" , "tool" } ;
543585 var toolName = includes
@@ -553,9 +595,6 @@ private static ActionRuleModel ConvertToRule(
553595 ? parametersOut
554596 : JsonDocument . Parse ( "{}" ) . RootElement ;
555597
556- // Format the parameters into a command-line style string (e.g., "--param1:value1 --param2:value2").
557- var parametersCli = FormatParameters ( parameters ) ;
558-
559598 // Get the properties from the ruleData, defaulting to a JSON object with the plugin name if not provided.
560599 var properties = ruleData . TryGetProperty ( "toolProperties" , out var propertiesOut )
561600 ? propertiesOut . GetRawText ( )
@@ -573,10 +612,15 @@ private static ActionRuleModel ConvertToRule(
573612 // Set the PluginName property of the rule to the retrieved plugin name
574613 rule . PluginName = pluginName ;
575614
576- // Set the Argument property of the rule to the formatted parameters if provided
577- rule . Argument = string . IsNullOrEmpty ( parametersCli ) || parametersCli . Equals ( "{{$ }}" )
578- ? rule . Argument
579- : parametersCli ;
615+ // Preserve an explicitly supplied G4 expression and synthesize one only as a fallback.
616+ if ( string . IsNullOrWhiteSpace ( rule . Argument ) )
617+ {
618+ var parametersCli = FormatParameters ( parameters ) ;
619+ if ( ! string . IsNullOrEmpty ( parametersCli ) && ! parametersCli . Equals ( "{{$ }}" ) )
620+ {
621+ rule . Argument = parametersCli ;
622+ }
623+ }
580624
581625 // Return the created rule
582626 return rule ;
@@ -590,23 +634,20 @@ static string FormatParameters(JsonElement parameters)
590634 return string . Empty ;
591635 }
592636
593- // Build a case-insensitive dictionary of name → textual value.
594- // $"{i.Value}" calls JsonElement.ToString() (see boolean casing note above).
595- var parametersObject = parameters
637+ // Expand arrays into repeated switches, as required by the G4 parameter schema.
638+ var parametersCollection = parameters
596639 . EnumerateObject ( )
597- . ToDictionary ( i => i . Name , i => $ "{ i . Value } ", StringComparer . OrdinalIgnoreCase ) ;
640+ . SelectMany ( parameter => parameter . Value . ValueKind == JsonValueKind . Array
641+ ? parameter . Value
642+ . EnumerateArray ( )
643+ . Select ( value => new KeyValuePair < string , string > ( parameter . Name , $ "{ value } ") )
644+ : [ new KeyValuePair < string , string > ( parameter . Name , $ "{ parameter . Value } ") ] ) ;
598645
599- // Round-trip through System.Text.Json to apply G4JsonOptions policies (e.g., snake_case keys).
600- var json = JsonSerializer . Serialize ( value : parametersObject , options : AppSettings . JsonOptions ) ;
601- var parametersCollection = JsonSerializer . Deserialize < Dictionary < string , string > > (
602- json ,
603- options : AppSettings . JsonOptions ) ! ;
604-
605- // Convert keys to PascalCase and render as --Key[:Value] (omit :Value when empty).
646+ // Render each scalar value as --Key[:Value] and each array item as its own switch.
606647 var parametersExpression = parametersCollection
607- . Select ( i =>
608- $ "--{ i . Key } " +
609- ( string . IsNullOrEmpty ( i . Value ) ? "" : $ ":{ i . Value } ") )
648+ . Select ( parameter =>
649+ $ "--{ parameter . Key } " +
650+ ( string . IsNullOrEmpty ( parameter . Value ) ? "" : $ ":{ parameter . Value } ") )
610651 . ToArray ( ) ;
611652
612653 // Wrap with G4 template delimiters.
@@ -726,6 +767,7 @@ static McpToolModel[] GetSystemTools()
726767 ClientTool = clientTool ,
727768 QualifiedName = $ "system.{ clientTool . Name } ",
728769 Name = clientTool . Name ,
770+ Namespace = "G4.System" ,
729771 Description = clientTool . Description ,
730772 Metadata = new ( )
731773 {
@@ -776,10 +818,10 @@ private static object FindTool(InvokeOptions options)
776818 ? tool
777819 : options . Tools . Values . FirstOrDefault ( i => i . QualifiedName . Equals ( toolName , StringComparison . OrdinalIgnoreCase ) ) ;
778820
779- // Return the matched tool when a tool name was provided .
780- // Return null when the caller did not supply any tool name.
781- return ! string . IsNullOrEmpty ( toolName )
782- ? new { Tool = tool . ClientTool }
821+ // Return the complete authoritative tool model when it was found .
822+ // Return null when the caller did not supply a valid tool name.
823+ return ! string . IsNullOrEmpty ( toolName ) && tool != null
824+ ? new { Tool = tool }
783825 : null ;
784826 }
785827
@@ -1154,6 +1196,87 @@ private static object SendRule(InvokeOptions options)
11541196 } ;
11551197 }
11561198
1199+ // Executes an ordered collection of rules as a single job within one driver session,
1200+ // and retrieves the response, including the driver session and the ordered per-rule output.
1201+ private static object SendRules ( InvokeOptions options )
1202+ {
1203+ // Read the "rules" argument and reject the request when it is missing or not an array,
1204+ // mirroring the tool's required "rules" argument as declared in its input schema.
1205+ if ( ! options . Arguments . TryGetProperty ( "rules" , out var rulesElement ) ||
1206+ rulesElement . ValueKind != JsonValueKind . Array )
1207+ {
1208+ throw new ArgumentException ( "The rules argument is required and must be a non-empty array." ) ;
1209+ }
1210+
1211+ // Convert every rule-data entry into an executable G4 action rule, preserving order.
1212+ var rules = rulesElement
1213+ . EnumerateArray ( )
1214+ . Select ( ruleData => ConvertRuleData ( ruleData , options . Intent , options . Tools ) )
1215+ . ToArray ( ) ;
1216+
1217+ // Reject an empty collection so the engine is not invoked with a job that has no rules.
1218+ if ( rules . Length == 0 )
1219+ {
1220+ throw new ArgumentException ( "The rules argument is required and must be a non-empty array." ) ;
1221+ }
1222+
1223+ // Create a single automation model whose only job carries every converted rule in order.
1224+ var automation = NewAutomation ( options . DriverSession , options . Token , rules ) ;
1225+
1226+ // Invoke the automation process using the client and retrieve the response.
1227+ var response = options . G4Client . Automation . Invoke ( automation ) ;
1228+
1229+ // Extract the session object from the response, ensuring we track the latest session.
1230+ var session = response . Values . Last ( ) . Sessions . Last ( ) ;
1231+
1232+ // Add the session to the sessions dictionary to keep track of the active session.
1233+ options . Sessions [ session . Key ] = session . Value ;
1234+
1235+ // Add one property-backed response entry per rule so later MCP serialization preserves the buffer values.
1236+ var buffer = options . Buffer . GetValueOrDefault ( key : session . Key , defaultValue : null ) ;
1237+ var timestamp = DateTimeOffset . UtcNow . ToUnixTimeMilliseconds ( ) ;
1238+ for ( var i = 0 ; i < rules . Length ; i ++ )
1239+ {
1240+ buffer ? . Add ( new BufferResponseModel . BufferItem
1241+ {
1242+ Timestamp = timestamp + i ,
1243+ Rule = rules [ i ]
1244+ } ) ;
1245+ }
1246+
1247+ // Retrieve the complete set of plugin responses for the executed rule collection.
1248+ var ruleResponse = session
1249+ . Value
1250+ . ResponseTree
1251+ . Stages . Last ( )
1252+ . Jobs . Last ( )
1253+ . Plugins ;
1254+
1255+ // Get the current buffer for the session, defaulting to an empty list if none exists.
1256+ var bufferOut = buffer ?? [ ] ;
1257+
1258+ // If the last rule closes the browser, clean up the session and buffer.
1259+ if ( rules [ ^ 1 ] . PluginName . Equals ( "CloseBrowser" , StringComparison . OrdinalIgnoreCase ) )
1260+ {
1261+ // Remove the session from the sessions dictionary when closing the browser.
1262+ options . Sessions . TryRemove ( session . Key , out _ ) ;
1263+
1264+ // Clear the buffer associated with the session key.
1265+ options . Buffer . TryRemove ( session . Key , out bufferOut ) ;
1266+ }
1267+
1268+ // Return an anonymous object containing the session key and the ordered rule response values.
1269+ return new
1270+ {
1271+ DriverSession = session . Key ,
1272+ Value = new
1273+ {
1274+ Buffer = bufferOut . OrderBy ( i => i . Timestamp ) . Select ( i => i . Rule ) ,
1275+ Response = ruleResponse
1276+ }
1277+ } ;
1278+ }
1279+
11571280 // Starts a new G4 session by invoking the automation process with the provided parameters.
11581281 // The session information is returned, and the session is added to the provided dictionary of active sessions.
11591282 private static object StartSession ( InvokeOptions options )
@@ -1174,8 +1297,6 @@ private static object StartSession(InvokeOptions options)
11741297 // These parameters are required for the automation process to know which browser and driver binaries to use.
11751298 automation . DriverParameters = driverParameters ;
11761299
1177- var j = JsonSerializer . Serialize ( automation , AppSettings . JsonOptions ) ;
1178-
11791300 // Invoke the automation process and get the response.
11801301 var response = options . G4Client . Automation . Invoke ( automation ) ;
11811302
0 commit comments