# Directory Structure ``` ├── .dockerignore ├── client_config.json ├── Dockerfile ├── LICENSE ├── pyproject.toml ├── README.md ├── src │ └── nmap_mcp │ ├── __init__.py │ ├── __main__.py │ ├── __pycache__ │ │ ├── __init__.cpython-313.pyc │ │ ├── __main__.cpython-313.pyc │ │ └── server.cpython-313.pyc │ ├── server.py │ └── test.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` 1 | .git 2 | .gitignore 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | *.so 7 | .Python 8 | env/ 9 | build/ 10 | develop-eggs/ 11 | dist/ 12 | downloads/ 13 | eggs/ 14 | .eggs/ 15 | lib/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | .env 24 | .venv 25 | venv/ 26 | ENV/ 27 | .idea/ 28 | .vscode/ 29 | *.swp 30 | *.swo ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Nmap MCP Server 2 | 3 | This is a Model Control Protocol (MCP) server that provides access to nmap network scanning functionality. 4 | 5 | ## Features 6 | 7 | - Run nmap scans on specified targets with customizable options 8 | - Store and retrieve scan results 9 | - Analyze scan results using AI prompts 10 | 11 | ## Installation 12 | 13 | Requirements: 14 | - Python 3.10+ 15 | - python-libnmap 16 | - nmap (installed on the system) 17 | 18 | ```bash 19 | pip install python-libnmap 20 | ``` 21 | 22 | Make sure nmap is installed on your system: 23 | ```bash 24 | # On Debian/Ubuntu 25 | sudo apt-get install nmap 26 | 27 | # On Fedora/CentOS 28 | sudo dnf install nmap 29 | ``` 30 | 31 | ## Usage 32 | 33 | ### Running the Server 34 | 35 | To run the server directly from the source code: 36 | 37 | ```bash 38 | python -m src.nmap_mcp 39 | ``` 40 | 41 | To install the package and run as a command: 42 | 43 | ```bash 44 | pip install -e . 45 | nmap-mcp 46 | ``` 47 | 48 | ### Available Tools 49 | 50 | 1. **run-nmap-scan** 51 | - Run an nmap scan on specified targets 52 | - Parameters: 53 | - `target`: Target host or network (e.g., 192.168.1.1 or 192.168.1.0/24) 54 | - `options`: Nmap options (e.g., -sV -p 1-1000) 55 | 56 | 2. **get-scan-details** 57 | - Get detailed information about a specific scan 58 | - Parameters: 59 | - `scan_id`: ID of the scan to retrieve 60 | 61 | 3. **list-all-scans** 62 | - List all available scan results 63 | - No parameters required 64 | 65 | ### Available Prompts 66 | 67 | 1. **analyze-scan** 68 | - Analyze an nmap scan result 69 | - Parameters: 70 | - `scan_id`: ID of the scan to analyze 71 | - `focus`: Focus area (security/services/overview) 72 | 73 | ### Resources 74 | 75 | Scan results are available as resources with the `nmap://scan/{scan_id}` URI scheme. 76 | 77 | ## Example Workflow 78 | 79 | 1. Run a scan: 80 | ``` 81 | Call tool: run-nmap-scan 82 | Parameters: {"target": "192.168.1.0/24", "options": "-sV -p 22,80,443"} 83 | ``` 84 | 85 | 2. Get scan details: 86 | ``` 87 | Call tool: get-scan-details 88 | Parameters: {"scan_id": "<scan_id from previous step>"} 89 | ``` 90 | 91 | 3. List all scans: 92 | ``` 93 | Call tool: list-all-scans 94 | ``` 95 | 96 | 4. Analyze scan results: 97 | ``` 98 | Get prompt: analyze-scan 99 | Parameters: {"scan_id": "<scan_id>", "focus": "security"} 100 | ``` 101 | 102 | ## Security Considerations 103 | 104 | This server executes nmap commands on your system. Be cautious when scanning networks you don't own or have permission to scan, as unauthorized scanning may be illegal in some jurisdictions. 105 | 106 | ## Troubleshooting 107 | 108 | If you encounter errors related to nmap not being found or being executed incorrectly: 109 | 110 | 1. Make sure nmap is installed and available in your PATH 111 | 2. Check the logs for which nmap executable is being used 112 | 3. The server will attempt to use the full path to nmap to avoid conflicts 113 | 114 | ## Docker Usage 115 | 116 | You can run the MCP server in a Docker container: 117 | 118 | ```bash 119 | # Build the Docker image 120 | docker build -t nmap-mcp-server . 121 | 122 | # Run the Docker container 123 | docker run -it --rm nmap-mcp-server 124 | ``` 125 | 126 | For integration with the Glama MCP directory, the Docker container allows others to easily use this MCP server without worrying about installation dependencies. 127 | 128 | ## License 129 | 130 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ``` -------------------------------------------------------------------------------- /client_config.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "mcpServers": { 3 | "nmap-mcp": { 4 | "command": "uv", 5 | "args": [ 6 | "--directory", 7 | "/home/kali/mcp-servers/nmap", 8 | "run", 9 | "nmap-mcp" 10 | ] 11 | } 12 | } 13 | } ``` -------------------------------------------------------------------------------- /src/nmap_mcp/__main__.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Main entry point for the nmap MCP server. 4 | This allows running the server as `python -m src.nmap_mcp`. 5 | """ 6 | 7 | from . import main 8 | 9 | if __name__ == "__main__": 10 | # Run the package's main function 11 | main() ``` -------------------------------------------------------------------------------- /src/nmap_mcp/__init__.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | Nmap MCP Server Package 3 | 4 | This package provides an MCP (Model Control Protocol) interface for running nmap scans. 5 | It allows AI assistants to run network scans and analyze the results. 6 | """ 7 | 8 | __version__ = "0.1.0" 9 | 10 | from . import server 11 | import asyncio 12 | 13 | def main(): 14 | """Main entry point for the package.""" 15 | asyncio.run(server.main()) 16 | 17 | # Optionally expose other important items at package level 18 | __all__ = ['main', 'server'] ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "nmap-mcp" 3 | version = "0.1.0" 4 | description = "A MCP server for nmap network scanning" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | license = {text = "MIT"} 8 | dependencies = [ 9 | "mcp>=1.4.1", 10 | "python-libnmap>=0.7.2" 11 | ] 12 | 13 | [build-system] 14 | requires = [ "hatchling",] 15 | build-backend = "hatchling.build" 16 | 17 | [project.scripts] 18 | nmap-mcp = "nmap_mcp:main" 19 | 20 | [tool.hatch.build.targets.wheel] 21 | packages = ["src/nmap_mcp"] 22 | 23 | [tool.hatch.build] 24 | packages = ["src"] 25 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | FROM python:3.10-slim 2 | 3 | # Install system dependencies 4 | RUN apt-get update && apt-get install -y \ 5 | nmap \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | # Set working directory 9 | WORKDIR /app 10 | 11 | # Copy project files 12 | COPY pyproject.toml README.md LICENSE ./ 13 | COPY src ./src/ 14 | 15 | # Install Python dependencies and the package 16 | RUN pip install --no-cache-dir -e . 17 | 18 | # Expose port if needed 19 | # Note: MCP servers often use stdio, but if you need a port, uncomment and set accordingly 20 | # EXPOSE 8080 21 | 22 | # Run the MCP server 23 | CMD ["python", "-m", "src.nmap_mcp"] ``` -------------------------------------------------------------------------------- /src/nmap_mcp/test.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | Simple test script for the nmap MCP server. 4 | This script tests basic functionality without requiring a full MCP client. 5 | """ 6 | 7 | import asyncio 8 | import sys 9 | import time 10 | from libnmap.process import NmapProcess 11 | from libnmap.parser import NmapParser 12 | 13 | async def test_nmap_functionality(): 14 | """Test that we can run nmap scans and parse results.""" 15 | print("Testing nmap functionality...") 16 | 17 | # Test local scan with minimal options to ensure nmap works 18 | target = "127.0.0.1" 19 | options = "-p 22,80 -sV" 20 | 21 | print(f"Running nmap scan on {target} with options: {options}") 22 | nm_process = NmapProcess(target, options) 23 | rc = nm_process.run() 24 | 25 | if rc != 0: 26 | print(f"Error running nmap: {nm_process.stderr}") 27 | return False 28 | 29 | try: 30 | parsed = NmapParser.parse(nm_process.stdout) 31 | 32 | print("\nScan results:") 33 | print(f" Target: {target}") 34 | print(f" Start time: {parsed.started}") 35 | print(f" Hosts found: {len(parsed.hosts)}") 36 | 37 | for host in parsed.hosts: 38 | print(f"\n Host: {host.address}") 39 | print(f" Status: {host.status}") 40 | print(f" Services:") 41 | 42 | for service in host.services: 43 | print(f" - Port {service.port}/{service.protocol}: {service.state}") 44 | if service.service: 45 | print(f" Service: {service.service}") 46 | if service.banner: 47 | print(f" Banner: {service.banner}") 48 | 49 | print("\nNmap functionality test passed!") 50 | return True 51 | 52 | except Exception as e: 53 | print(f"Error parsing nmap results: {str(e)}") 54 | return False 55 | 56 | if __name__ == "__main__": 57 | print("Nmap MCP Server Test Script") 58 | print("--------------------------") 59 | 60 | # Check if python-libnmap is installed 61 | try: 62 | print("Checking for python-libnmap...") 63 | import libnmap 64 | print(f"Found libnmap version: {libnmap.__version__}") 65 | except ImportError: 66 | print("Error: python-libnmap is not installed.") 67 | print("Please install it with: pip install python-libnmap") 68 | sys.exit(1) 69 | 70 | # Check if nmap is installed and available 71 | try: 72 | print("\nChecking for nmap...") 73 | test_process = NmapProcess("localhost", "-sV -p 22") 74 | test_process.run_background() 75 | 76 | # Wait for the process to complete with a timeout 77 | timeout = 30 # 30 seconds timeout 78 | start_time = time.time() 79 | while test_process.is_running() and time.time() - start_time < timeout: 80 | time.sleep(0.5) 81 | 82 | if test_process.is_running(): 83 | test_process.stop() 84 | raise Exception("Nmap test timed out") 85 | 86 | if test_process.rc != 0: 87 | raise Exception(f"Nmap test failed with return code {test_process.rc}") 88 | 89 | print("Nmap is available and working.") 90 | except Exception as e: 91 | print(f"Error: {str(e)}") 92 | print("Please make sure nmap is installed and available in your PATH.") 93 | sys.exit(1) 94 | 95 | # Run the async test 96 | print("\nRunning functional tests...") 97 | result = asyncio.run(test_nmap_functionality()) 98 | 99 | if not result: 100 | print("\nTEST FAILED: There were errors during the test.") 101 | sys.exit(1) 102 | 103 | print("\nAll tests passed! The nmap MCP server should work correctly.") 104 | print("You can now run the server with: python -m src.nmap.server") ``` -------------------------------------------------------------------------------- /src/nmap_mcp/server.py: -------------------------------------------------------------------------------- ```python 1 | import asyncio 2 | import json 3 | import uuid 4 | import time 5 | import logging 6 | import shutil 7 | import subprocess 8 | from typing import Dict, List, Optional, Set 9 | 10 | from libnmap.process import NmapProcess 11 | from libnmap.parser import NmapParser 12 | from mcp.server.models import InitializationOptions 13 | import mcp.types as types 14 | from mcp.server import NotificationOptions, Server 15 | from pydantic import AnyUrl 16 | import mcp.server.stdio 17 | 18 | # Configure logging 19 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 20 | logger = logging.getLogger("nmap-mcp") 21 | 22 | # Store scan results as a dictionary with scan_id as the key 23 | scan_results: Dict[str, Dict] = {} 24 | 25 | # Track ongoing scans to prevent duplicates 26 | ongoing_scans: Set[str] = set() 27 | 28 | # Rate limiting settings 29 | RATE_LIMIT_PERIOD = 60 # seconds 30 | RATE_LIMIT_MAX_SCANS = 3 31 | last_scan_times = [] 32 | 33 | # Find the full path to the nmap executable 34 | NMAP_PATH = shutil.which("nmap") 35 | if not NMAP_PATH: 36 | logger.error("Could not find nmap executable. Please ensure nmap is installed.") 37 | # Default to standard path if not found 38 | NMAP_PATH = "/usr/bin/nmap" 39 | 40 | logger.info(f"Using nmap executable at: {NMAP_PATH}") 41 | 42 | server = Server("nmap") 43 | 44 | @server.list_resources() 45 | async def handle_list_resources() -> list[types.Resource]: 46 | """ 47 | List available scan results as resources. 48 | Each scan result is exposed as a resource with a custom nmap:// URI scheme. 49 | """ 50 | return [ 51 | types.Resource( 52 | uri=AnyUrl(f"nmap://scan/{scan_id}"), 53 | name=f"Scan: {scan_data.get('target', 'Unknown')}", 54 | description=f"Nmap scan of {scan_data.get('target', 'Unknown')} - {scan_data.get('timestamp', 'Unknown')}", 55 | mimeType="application/json", 56 | ) 57 | for scan_id, scan_data in scan_results.items() 58 | ] 59 | 60 | @server.read_resource() 61 | async def handle_read_resource(uri: AnyUrl) -> str: 62 | """ 63 | Read a specific scan result by its URI. 64 | The scan ID is extracted from the URI path component. 65 | """ 66 | if uri.scheme != "nmap": 67 | raise ValueError(f"Unsupported URI scheme: {uri.scheme}") 68 | 69 | scan_id = uri.path 70 | if scan_id is not None: 71 | scan_id = scan_id.lstrip("/") 72 | if scan_id in scan_results: 73 | return json.dumps(scan_results[scan_id], indent=2) 74 | raise ValueError(f"Scan result not found: {scan_id}") 75 | 76 | @server.list_prompts() 77 | async def handle_list_prompts() -> list[types.Prompt]: 78 | """ 79 | List available prompts related to nmap scanning. 80 | """ 81 | return [ 82 | types.Prompt( 83 | name="analyze-scan", 84 | description="Analyze an nmap scan result", 85 | arguments=[ 86 | types.PromptArgument( 87 | name="scan_id", 88 | description="ID of the scan to analyze", 89 | required=True, 90 | ), 91 | types.PromptArgument( 92 | name="focus", 93 | description="Focus area (security/services/overview)", 94 | required=False, 95 | ) 96 | ], 97 | ) 98 | ] 99 | 100 | @server.get_prompt() 101 | async def handle_get_prompt( 102 | name: str, arguments: dict[str, str] | None 103 | ) -> types.GetPromptResult: 104 | """ 105 | Generate a prompt for analyzing nmap scan results. 106 | """ 107 | if name != "analyze-scan": 108 | raise ValueError(f"Unknown prompt: {name}") 109 | 110 | if not arguments or "scan_id" not in arguments: 111 | raise ValueError("Missing scan_id argument") 112 | 113 | scan_id = arguments["scan_id"] 114 | focus = arguments.get("focus", "overview") 115 | 116 | if scan_id not in scan_results: 117 | raise ValueError(f"Scan result not found: {scan_id}") 118 | 119 | scan_data = scan_results[scan_id] 120 | 121 | focus_prompt = "" 122 | if focus == "security": 123 | focus_prompt = "Focus on security vulnerabilities and potential risks." 124 | elif focus == "services": 125 | focus_prompt = "Focus on identifying running services and their versions." 126 | else: # overview 127 | focus_prompt = "Provide a general overview of the scan results." 128 | 129 | return types.GetPromptResult( 130 | description=f"Analyze nmap scan results for {scan_data.get('target', 'Unknown')}", 131 | messages=[ 132 | types.PromptMessage( 133 | role="user", 134 | content=types.TextContent( 135 | type="text", 136 | text=f"Analyze the following nmap scan results. {focus_prompt}\n\n{json.dumps(scan_data, indent=2)}", 137 | ), 138 | ) 139 | ], 140 | ) 141 | 142 | @server.list_tools() 143 | async def handle_list_tools() -> list[types.Tool]: 144 | """ 145 | List available tools for nmap scanning. 146 | """ 147 | return [ 148 | types.Tool( 149 | name="run-nmap-scan", 150 | description="Run an nmap scan on specified targets", 151 | inputSchema={ 152 | "type": "object", 153 | "properties": { 154 | "target": {"type": "string", "description": "Target host or network (e.g., 192.168.1.1 or 192.168.1.0/24)"}, 155 | "options": {"type": "string", "description": "Nmap options (e.g., -sV -p 1-1000)"}, 156 | }, 157 | "required": ["target"], 158 | }, 159 | ), 160 | types.Tool( 161 | name="get-scan-details", 162 | description="Get detailed information about a specific scan", 163 | inputSchema={ 164 | "type": "object", 165 | "properties": { 166 | "scan_id": {"type": "string", "description": "ID of the scan to retrieve"}, 167 | }, 168 | "required": ["scan_id"], 169 | }, 170 | ), 171 | types.Tool( 172 | name="list-all-scans", 173 | description="List all available scan results", 174 | inputSchema={ 175 | "type": "object", 176 | "properties": {}, 177 | }, 178 | ) 179 | ] 180 | 181 | def check_rate_limit() -> bool: 182 | """Check if we're exceeding the rate limit.""" 183 | global last_scan_times 184 | current_time = time.time() 185 | 186 | # Remove timestamps older than the rate limit period 187 | last_scan_times = [t for t in last_scan_times if current_time - t < RATE_LIMIT_PERIOD] 188 | 189 | # Check if we're under the limit 190 | return len(last_scan_times) < RATE_LIMIT_MAX_SCANS 191 | 192 | def add_scan_timestamp(): 193 | """Add current timestamp to track rate limiting.""" 194 | global last_scan_times 195 | last_scan_times.append(time.time()) 196 | 197 | def run_nmap_directly(target, options): 198 | """Run nmap directly using subprocess instead of relying on python-libnmap.""" 199 | try: 200 | # Construct the basic command with XML output 201 | cmd = [NMAP_PATH, "-oX", "-"] 202 | 203 | # Split options into separate arguments 204 | if options: 205 | option_args = options.split() 206 | cmd.extend(option_args) 207 | 208 | # Add target at the end 209 | cmd.append(target) 210 | logger.info(f"Executing nmap command: {' '.join(cmd)}") 211 | 212 | # Run the command and capture both stdout and stderr 213 | process = subprocess.run( 214 | cmd, 215 | capture_output=True, 216 | text=False, 217 | check=True 218 | ) 219 | 220 | return process.stdout, None 221 | except subprocess.CalledProcessError as e: 222 | return None, f"nmap failed with exit code {e.returncode}: {e.stderr.decode('utf-8', errors='replace')}" 223 | except Exception as e: 224 | return None, str(e) 225 | 226 | @server.call_tool() 227 | async def handle_call_tool( 228 | name: str, arguments: dict | None 229 | ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: 230 | """ 231 | Handle tool execution requests for nmap scanning. 232 | """ 233 | if not arguments and name != "list-all-scans": 234 | raise ValueError("Missing arguments") 235 | 236 | if name == "run-nmap-scan": 237 | target = arguments.get("target") 238 | options = arguments.get("options", "-sV") # Default to version detection 239 | 240 | if not target: 241 | raise ValueError("Missing target") 242 | 243 | # Create a unique scan identifier based on target and options 244 | scan_key = f"{target}:{options}" 245 | 246 | # Check if an identical scan is already running 247 | if scan_key in ongoing_scans: 248 | return [ 249 | types.TextContent( 250 | type="text", 251 | text=f"A scan with the same target and options is already running. Please wait for it to complete.", 252 | ) 253 | ] 254 | 255 | # Check rate limiting 256 | if not check_rate_limit(): 257 | return [ 258 | types.TextContent( 259 | type="text", 260 | text=f"Rate limit exceeded. Please wait before starting another scan. Maximum {RATE_LIMIT_MAX_SCANS} scans per {RATE_LIMIT_PERIOD} seconds.", 261 | ) 262 | ] 263 | 264 | try: 265 | # Mark this scan as ongoing 266 | ongoing_scans.add(scan_key) 267 | add_scan_timestamp() 268 | 269 | logger.info(f"Starting nmap scan on {target} with options {options}") 270 | 271 | # Use direct subprocess call instead of NmapProcess 272 | stdout, stderr = run_nmap_directly(target, options) 273 | 274 | if stderr: 275 | logger.error(f"Nmap scan failed: {stderr}") 276 | return [ 277 | types.TextContent( 278 | type="text", 279 | text=f"Nmap scan failed: {stderr}", 280 | ) 281 | ] 282 | 283 | # Parse results - convert bytes to string first 284 | try: 285 | xml_string = stdout.decode('utf-8', errors='replace') 286 | parsed = NmapParser.parse_fromstring(xml_string) 287 | except Exception as e: 288 | logger.error(f"Error parsing nmap results: {str(e)}") 289 | return [ 290 | types.TextContent( 291 | type="text", 292 | text=f"Error parsing nmap results: {str(e)}", 293 | ) 294 | ] 295 | 296 | # Generate a unique ID for this scan 297 | scan_id = str(uuid.uuid4()) 298 | 299 | # Store scan results 300 | scan_results[scan_id] = { 301 | "target": target, 302 | "options": options, 303 | "timestamp": parsed.started, 304 | "hosts": [ 305 | { 306 | "address": host.address, 307 | "status": host.status, 308 | "hostnames": [ 309 | hostname.name if hasattr(hostname, 'name') else str(hostname) 310 | for hostname in host.hostnames 311 | ], 312 | "services": [ 313 | { 314 | "port": service.port, 315 | "protocol": service.protocol, 316 | "state": service.state, 317 | "service": service.service, 318 | "banner": service.banner 319 | } 320 | for service in host.services 321 | ] 322 | } 323 | for host in parsed.hosts 324 | ] 325 | } 326 | 327 | # Notify clients that new resources are available 328 | await server.request_context.session.send_resource_list_changed() 329 | 330 | logger.info(f"Scan completed. Found {len(parsed.hosts)} hosts. Scan ID: {scan_id}") 331 | 332 | return [ 333 | types.TextContent( 334 | type="text", 335 | text=f"Scan completed. Found {len(parsed.hosts)} hosts. Scan ID: {scan_id}", 336 | ) 337 | ] 338 | except Exception as e: 339 | logger.error(f"Error during nmap scan: {str(e)}") 340 | return [ 341 | types.TextContent( 342 | type="text", 343 | text=f"Error during nmap scan: {str(e)}", 344 | ) 345 | ] 346 | finally: 347 | # Remove from ongoing scans when done 348 | ongoing_scans.discard(scan_key) 349 | 350 | elif name == "get-scan-details": 351 | scan_id = arguments.get("scan_id") 352 | 353 | if not scan_id: 354 | raise ValueError("Missing scan_id") 355 | 356 | if scan_id not in scan_results: 357 | return [ 358 | types.TextContent( 359 | type="text", 360 | text=f"Scan with ID {scan_id} not found", 361 | ) 362 | ] 363 | 364 | scan_data = scan_results[scan_id] 365 | 366 | # Extract summary information 367 | hosts_up = sum(1 for host in scan_data.get("hosts", []) if host.get("status") == "up") 368 | total_ports = sum(len(host.get("services", [])) for host in scan_data.get("hosts", [])) 369 | 370 | return [ 371 | types.TextContent( 372 | type="text", 373 | text=f"Scan of {scan_data.get('target')} (ID: {scan_id}):\n" 374 | f"- Options: {scan_data.get('options')}\n" 375 | f"- Timestamp: {scan_data.get('timestamp')}\n" 376 | f"- Hosts: {len(scan_data.get('hosts', []))} ({hosts_up} up)\n" 377 | f"- Total ports/services: {total_ports}\n\n" 378 | f"Use the nmap://scan/{scan_id} resource to access full results", 379 | ) 380 | ] 381 | elif name == "list-all-scans": 382 | if not scan_results: 383 | return [ 384 | types.TextContent( 385 | type="text", 386 | text="No scans have been performed yet.", 387 | ) 388 | ] 389 | 390 | scan_list = [] 391 | for scan_id, scan_data in scan_results.items(): 392 | hosts_count = len(scan_data.get("hosts", [])) 393 | scan_list.append(f"- Scan ID: {scan_id}") 394 | scan_list.append(f" Target: {scan_data.get('target')}") 395 | scan_list.append(f" Options: {scan_data.get('options')}") 396 | scan_list.append(f" Hosts: {hosts_count}") 397 | scan_list.append("") 398 | 399 | return [ 400 | types.TextContent( 401 | type="text", 402 | text="Available scans:\n\n" + "\n".join(scan_list), 403 | ) 404 | ] 405 | else: 406 | raise ValueError(f"Unknown tool: {name}") 407 | 408 | async def main(): 409 | logger.info("Starting nmap MCP server") 410 | # Run the server using stdin/stdout streams 411 | async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): 412 | await server.run( 413 | read_stream, 414 | write_stream, 415 | InitializationOptions( 416 | server_name="nmap", 417 | server_version="0.1.0", 418 | capabilities=server.get_capabilities( 419 | notification_options=NotificationOptions(), 420 | experimental_capabilities={}, 421 | ), 422 | ), 423 | ) ```