@@ -186,6 +186,55 @@ def print_subject_summary(results: list[dict[str, Any]]) -> None:
186186 print (f" - { reason } " )
187187
188188
189+ def print_config_summary (payload : dict [str , Any ]) -> None :
190+ inventory = payload .get ("inventory" ) or {}
191+ gate = payload .get ("gate" ) or {}
192+ decision_counts = inventory .get ("decision_counts" ) or {}
193+ print (
194+ "configs_checked={configs_checked} configs_found={configs_found} "
195+ "configs_with_mcp={configs_with_mcp_servers} mcp_servers={mcp_servers_found} "
196+ "scanned={servers_scanned} decision={decision} "
197+ "allow={allow} review={warn} block={block}" .format (
198+ decision = gate .get ("decision" , "allow" ),
199+ allow = decision_counts .get ("allow" , 0 ),
200+ warn = decision_counts .get ("warn" , 0 ),
201+ block = decision_counts .get ("block" , 0 ),
202+ ** inventory ,
203+ )
204+ )
205+
206+ skipped = [
207+ config
208+ for config in payload .get ("configs" , [])
209+ if config .get ("exists" ) and config .get ("status" ) == "no_mcp_servers"
210+ ]
211+ for config in skipped :
212+ print (f"SKIP { config .get ('source' )} : no MCP servers ({ config .get ('path' )} )" )
213+
214+ for row in payload .get ("results" , []):
215+ server = row .get ("server" ) or {}
216+ raw_decision = str (row .get ("decision" , "allow" ))
217+ decision = "REVIEW" if raw_decision == "warn" else raw_decision .upper ()
218+ name = server .get ("name" ) or "unknown"
219+ source = server .get ("source" ) or "config"
220+ activation = row .get ("activation_score" )
221+ static_score = row .get ("static_score" )
222+ flags = row .get ("inventory_flags" ) or []
223+ review_flags = [flag .get ("id" ) for flag in flags if flag .get ("level" ) == "review" ]
224+ info_flags = [flag .get ("id" ) for flag in flags if flag .get ("level" ) == "info" ]
225+ flag_text = ""
226+ if review_flags :
227+ flag_text = " review=" + "," .join (str (flag ) for flag in review_flags )
228+ elif info_flags :
229+ flag_text = " info=" + "," .join (str (flag ) for flag in info_flags [:2 ])
230+ print (
231+ f"{ decision :<5} { source } /{ name } : activation={ float (activation or 0 ):.3f} "
232+ f"static={ float (static_score or 0 ):.3f} { flag_text } "
233+ )
234+ for reason in row .get ("decision_reasons" , [])[:2 ]:
235+ print (f" - { reason } " )
236+
237+
189238def command_scan_path (args : argparse .Namespace ) -> int :
190239 from .hook import scan_subjects
191240 from .targets import collect_subjects_from_path
@@ -206,6 +255,30 @@ def command_scan_path(args: argparse.Namespace) -> int:
206255 return int (payload ["gate" ]["exit_code" ])
207256
208257
258+ def command_scan_config (args : argparse .Namespace ) -> int :
259+ from .configs import build_config_scan_payload , collect_config_servers , config_candidates_from_target
260+ from .hook import scan_subjects
261+
262+ target = args .target or "auto"
263+ candidates = config_candidates_from_target (target , Path .cwd ())
264+ configs , servers = collect_config_servers (candidates , max_file_bytes = args .max_file_bytes )
265+ scan_payload = None
266+ if servers :
267+ scan_payload = scan_subjects ([server .subject for server in servers ], args )
268+ payload = build_config_scan_payload (
269+ target = str (target ),
270+ configs = configs ,
271+ servers = servers ,
272+ scan_payload = scan_payload ,
273+ fail_on = args .fail_on ,
274+ )
275+ if args .format == "summary" :
276+ print_config_summary (payload )
277+ else :
278+ print_json (payload , args .pretty )
279+ return int (payload ["gate" ]["exit_code" ])
280+
281+
209282def command_doctor (args : argparse .Namespace ) -> int :
210283 complete = artifact_complete (args .artifact )
211284 payload : dict [str , Any ] = {
@@ -279,6 +352,20 @@ def build_parser() -> argparse.ArgumentParser:
279352 add_runtime_args (scan_path )
280353 scan_path .set_defaults (func = command_scan_path )
281354
355+ scan_config = subparsers .add_parser (
356+ "scan-config" ,
357+ help = "Scan installed Claude/Cursor/Claude Code MCP client configs." ,
358+ )
359+ scan_config .add_argument (
360+ "target" ,
361+ nargs = "?" ,
362+ default = "auto" ,
363+ help = "Config path to scan, or 'auto' to scan common local MCP client config paths." ,
364+ )
365+ scan_config .add_argument ("--max-file-bytes" , type = int , default = 200_000 , help = "Maximum bytes read from each config file." )
366+ add_runtime_args (scan_config )
367+ scan_config .set_defaults (func = command_scan_config )
368+
282369 doctor = subparsers .add_parser ("doctor" , help = "Check the cached scanner artifact." )
283370 doctor .add_argument ("--artifact" , type = Path , default = DEFAULT_ARTIFACT )
284371 doctor .add_argument ("--pretty" , action = "store_true" )
0 commit comments