netcheckx 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
netcheck/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "2.1.0"
netcheck/__main__.py ADDED
@@ -0,0 +1,14 @@
1
+ import os
2
+ import sys
3
+
4
+ # Ensure parent directory is in sys.path when run directly as `python3 netcheck`
5
+ if __name__ == "__main__" and not __package__:
6
+ parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
7
+ if parent_dir not in sys.path:
8
+ sys.path.insert(0, parent_dir)
9
+ __package__ = "netcheck"
10
+
11
+ from netcheck.cli import main
12
+
13
+ if __name__ == "__main__":
14
+ main()
netcheck/cli.py ADDED
@@ -0,0 +1,512 @@
1
+ import sys
2
+ import os
3
+ import time
4
+ import argparse
5
+ import csv
6
+ import io
7
+ from datetime import datetime
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ from typing import List, Dict, Any, Tuple, Optional
10
+
11
+ from netcheck.modules.tcp import check_tcp_connect
12
+ from netcheck.modules.dns import dns_lookup
13
+ from netcheck.modules.http import check_http_status
14
+ from netcheck.modules.ssl import check_ssl_certificate
15
+ from netcheck.modules.ping import ping_host
16
+ from netcheck.modules.interfaces import get_network_interfaces
17
+ from netcheck.utils.formatters import format_text, format_json, format_csv, format_xml, get_colors
18
+ from netcheck.utils.range_expanders import expand_ip_range, expand_port_range
19
+ from netcheck.utils.normalize import parse_line_to_raw_host_port
20
+
21
+ def run_check_with_retry(check_fn, args=(), kwargs=None, retries=1, delay=1.0) -> Dict[str, Any]:
22
+ """Runs a check function and retries it if it fails or returns success=False."""
23
+ if kwargs is None:
24
+ kwargs = {}
25
+
26
+ attempt = 1
27
+ result = None
28
+ while attempt <= retries:
29
+ try:
30
+ result = check_fn(*args, **kwargs)
31
+ if result.get("success", False):
32
+ return result
33
+ except Exception as e:
34
+ result = {
35
+ "target": str(args[0]) if args else "unknown",
36
+ "status": "FAILED",
37
+ "latency_ms": 0.0,
38
+ "success": False,
39
+ "error": str(e),
40
+ "metadata": {}
41
+ }
42
+
43
+ if attempt < retries:
44
+ time.sleep(delay)
45
+ attempt += 1
46
+
47
+ return result or {"success": False, "status": "FAILED", "target": "unknown", "error": "No attempts made"}
48
+
49
+ def parse_csv_content(content: str) -> List[Tuple[str, str]]:
50
+ """Parses CSV content of hosts and ports."""
51
+ targets = []
52
+ try:
53
+ reader = csv.reader(io.StringIO(content))
54
+ # Skip header if it exists
55
+ first_row = next(reader, None)
56
+ if first_row:
57
+ # Check if first row is header
58
+ if len(first_row) >= 2 and (first_row[0].lower() in ("host", "target", "hostname") or first_row[1].lower() in ("port", "ports")):
59
+ pass
60
+ else:
61
+ targets.append((first_row[0].strip(), first_row[1].strip()))
62
+
63
+ for row in reader:
64
+ if len(row) >= 2:
65
+ targets.append((row[0].strip(), row[1].strip()))
66
+ except Exception as e:
67
+ print(f"Error parsing CSV content: {e}", file=sys.stderr)
68
+ return targets
69
+
70
+ def parse_csv_file(filepath: str) -> List[Tuple[str, str]]:
71
+ """Parses a CSV file of hosts and ports."""
72
+ try:
73
+ with open(filepath, "r", newline="") as f:
74
+ return parse_csv_content(f.read())
75
+ except Exception as e:
76
+ print(f"Error reading CSV file {filepath}: {e}", file=sys.stderr)
77
+ return []
78
+
79
+ def parse_batch_content(content: str) -> List[Tuple[str, str]]:
80
+ """Parses a lenient batch content containing targets (using parse_line_to_raw_host_port)."""
81
+ targets = []
82
+ for line in content.splitlines():
83
+ h, p = parse_line_to_raw_host_port(line)
84
+ if h and p:
85
+ targets.append((h, p))
86
+ return targets
87
+
88
+ def parse_batch_file(filepath: str) -> List[Tuple[str, str]]:
89
+ """Parses a lenient batch file of targets."""
90
+ try:
91
+ with open(filepath, "r") as f:
92
+ return parse_batch_content(f.read())
93
+ except Exception as e:
94
+ print(f"Error reading batch file {filepath}: {e}", file=sys.stderr)
95
+ return []
96
+
97
+ def print_help():
98
+ cmd_name = "netcheck"
99
+ if len(sys.argv) > 0:
100
+ prog = sys.argv[0]
101
+ if "netcheck" not in prog and ("__main__.py" in prog or "cli.py" in prog):
102
+ cmd_name = "python3 -m netcheck"
103
+
104
+ help_text = f"""Network Connectivity Checker - Advanced Version
105
+
106
+ Usage: {cmd_name} [OPTIONS] [input_file]
107
+
108
+ OPTIONS:
109
+ -t, --timeout <seconds> Connection timeout (default: 5)
110
+ -j, --jobs <number> Max parallel jobs (default: 10)
111
+ -V, --verbose Verbose output
112
+ -f, --format <format> Output format: text, json, csv, xml (default: text)
113
+ -c, --combined Create combined report with all results
114
+ -q, --quick <host> <port> Quick test mode (supports ranges: 80,443 or 8000-8100)
115
+ -o, --output <file> Save quick mode results to file
116
+ -d, --dns <host> Resolve DNS and show IP address (accepts URLs)
117
+ -p, --ping <host> Ping host using ICMP (accepts URLs/IPs)
118
+ -s, --status <url> Check HTTP/HTTPS status code and response time
119
+ --cert <host> Check SSL/TLS certificate validity and expiration
120
+ --my-ip, -ip Show all network interfaces and IP addresses (UP only)
121
+ --my-ip --all Show all interfaces including inactive ones
122
+ --retry <number> Retry failed connections N times (default: 1, no retry)
123
+ --retry-delay <seconds> Delay between retries in seconds (default: 1)
124
+ --csv Input file is in CSV format (host,port)
125
+ -h, --help Show this help message
126
+ -v, --version Show version information
127
+
128
+ INPUT:
129
+ input_file File containing IP:port pairs (one per line)
130
+ If not specified, reads from stdin
131
+ Use --csv flag for CSV format files
132
+
133
+ EXAMPLES:
134
+ {cmd_name} ip-text.txt # Basic usage
135
+ {cmd_name} --csv hosts.csv # Read from CSV file
136
+ {cmd_name} -t 10 -j 20 ip-text.txt # Custom timeout and parallel jobs
137
+ {cmd_name} -f json -c ip-text.txt # JSON output with combined report
138
+ cat ip-text.txt | {cmd_name} -V # Verbose mode from stdin
139
+ {cmd_name} -q 192.168.1.1 80 # Quick test single port
140
+ {cmd_name} -q google.com 80,443 # Quick test multiple ports
141
+ {cmd_name} -q 10.0.0.1-50 22 # Quick test IP range
142
+ {cmd_name} -q 192.168.1.90-95 22 -o results.txt # Save quick mode to file
143
+ {cmd_name} -q 10.0.0.1-100 22 -j 20 # Quick mode with parallel jobs
144
+ {cmd_name} -d google.com # Resolve DNS to IP
145
+ {cmd_name} -d https://api.example.com # DNS from URL (strips scheme/path)
146
+ {cmd_name} -p 8.8.8.8 # Ping Google DNS
147
+ {cmd_name} -p https://github.com # Ping from URL
148
+ {cmd_name} -s https://google.com # Check HTTP status
149
+ {cmd_name} -s api.example.com -V # HTTP status with headers
150
+ {cmd_name} --cert https://google.com # Check SSL certificate
151
+ {cmd_name} --cert github.com:443 -V # Certificate with SANs
152
+ {cmd_name} --my-ip # Show all network interfaces and IPs
153
+ {cmd_name} --my-ip --all # Show all interfaces (including down)
154
+ {cmd_name} --retry 3 --retry-delay 2 hosts.txt # Retry failed connections 3 times with 2s delay
155
+ {cmd_name} -v # Show version
156
+ {cmd_name} -q localhost 8000-8100 # Quick test port range
157
+ echo "192.168.1.1-50 80" | {cmd_name} # Check IP range
158
+ echo "192.168.1.0/24 22" | {cmd_name} # Check CIDR subnet
159
+ echo "host.com 80,443,8080" | {cmd_name} # Check multiple ports
160
+ echo "host.com 8000-8100" | {cmd_name} # Check port range
161
+
162
+ INPUT FORMAT:
163
+ Each line should contain: HOST PORT(S)
164
+
165
+ Basic: 192.168.1.1 80
166
+ IP Range: 192.168.1.1-50 80 (checks .1 through .50)
167
+ CIDR: 192.168.1.0/24 80 (checks entire subnet)
168
+ Multi-port: 192.168.1.1 80,443,8080 (checks multiple ports)
169
+ Port Range: 192.168.1.1 8000-8100 (checks port range)
170
+ Combined: 192.168.1.1-10 80,443 (IP range with multiple ports)
171
+
172
+ CSV FORMAT (with --csv flag):
173
+ host,port
174
+ 192.168.1.1,80
175
+ server.com,443
176
+ 10.0.0.1-5,22 (ranges supported)
177
+ host.local,"80,443" (multiple ports in quotes)
178
+ """
179
+ print(help_text)
180
+
181
+ class NetCheckArgumentParser(argparse.ArgumentParser):
182
+ """Custom parser to output advanced example-rich help on syntax or argument errors."""
183
+ def error(self, message):
184
+ print(f"Error: {message}\n", file=sys.stderr)
185
+ print_help()
186
+ sys.exit(2)
187
+
188
+ def main():
189
+ # Force stdout and stderr to UTF-8 to prevent UnicodeEncodeError on Windows
190
+ try:
191
+ sys.stdout.reconfigure(encoding="utf-8")
192
+ sys.stderr.reconfigure(encoding="utf-8")
193
+ except (AttributeError, TypeError):
194
+ pass
195
+
196
+ if len(sys.argv) < 2:
197
+ # Check if stdin has data
198
+ if not sys.stdin.isatty():
199
+ lines = sys.stdin.read().splitlines()
200
+ run_batch_lines(lines, timeout=5.0, max_jobs=10, format_name="text", combined=False, retries=1, retry_delay=1.0, verbose=False)
201
+ return
202
+ print_help()
203
+ sys.exit(1)
204
+
205
+ first_arg = sys.argv[1]
206
+
207
+ # 1. Redesigned Subcommand Route
208
+ if first_arg in ("tcp", "dns", "http", "ssl", "ping", "interfaces"):
209
+ handle_subcommands(first_arg, sys.argv[2:])
210
+ return
211
+
212
+ # 2. Legacy Parsing Route
213
+ parser = NetCheckArgumentParser(add_help=False)
214
+ parser.add_argument("-h", "--help", action="store_true")
215
+ parser.add_argument("-v", "--version", action="store_true")
216
+ parser.add_argument("-q", "--quick", nargs=2)
217
+ parser.add_argument("-d", "--dns")
218
+ parser.add_argument("-p", "--ping")
219
+ parser.add_argument("-s", "--status")
220
+ parser.add_argument("--cert")
221
+ parser.add_argument("-ip", "--my-ip", action="store_true")
222
+ parser.add_argument("--mcp", action="store_true")
223
+ parser.add_argument("--csv", action="store_true")
224
+ parser.add_argument("-t", "--timeout", type=float, default=5.0)
225
+ parser.add_argument("-j", "--jobs", type=int, default=10)
226
+ parser.add_argument("-f", "--format", default="text", choices=["text", "json", "csv", "xml"])
227
+ parser.add_argument("-c", "--combined", action="store_true")
228
+ parser.add_argument("-o", "--output")
229
+ parser.add_argument("--retry", type=int, default=1)
230
+ parser.add_argument("--retry-delay", type=float, default=1.0)
231
+ parser.add_argument("-V", "--verbose", action="store_true")
232
+ parser.add_argument("--all", action="store_true")
233
+ parser.add_argument("input_file", nargs="?")
234
+
235
+ args, unknown = parser.parse_known_args()
236
+
237
+ if args.help:
238
+ print_help()
239
+ sys.exit(0)
240
+
241
+ if args.version:
242
+ from netcheck import __version__
243
+ print(f"netcheck version {__version__}")
244
+ sys.exit(0)
245
+
246
+ if args.mcp:
247
+ from netcheck.mcp.server import start_mcp_server
248
+ start_mcp_server()
249
+ return
250
+
251
+ # Apply format and parameters
252
+ fmt = args.format
253
+ timeout = args.timeout
254
+ retries = args.retry
255
+ retry_delay = args.retry_delay
256
+ verbose = args.verbose
257
+
258
+ if args.my_ip:
259
+ res = get_network_interfaces(all_interfaces=args.all)
260
+ print(format_output([res], fmt, verbose=verbose))
261
+ sys.exit(0 if res["success"] else 1)
262
+
263
+ if args.dns:
264
+ res = run_check_with_retry(dns_lookup, (args.dns, timeout), retries=retries, delay=retry_delay)
265
+ print(format_output([res], fmt, verbose=verbose))
266
+ sys.exit(0 if res["success"] else 1)
267
+
268
+ if args.ping:
269
+ res = run_check_with_retry(ping_host, (args.ping, 4, timeout), retries=retries, delay=retry_delay)
270
+ print(format_output([res], fmt, verbose=verbose))
271
+ sys.exit(0 if res["success"] else 1)
272
+
273
+ if args.status:
274
+ res = run_check_with_retry(check_http_status, (args.status, timeout), retries=retries, delay=retry_delay)
275
+ print(format_output([res], fmt, verbose=verbose))
276
+ sys.exit(0 if res["success"] else 1)
277
+
278
+ if args.cert:
279
+ res = run_check_with_retry(check_ssl_certificate, (args.cert, 443, timeout), retries=retries, delay=retry_delay)
280
+ print(format_output([res], fmt, verbose=verbose))
281
+ sys.exit(0 if res["success"] else 1)
282
+
283
+ if args.quick:
284
+ host, port_str = args.quick
285
+ run_quick_test(host, port_str, timeout, args.jobs, fmt, args.output, retries, retry_delay, verbose=verbose)
286
+ return
287
+
288
+ # Stdin or File Batch checks
289
+ targets = []
290
+ if args.csv:
291
+ if args.input_file:
292
+ targets = parse_csv_file(args.input_file)
293
+ elif not sys.stdin.isatty():
294
+ targets = parse_csv_content(sys.stdin.read())
295
+ else:
296
+ print("Error: No CSV input file or stdin stream provided", file=sys.stderr)
297
+ sys.exit(1)
298
+ run_batch_targets(targets, timeout, args.jobs, fmt, args.combined, retries, retry_delay, verbose=verbose)
299
+ return
300
+
301
+ if args.input_file:
302
+ targets = parse_batch_file(args.input_file)
303
+ run_batch_targets(targets, timeout, args.jobs, fmt, args.combined, retries, retry_delay, verbose=verbose)
304
+ return
305
+
306
+ # Stdin fallback if no args are matched
307
+ if not sys.stdin.isatty():
308
+ targets = parse_batch_content(sys.stdin.read())
309
+ run_batch_targets(targets, timeout, args.jobs, fmt, args.combined, retries, retry_delay, verbose=verbose)
310
+ return
311
+
312
+ print_help()
313
+ sys.exit(1)
314
+
315
+ def handle_subcommands(subcommand: str, sub_args: List[str]):
316
+ parser = argparse.ArgumentParser(prog=f"netcheck {subcommand}")
317
+ parser.add_argument("-t", "--timeout", type=float, default=5.0)
318
+ parser.add_argument("-f", "--format", default="text", choices=["text", "json", "csv", "xml"])
319
+ parser.add_argument("--retry", type=int, default=1)
320
+ parser.add_argument("--retry-delay", type=float, default=1.0)
321
+ parser.add_argument("-V", "--verbose", action="store_true")
322
+
323
+ if subcommand == "tcp":
324
+ parser.add_argument("host")
325
+ parser.add_argument("port")
326
+ parser.add_argument("-j", "--jobs", type=int, default=10)
327
+ parser.add_argument("-o", "--output")
328
+ args = parser.parse_args(sub_args)
329
+ run_quick_test(args.host, args.port, args.timeout, args.jobs, args.format, args.output, args.retry, args.retry_delay, verbose=args.verbose)
330
+
331
+ elif subcommand == "dns":
332
+ parser.add_argument("host")
333
+ args = parser.parse_args(sub_args)
334
+ res = run_check_with_retry(dns_lookup, (args.host, args.timeout), retries=args.retry, delay=args.retry_delay)
335
+ print(format_output([res], args.format, verbose=args.verbose))
336
+ sys.exit(0 if res["success"] else 1)
337
+
338
+ elif subcommand == "http":
339
+ parser.add_argument("url")
340
+ args = parser.parse_args(sub_args)
341
+ res = run_check_with_retry(check_http_status, (args.url, args.timeout), retries=args.retry, delay=args.retry_delay)
342
+ print(format_output([res], args.format, verbose=args.verbose))
343
+ sys.exit(0 if res["success"] else 1)
344
+
345
+ elif subcommand == "ssl":
346
+ parser.add_argument("host")
347
+ parser.add_argument("port", type=int, nargs="?", default=443)
348
+ args = parser.parse_args(sub_args)
349
+ res = run_check_with_retry(check_ssl_certificate, (args.host, args.port, args.timeout), retries=args.retry, delay=args.retry_delay)
350
+ print(format_output([res], args.format, verbose=args.verbose))
351
+ sys.exit(0 if res["success"] else 1)
352
+
353
+ elif subcommand == "ping":
354
+ parser.add_argument("host")
355
+ parser.add_argument("-c", "--count", type=int, default=4)
356
+ args = parser.parse_args(sub_args)
357
+ res = run_check_with_retry(ping_host, (args.host, args.count, args.timeout), retries=args.retry, delay=args.retry_delay)
358
+ print(format_output([res], args.format, verbose=args.verbose))
359
+ sys.exit(0 if res["success"] else 1)
360
+
361
+ elif subcommand == "interfaces":
362
+ parser.add_argument("--all", action="store_true")
363
+ args = parser.parse_args(sub_args)
364
+ res = get_network_interfaces(all_interfaces=args.all)
365
+ print(format_output([res], args.format, verbose=args.verbose))
366
+ sys.exit(0 if res["success"] else 1)
367
+
368
+ def run_quick_test(host: str, port_str: str, timeout: float, max_jobs: int, fmt: str, output_file: str, retries: int, retry_delay: float, verbose: bool = False):
369
+ hosts = expand_ip_range(host)
370
+ ports = expand_port_range(port_str)
371
+
372
+ targets = []
373
+ for h in hosts:
374
+ for p in ports:
375
+ targets.append((h, p))
376
+
377
+ if not targets:
378
+ print("Error: No valid host or port specified", file=sys.stderr)
379
+ sys.exit(1)
380
+
381
+ results = execute_concurrent_checks(targets, timeout, max_jobs, retries, retry_delay, verbose=verbose)
382
+
383
+ output_str = format_output(results, fmt, verbose=verbose)
384
+ print(output_str)
385
+
386
+ if output_file:
387
+ try:
388
+ with open(output_file, "w") as f:
389
+ f.write(format_output(results, fmt, verbose=verbose, use_color=False))
390
+ print(f"Results saved to: {output_file}")
391
+ except Exception as e:
392
+ print(f"Error saving results to file {output_file}: {e}", file=sys.stderr)
393
+
394
+ all_success = all(r["success"] for r in results)
395
+ sys.exit(0 if all_success else 1)
396
+
397
+ def run_batch_targets(targets: List[Tuple[str, str]], timeout: float, max_jobs: int, fmt: str, combined: bool, retries: int, retry_delay: float, verbose: bool = False):
398
+ expanded_targets = []
399
+ for host, p_str in targets:
400
+ ports = expand_port_range(p_str)
401
+ hosts = expand_ip_range(host)
402
+ for h in hosts:
403
+ for p in ports:
404
+ expanded_targets.append((h, p))
405
+
406
+ if not expanded_targets:
407
+ print("Error: No targets found to test", file=sys.stderr)
408
+ sys.exit(1)
409
+
410
+ results = execute_concurrent_checks(expanded_targets, timeout, max_jobs, retries, retry_delay, verbose=verbose)
411
+
412
+ date_str = datetime.now().strftime("%Y-%m-%d")
413
+ ext = "json" if fmt == "json" else "csv" if fmt == "csv" else "xml" if fmt == "xml" else "txt"
414
+
415
+ success_results = [r for r in results if r["success"]]
416
+ fail_results = [r for r in results if not r["success"]]
417
+
418
+ res_filename = f"result-{date_str}.{ext}"
419
+ fail_filename = f"fail-{date_str}.{ext}"
420
+ comb_filename = f"combined-{date_str}.{ext}"
421
+
422
+ try:
423
+ if success_results:
424
+ with open(res_filename, "w") as f:
425
+ f.write(format_output(success_results, fmt, verbose=verbose, use_color=False))
426
+ if fail_results:
427
+ with open(fail_filename, "w") as f:
428
+ f.write(format_output(fail_results, fmt, verbose=verbose, use_color=False))
429
+ if combined:
430
+ with open(comb_filename, "w") as f:
431
+ f.write(format_output(results, fmt, verbose=verbose, use_color=False))
432
+
433
+ print(f"Check Complete! Results written to output files.")
434
+ print(f"Successful checks written to: {res_filename} ({len(success_results)} items)")
435
+ print(f"Failed checks written to: {fail_filename} ({len(fail_results)} items)")
436
+ if combined:
437
+ print(f"Combined report written to: {comb_filename}")
438
+
439
+ except Exception as e:
440
+ print(f"Error saving batch output files: {e}", file=sys.stderr)
441
+
442
+ print(format_output(results, fmt, verbose=verbose))
443
+ sys.exit(0 if len(fail_results) == 0 else 1)
444
+
445
+ def run_batch_lines(lines: List[str], timeout: float, max_jobs: int, format_name: str, combined: bool, retries: int, retry_delay: float, verbose: bool = False):
446
+ content = "\n".join(lines)
447
+ targets = parse_batch_content(content)
448
+ run_batch_targets(targets, timeout, max_jobs, format_name, combined, retries, retry_delay, verbose=verbose)
449
+
450
+ def execute_concurrent_checks(targets: List[Tuple[str, int]], timeout: float, max_jobs: int, retries: int, retry_delay: float, verbose: bool = False) -> List[Dict[str, Any]]:
451
+ results = []
452
+
453
+ with ThreadPoolExecutor(max_workers=max_jobs) as executor:
454
+ futures = {}
455
+ for host, port in targets:
456
+ fut = executor.submit(
457
+ run_check_with_retry,
458
+ check_tcp_connect,
459
+ args=(host, int(port), timeout),
460
+ retries=retries,
461
+ delay=retry_delay
462
+ )
463
+ futures[fut] = (host, port)
464
+
465
+ completed = 0
466
+ total = len(targets)
467
+
468
+ for fut in as_completed(futures):
469
+ host, port = futures[fut]
470
+ try:
471
+ res = fut.result()
472
+ results.append(res)
473
+ except Exception as e:
474
+ res = {
475
+ "target": f"{host}:{port}",
476
+ "status": "FAILED",
477
+ "latency_ms": 0.0,
478
+ "success": False,
479
+ "error": str(e),
480
+ "metadata": {"host": host, "port": port}
481
+ }
482
+ results.append(res)
483
+
484
+ completed += 1
485
+
486
+ # Print real-time connection status if verbose is enabled
487
+ if verbose:
488
+ use_color = sys.stdout.isatty()
489
+ c_ansi = get_colors(use_color)
490
+ if res.get("success", False):
491
+ sys.stderr.write(f"{c_ansi['green']}✓ SUCCESS:{c_ansi['reset']} {host}:{port} ({res.get('latency_ms', '?')}ms)\n")
492
+ else:
493
+ sys.stderr.write(f"{c_ansi['red']}✗ FAILED:{c_ansi['reset']} {host}:{port} ({res.get('error', 'unknown error')})\n")
494
+ sys.stderr.flush()
495
+ elif total > 5 and sys.stdout.isatty():
496
+ sys.stdout.write(f"\rProgress: {completed}/{total} completed ({int(completed/total * 100)}%)...")
497
+ sys.stdout.flush()
498
+
499
+ if total > 5 and sys.stdout.isatty() and not verbose:
500
+ print("")
501
+
502
+ return results
503
+
504
+ def format_output(results: List[Dict[str, Any]], format_name: str, verbose: bool = False, use_color: Optional[bool] = None) -> str:
505
+ if format_name == "json":
506
+ return format_json(results)
507
+ elif format_name == "csv":
508
+ return format_csv(results)
509
+ elif format_name == "xml":
510
+ return format_xml(results)
511
+ else:
512
+ return format_text(results, verbose=verbose, use_color=use_color)
netcheck/mcp/server.py ADDED
@@ -0,0 +1,110 @@
1
+ import sys
2
+ import json
3
+ from netcheck.mcp.tools import TOOLS_LIST, call_tool
4
+
5
+ def start_mcp_server():
6
+ """
7
+ Starts the MCP Server listening for JSON-RPC 2.0 messages on stdin
8
+ and writing responses to stdout. Redirects other standard outputs to stderr.
9
+ """
10
+ # Configure stdin and stdout to use UTF-8 and line-buffering
11
+ sys.stdout.reconfigure(encoding='utf-8')
12
+ sys.stdin.reconfigure(encoding='utf-8')
13
+
14
+ # Save original stdout for JSON-RPC messages
15
+ original_stdout = sys.stdout
16
+
17
+ # Redirect all standard prints to sys.stderr to prevent corrupting the JSON-RPC stream
18
+ sys.stdout = sys.stderr
19
+
20
+ for line in sys.stdin:
21
+ line = line.strip()
22
+ if not line:
23
+ continue
24
+ try:
25
+ request = json.loads(line)
26
+ method = request.get("method")
27
+ req_id = request.get("id")
28
+
29
+ if method == "initialize":
30
+ response = {
31
+ "jsonrpc": "2.0",
32
+ "id": req_id,
33
+ "result": {
34
+ "protocolVersion": "2024-11-05",
35
+ "capabilities": {
36
+ "tools": {}
37
+ },
38
+ "serverInfo": {
39
+ "name": "netcheck",
40
+ "version": "2.1.0"
41
+ }
42
+ }
43
+ }
44
+ elif method == "notifications/initialized":
45
+ continue
46
+ elif method == "tools/list":
47
+ response = {
48
+ "jsonrpc": "2.0",
49
+ "id": req_id,
50
+ "result": {
51
+ "tools": TOOLS_LIST
52
+ }
53
+ }
54
+ elif method == "tools/call":
55
+ params = request.get("params", {})
56
+ tool_name = params.get("name")
57
+ tool_args = params.get("arguments", {})
58
+
59
+ tool_res = call_tool(tool_name, tool_args)
60
+
61
+ response = {
62
+ "jsonrpc": "2.0",
63
+ "id": req_id,
64
+ "result": tool_res
65
+ }
66
+ elif method == "ping":
67
+ response = {
68
+ "jsonrpc": "2.0",
69
+ "id": req_id,
70
+ "result": {}
71
+ }
72
+ else:
73
+ response = {
74
+ "jsonrpc": "2.0",
75
+ "id": req_id,
76
+ "error": {
77
+ "code": -32601,
78
+ "message": f"Method not found: {method}"
79
+ }
80
+ }
81
+
82
+ original_stdout.write(json.dumps(response) + "\n")
83
+ original_stdout.flush()
84
+
85
+ except json.JSONDecodeError:
86
+ response = {
87
+ "jsonrpc": "2.0",
88
+ "id": None,
89
+ "error": {
90
+ "code": -32700,
91
+ "message": "Parse error"
92
+ }
93
+ }
94
+ original_stdout.write(json.dumps(response) + "\n")
95
+ original_stdout.flush()
96
+ except Exception as e:
97
+ req_id = locals().get("req_id", None)
98
+ response = {
99
+ "jsonrpc": "2.0",
100
+ "id": req_id,
101
+ "error": {
102
+ "code": -32603,
103
+ "message": f"Internal error: {str(e)}"
104
+ }
105
+ }
106
+ original_stdout.write(json.dumps(response) + "\n")
107
+ original_stdout.flush()
108
+
109
+ # Restore stdout upon exit
110
+ sys.stdout = original_stdout