import os
import subprocess
import uuid
import datetime
import errno
import platform
from typing import List, Any, Union, Tuple
try:
import psutil
except ImportError:
psutil = None
def run_nmap(target: str, args: str) -> Tuple[str, int]:
"""
Launches nmap against `target` with optional `args`,
writing normal output to a timestamped file, and returns
the file path plus the scan process PID.
"""
executable = "nmap.exe" if os.name == 'nt' else "nmap"
output_dir = os.path.join(os.getcwd(), "nmap_output")
os.makedirs(output_dir, exist_ok=True)
ts = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
uid = uuid.uuid4().hex
filename = f"{ts}_{uid}"
output_path = os.path.join(output_dir, filename)
args_list = args.split() if args else []
cmd = [executable, "-oN", output_path] + args_list + [target]
if os.name == 'nt':
proc = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
)
else:
proc = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
pid = proc.pid
if psutil and platform.system() == "Windows":
try:
parent = psutil.Process(proc.pid)
for child in parent.children(recursive=True):
if child.name().lower().startswith("nmap"):
pid = child.pid
break
except Exception:
pass
return output_path, pid
def _is_process_running(pid: int) -> bool:
try:
os.kill(pid, 0)
except OSError as e:
if getattr(e, 'errno', None) == errno.ESRCH:
return False
if getattr(e, 'errno', None) == errno.EPERM:
return True
return False
except ValueError:
return True
return True
def get_nmap_output(file_path: str, pid: int) -> Union[str, List[str]]:
"""
If the scan is still running (process exists or file incomplete),
returns a “still running” message. Once finished, reads the file
and returns a list of lines.
"""
running = _is_process_running(pid)
if not os.path.isfile(file_path):
if running:
return f"Scan running with PID {pid}"
raise FileNotFoundError(f"Output file not found (scan PID {pid} has exited): {file_path}")
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
if not any("Nmap done at" in line for line in lines[-5:]):
return f"Scan still in progress (waiting for Nmap to finish)... PID {pid}"
return lines
from mcp.server.fastmcp import FastMCP
mcp = FastMCP()
@mcp.tool()
def launch_nmap_scan(target: str, args: str = "") -> dict:
"""
Launch an Nmap scan against `target` with optional `args`.
Returns:
- output_path: where plain text will be written
- pid: process ID to track
"""
output_path, pid = run_nmap(target, args)
return {"output_path": output_path, "pid": pid}
@mcp.tool()
def fetch_nmap_results(output_path: str, pid: int) -> Union[str, List[str]]:
"""
Read the normal Nmap output from `output_path` once the scan completes,
or report if it’s still running.
"""
return get_nmap_output(output_path, pid)
if __name__ == "__main__":
mcp.run()