# Directory Structure
```
├── .gitattributes
├── .github
│ ├── assets
│ │ └── nmap-mcp.gif
│ └── README.md
├── .gitignore
├── LICENSE
├── pyproject.toml
├── src
│ ├── __main__.py
│ ├── server.py
│ └── tools
│ ├── __main__.py
│ ├── banner.py
│ └── nmap.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
```
# Auto detect text files and perform LF normalization
* text=auto
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
```
--------------------------------------------------------------------------------
/.github/README.md:
--------------------------------------------------------------------------------
```markdown
# NMAP MCP Server
## Description
Simple MCP server for communicating with NMAP.
## Demo

## Getting Started
```bash
venv venv
source venv/bin/activate
pip install -e .
mcp dev src/server.py
```
## Resources
- [MCP Windows](https://gist.github.com/feveromo/7a340d7795fca1ccd535a5802b976e1f)
- [MCP Python SDK](https://glama.ai/mcp/servers/@wanderingnature/mcp-typed-prompts)
- [Building an MCP Server](https://www.dremio.com/blog/building-a-basic-mcp-server-with-python/)
- [MCP Quick Start](https://modelcontextprotocol.io/quickstart/server)
- [`uv` Version Error](https://bryantson.medium.com/how-to-solve-issue-pythons-package-and-project-manager-uv-error-because-the-requested-python-44f5af9c536f)
```
--------------------------------------------------------------------------------
/src/__main__.py:
--------------------------------------------------------------------------------
```python
```
--------------------------------------------------------------------------------
/src/tools/__main__.py:
--------------------------------------------------------------------------------
```python
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[project.scripts]
nmap-mcp = "server:main"
[build-system]
requires = ["hatchling>=1.11.0"]
build-backend = "hatchling.build"
[project]
requires-python = ">=3.10"
version = "0.0.1"
name = "nmap-mcp"
description = "MCP server for NMap."
readme = "README.md"
license = "MIT"
dependencies = [
"black",
"isort",
"mcp[cli]",
# Development/Testing
# "scapy",
]
[tool.hatch.build]
ignore-vcs = false
reproducible = true
directory = "dist"
sources = ["src"]
include = ["src/", "LICENSE", "pyproject.toml",]
```
--------------------------------------------------------------------------------
/src/server.py:
--------------------------------------------------------------------------------
```python
from tools import banner, nmap
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("NMAP MCP Server")
@mcp.tool()
def network_scan(ip_address: str, ports: str = None, scripts: list = None, arguments: str = None) -> dict:
"""
Perform a network scan using Nmap.
Args:
ip_address (str): The target IP address or hostname.
ports (str, optional): Comma-separated list of ports to scan (e.g., "80,443").
scripts (list, optional): List of NSE scripts to run.
arguments (str, optional): Additional Nmap arguments.
Returns:
dict: A dictionary containing the scan results.
"""
return nmap.run_scan(ip_address, ports, scripts, arguments)
@mcp.tool()
def grab_banner( host: str, port: int, timeout: float = 3.0, send_data: bytes = None) -> str:
"""
Attempts to grab a banner from a TCP service on the specified host and port.
Parameters:
host (str): Target hostname or IP address.
port (int): Port number to connect to.
timeout (float): Connection timeout in seconds (default: 3.0).
send_data (bytes, optional): Optional data to send after connecting.
Useful for protocols that require a trigger (e.g., "HEAD / HTTP/1.0\\r\\n\\r\\n").
Returns:
str: The received banner or response from the server (decoded to UTF-8, with errors ignored).
"""
return banner.grab(host, port, timeout, send_data)
def main():
mcp.run(transport="stdio")
if __name__ == "__main__":
mcp.run(transport="stdio")
```
--------------------------------------------------------------------------------
/src/tools/banner.py:
--------------------------------------------------------------------------------
```python
import socket
from typing import Optional
def grab(
host: str,
port: int,
timeout: float = 3.0,
send_data: Optional[bytes] = None
) -> str:
"""
Attempts to grab a banner from a TCP service on the specified host and port.
Parameters:
host (str): Target hostname or IP address.
port (int): Port number to connect to.
timeout (float): Connection timeout in seconds (default: 3.0).
send_data (bytes, optional): Optional data to send after connecting.
Useful for protocols that require a trigger (e.g., "HEAD / HTTP/1.0\\r\\n\\r\\n").
Returns:
str: The received banner or response from the server (decoded to UTF-8, with errors ignored).
Raises:
ValueError: If the host is empty or the port is invalid.
socket.error: If a connection or communication error occurs.
"""
if not host or not isinstance(port, int) or not (1 <= port <= 65535):
raise ValueError("Invalid host or port.")
banner = b""
try:
with socket.create_connection((host, port), timeout=timeout) as sock:
# Optionally send a protocol-specific request
if send_data:
sock.sendall(send_data)
# Try to receive banner data
banner = sock.recv(1024)
except socket.timeout:
return "Connection timed out."
except socket.error as e:
return f"Socket error: {e}"
return banner.decode("utf-8", errors="ignore").strip()
```
--------------------------------------------------------------------------------
/src/tools/nmap.py:
--------------------------------------------------------------------------------
```python
import subprocess
import shlex
from typing import List, Optional, Dict
def run_scan(
target: str,
ports: Optional[str] = None,
scripts: Optional[List[str]] = None,
arguments: Optional[str] = None,
timeout: int = 60
) -> Dict[str, str]:
"""
Runs an Nmap scan on the specified target with optional ports, NSE scripts, and arguments.
Parameters:
target (str): The IP address or hostname of the target system.
ports (str, optional): Comma-separated list of ports or port ranges to scan (e.g., "80,443" or "1-1000").
scripts (List[str], optional): A list of NSE scripts to include in the scan (e.g., ["http-title", "ssh-auth-methods"]).
arguments (str, optional): Additional raw arguments to pass to Nmap (e.g., "-sV -Pn").
timeout (int): Time in seconds before the scan is forcefully terminated.
Returns:
Dict[str, str]: A dictionary with two keys:
- "stdout": The standard output from the Nmap scan.
- "stderr": The standard error output, if any.
Raises:
ValueError: If the target is not provided or is empty.
subprocess.TimeoutExpired: If the Nmap scan exceeds the timeout duration.
subprocess.CalledProcessError: If Nmap exits with a non-zero status.
"""
if not target.strip():
raise ValueError("Target must be a non-empty string.")
# Construct the base Nmap command
cmd_parts = ["nmap", target]
# Append port range if specified
if ports:
cmd_parts.extend(["-p", ports])
# Append NSE scripts if provided
if scripts:
script_str = ",".join(scripts)
cmd_parts.extend(["--script", script_str])
# Append any additional arguments (e.g., -sV, -A)
if arguments:
cmd_parts.extend(shlex.split(arguments))
# Run the command using subprocess and capture output
try:
result = subprocess.run(
cmd_parts,
capture_output=True,
text=True,
timeout=timeout,
check=True
)
return {
"stdout": result.stdout,
"stderr": result.stderr
}
except subprocess.TimeoutExpired as e:
raise subprocess.TimeoutExpired(cmd=e.cmd, timeout=e.timeout) from e
except subprocess.CalledProcessError as e:
raise subprocess.CalledProcessError(
returncode=e.returncode,
cmd=e.cmd,
output=e.output,
stderr=e.stderr
) from e
```