@zibdie/port-scanner-mcp
Version:
MCP server for network port scanning using nmap
319 lines • 12.7 kB
JavaScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
import { spawn } from 'child_process';
class PortScannerMCPServer {
server;
defaultPorts = [22, 23, 80, 443, 8080, 3000];
constructor() {
this.server = new Server({
name: 'port-scanner-mcp-server',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
this.setupToolHandlers();
}
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'scan_ports',
description: 'Scan specified ports on target hosts using nmap',
inputSchema: {
type: 'object',
properties: {
targets: {
type: 'array',
items: { type: 'string' },
description: 'IP addresses or hostnames to scan',
},
ports: {
type: 'array',
items: { type: 'number' },
description: 'Ports to scan (defaults to 22,23,80,443,8080,3000)',
},
scan_type: {
type: 'string',
enum: ['tcp', 'udp', 'both'],
default: 'tcp',
description: 'Type of port scan to perform',
},
service_detection: {
type: 'boolean',
default: true,
description: 'Enable service version detection',
},
os_detection: {
type: 'boolean',
default: false,
description: 'Enable OS detection (requires root privileges)',
},
},
required: ['targets'],
},
},
{
name: 'network_discovery',
description: 'Discover live hosts on a network segment',
inputSchema: {
type: 'object',
properties: {
network: {
type: 'string',
description: 'Network range in CIDR notation (e.g., 192.168.1.0/24)',
},
ping_scan: {
type: 'boolean',
default: true,
description: 'Use ping scan for host discovery',
},
},
required: ['network'],
},
},
{
name: 'service_scan',
description: 'Perform detailed service enumeration on specific ports',
inputSchema: {
type: 'object',
properties: {
target: {
type: 'string',
description: 'IP address or hostname to scan',
},
port: {
type: 'number',
description: 'Port number to examine in detail',
},
},
required: ['target', 'port'],
},
},
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
switch (request.params.name) {
case 'scan_ports':
return await this.handlePortScan(request.params.arguments);
case 'network_discovery':
return await this.handleNetworkDiscovery(request.params.arguments);
case 'service_scan':
return await this.handleServiceScan(request.params.arguments);
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}
}
catch (error) {
return {
content: [
{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
});
}
async executeNmap(args) {
return new Promise((resolve, reject) => {
const nmap = spawn('nmap', args);
let stdout = '';
let stderr = '';
nmap.stdout.on('data', (data) => {
stdout += data.toString();
});
nmap.stderr.on('data', (data) => {
stderr += data.toString();
});
nmap.on('close', (code) => {
if (code === 0) {
resolve(stdout);
}
else {
let errorMessage = `nmap exited with code ${code}: ${stderr}`;
if (stderr.includes('requires root privileges')) {
errorMessage += '\n\nSuggestion: This scan type requires elevated privileges. Try using TCP connect scan (-sT) or run as root if authorized.';
}
reject(new Error(errorMessage));
}
});
nmap.on('error', (error) => {
reject(new Error(`Failed to execute nmap: ${error.message}`));
});
});
}
parseNmapOutput(output) {
const results = [];
const lines = output.split('\n');
let currentHost = null;
for (const line of lines) {
const trimmedLine = line.trim();
// Host detection
const hostMatch = trimmedLine.match(/^Nmap scan report for (.+)$/);
if (hostMatch) {
if (currentHost) {
results.push(currentHost);
}
currentHost = {
host: hostMatch[1],
ports: [],
hostInfo: {},
};
continue;
}
// Port information
const portMatch = trimmedLine.match(/^(\d+)\/(tcp|udp)\s+(\w+)\s*(.*)$/);
if (portMatch && currentHost) {
const [, portNum, protocol, state, serviceInfo] = portMatch;
const portInfo = {
port: parseInt(portNum),
protocol: protocol,
state: state,
};
if (serviceInfo) {
const serviceParts = serviceInfo.split(/\s+/);
if (serviceParts[0]) {
portInfo.service = serviceParts[0];
}
if (serviceParts.length > 1) {
portInfo.version = serviceParts.slice(1).join(' ');
}
}
currentHost.ports.push(portInfo);
continue;
}
// MAC address and vendor
const macMatch = trimmedLine.match(/^MAC Address: ([A-Fa-f0-9:]{17})\s*\((.+)\)$/);
if (macMatch && currentHost) {
currentHost.hostInfo.macAddress = macMatch[1];
currentHost.hostInfo.vendor = macMatch[2];
continue;
}
// OS detection
const osMatch = trimmedLine.match(/^Running: (.+)$/);
if (osMatch && currentHost) {
currentHost.hostInfo.osGuess = osMatch[1];
continue;
}
}
if (currentHost) {
results.push(currentHost);
}
return results;
}
async handlePortScan(args) {
const targets = args.targets;
const ports = args.ports || this.defaultPorts;
const scanType = args.scan_type || 'tcp';
const serviceDetection = args.service_detection !== false;
const osDetection = args.os_detection || false;
const nmapArgs = ['-oN', '-']; // Output to stdout
// Port specification
nmapArgs.push('-p', ports.join(','));
// Scan type
if (scanType === 'tcp' || scanType === 'both') {
nmapArgs.push('-sT'); // TCP connect scan (doesn't require root)
}
if (scanType === 'udp' || scanType === 'both') {
// UDP scan requires root, skip or warn user
console.warn('UDP scan requires root privileges, skipping UDP scan');
}
// Service detection
if (serviceDetection) {
nmapArgs.push('-sV');
}
// OS detection requires root, skip if not available
if (osDetection) {
console.warn('OS detection requires root privileges, skipping OS detection');
}
// Add targets
nmapArgs.push(...targets);
try {
const output = await this.executeNmap(nmapArgs);
const results = this.parseNmapOutput(output);
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2),
},
],
};
}
catch (error) {
throw new Error(`Port scan failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async handleNetworkDiscovery(args) {
const network = args.network;
const pingScan = args.ping_scan !== false;
const nmapArgs = ['-oN', '-'];
if (pingScan) {
nmapArgs.push('-sn'); // Ping scan only
}
else {
nmapArgs.push('-Pn'); // Skip ping
}
nmapArgs.push(network);
try {
const output = await this.executeNmap(nmapArgs);
const results = this.parseNmapOutput(output);
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2),
},
],
};
}
catch (error) {
throw new Error(`Network discovery failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async handleServiceScan(args) {
const target = args.target;
const port = args.port;
const nmapArgs = [
'-oN', '-',
'-sT', // TCP connect scan (no root required)
'-sV', // Service version detection
'-sC', // Default scripts
'-p', port.toString(),
target
];
try {
const output = await this.executeNmap(nmapArgs);
const results = this.parseNmapOutput(output);
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2),
},
],
};
}
catch (error) {
throw new Error(`Service scan failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
}
const server = new PortScannerMCPServer();
server.run().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});
//# sourceMappingURL=index.js.map