# Directory Structure
```
├── external-recon.py
└── README.md
```
# Files
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# mcp-vscode-template
MCP server template for VS Code Agent
### Setup
Install uv however you like. May options available.
https://docs.astral.sh/uv/getting-started/installation/
Project setup is heavily based off of Renae Schilg's work. I didn't even bother to change the project name as an homage although I did deviate on a few things, namely not using Claude Desktop, but also modified `external-recon.py` fairly heavily.
https://nae-bo.medium.com/building-your-first-offensive-security-mcp-server-dd655e258d5f
```bash
# Initialize project
uv init external-recon
cd external-recon
```
I had to modify the python versions in `.python-version` to `3.11` or something above 3.8 or 3.10
I also had to modify the line `requires-python = ">=3.11"` in `pyroject.toml` to something above 3.8 or 3.10
Mileage will vary... It may not be necessary.
```bash
# Create virtual environment and activate it
uv venv --python 3.11
source .venv/bin/activate
# Install mcp
uv add "mcp[cli]"
# Install dnspython (dependency for external-recon.py, not necessary for all projects, but for this one yes)
uv pip install dnspython
# Create MCP server external-recon.py file or empty and rename main.py
touch external-recon.py
```
The VS Code `settings.json` should be modified.
Use `which uv` to find the path to uv.
The `"/path/to/project/external-recon"` should refer to the project path, where the MCP server .py file is located (use absoulte path).
settings.json
```json
"mcp": {
"servers": {
"external-recon": {
"command": "/path/to/uv",
"args": [
"--directory",
"/path/to/project/external-recon",
"run",
"external-recon.py"
]
}
}
},
```
One of the main differences between Renae's work and mine is I used `@mcp.tool()` instead of `@mcp.prompt()` in `external-recon.py`
### Start the server
From the venv of the project, start the server with `uv run external-recon.py`
Example:
```bash
(external-recon) user@workstation external-recon % uv run external-recon.py
```
```
--------------------------------------------------------------------------------
/external-recon.py:
--------------------------------------------------------------------------------
```python
from mcp.server.fastmcp import FastMCP
import subprocess
# import httpx
# import os
# import json
import socket
import dns.resolver
import re
from typing import Dict, Any
# from typing import List, Dict, Any, Optional
# Initialise FastMCP server
mcp = FastMCP("external-recon")
## Prompt to initialise the AI model to the task
@mcp.tool()
def setup_prompt(domainname: str) -> str:
"""
setup external reconnaissance by domain name
:param domainname: domain name to target
:type domainname: str
:return:
:rtype: str
"""
return f"""
Your role is a highly skilled penetration tester specialising in network reconnaissance. Your primary objective is to enumerate the {domainname} domain and report on discovered IP addresses, subdomains, and email security.
Observer carefully the output of the tools in inform next steps
Your objective is to perform reconnaissance against the organisation's domain name, identify IP addresses, discover subdomains, report on the ownership of the domains, and assess the email security measures. When you find new IP addresses or subdomains I want you to repeat enumeration steps.
First, reflect on the objective, then execute any tools you have access to on the target domain {domainname} and report your findings on all IP addresses and subdomains discovered.
"""
@mcp.tool()
def dns_lookup(domain: str) -> Dict[str, Any]:
"""
Perform DNS lookups for A, MX, NS, TXT, and SOA records
:param domain: The domain to look up
:type domain: str
:return: Dictionary containing different DNS records
:rtype: Dict[str, Any]
"""
results = {}
try:
# A records (IPv4 addresses)
try:
a_records = dns.resolver.resolve(domain, 'A')
results['a_records'] = [record.to_text() for record in a_records]
except Exception as e:
results['a_records'] = f"Error: {str(e)}"
# MX records (Mail servers)
try:
mx_records = dns.resolver.resolve(domain, 'MX')
results['mx_records'] = [record.to_text() for record in mx_records]
except Exception as e:
results['mx_records'] = f"Error: {str(e)}"
# NS records (Name servers)
try:
ns_records = dns.resolver.resolve(domain, 'NS')
results['ns_records'] = [record.to_text() for record in ns_records]
except Exception as e:
results['ns_records'] = f"Error: {str(e)}"
# TXT records (Text records, includes SPF usually)
try:
txt_records = dns.resolver.resolve(domain, 'TXT')
results['txt_records'] = [record.to_text() for record in txt_records]
except Exception as e:
results['txt_records'] = f"Error: {str(e)}"
# SOA records (Start of Authority)
try:
soa_records = dns.resolver.resolve(domain, 'SOA')
results['soa_records'] = [record.to_text() for record in soa_records]
except Exception as e:
results['soa_records'] = f"Error: {str(e)}"
except Exception as e:
results['error'] = f"General error: {str(e)}"
return results
@mcp.tool()
def check_email_security(domain: str) -> Dict[str, Any]:
"""
Check for email security measures like SPF, DMARC and DKIM
:param domain: The domain to check
:type domain: str
:return: Dictionary containing email security findings
:rtype: Dict[str, Any]
"""
results = {"spf": None, "dmarc": None, "dkim_configured": False}
# Check SPF (Sender Policy Framework)
try:
txt_records = dns.resolver.resolve(domain, 'TXT')
for record in txt_records:
if "v=spf1" in record.to_text():
results["spf"] = record.to_text()
break
except Exception as e:
results["spf_error"] = str(e)
# Check DMARC (Domain-based Message Authentication, Reporting & Conformance)
try:
dmarc_records = dns.resolver.resolve(f"_dmarc.{domain}", 'TXT')
for record in dmarc_records:
if "v=DMARC1" in record.to_text():
results["dmarc"] = record.to_text()
break
except Exception as e:
results["dmarc_error"] = str(e)
# Check DKIM (DomainKeys Identified Mail)
# We can't check directly as DKIM selectors are custom
# But we can try common selectors
common_selectors = ["default", "mail", "email", "selector1", "selector2", "k1", "dkim"]
for selector in common_selectors:
try:
dkim_records = dns.resolver.resolve(f"{selector}._domainkey.{domain}", 'TXT')
results["dkim_configured"] = True
results["dkim_selector"] = selector
results["dkim"] = dkim_records[0].to_text()
break
except:
pass
return results
@mcp.tool()
def scan_ports(target: str, ports: str = "21,22,23,25,53,80,443,8080,8443") -> Dict[str, Any]:
"""
Scan for open ports on a target IP or domain
:param target: IP or domain to scan
:type target: str
:param ports: Comma-separated list of ports to scan (defaults to common ports)
:type ports: str
:return: Dictionary containing open ports and their services
:rtype: Dict[str, Any]
"""
results = {"open_ports": []}
# First resolve the target if it's a domain
try:
if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", target):
ip = socket.gethostbyname(target)
results["resolved_ip"] = ip
else:
ip = target
except Exception as e:
results["error"] = f"Could not resolve hostname: {str(e)}"
return results
port_list = [int(p) for p in ports.split(",")]
for port in port_list:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
result = sock.connect_ex((ip, port))
if result == 0:
# Try to identify the service
service = "unknown"
try:
# Common port to service mapping
services = {
21: "FTP", 22: "SSH", 23: "Telnet", 25: "SMTP",
53: "DNS", 80: "HTTP", 443: "HTTPS", 3389: "RDP",
8080: "HTTP-Alt", 8443: "HTTPS-Alt"
}
if port in services:
service = services[port]
except:
pass
results["open_ports"].append({"port": port, "service": service})
sock.close()
except:
pass
return results
@mcp.tool()
def whois_lookup(domain: str) -> Dict[str, str]:
"""
Perform a WHOIS lookup on a domain
:param domain: The domain to look up
:type domain: str
:return: Dictionary containing WHOIS information
:rtype: Dict[str, str]
"""
results = {}
try:
# Use the whois command line tool
command = ["whois", domain]
result = subprocess.run(command, capture_output=True, text=True, timeout=30)
# Extract useful information from the output
output = result.stdout
# Extract registrar information
registrar_match = re.search(r"Registrar:\s*(.*)", output)
if registrar_match:
results["registrar"] = registrar_match.group(1).strip()
# Extract creation date
creation_date_match = re.search(r"Creation Date:\s*(.*)", output)
if creation_date_match:
results["creation_date"] = creation_date_match.group(1).strip()
# Extract expiration date
expiration_date_match = re.search(r"Registry Expiry Date:\s*(.*)", output)
if expiration_date_match:
results["expiration_date"] = expiration_date_match.group(1).strip()
# Include the full output
results["raw_data"] = output
except Exception as e:
results["error"] = f"WHOIS lookup failed: {str(e)}"
return results
if __name__ == "__main__":
# Initialise and run the server
mcp.run(transport='stdio')
```