#!/usr/bin/env node
// index.js - WireMCP Secure Server
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
const axios = require('axios');
const https = require('https');
// Import secure modules
const config = require('./config');
const audit = require('./audit');
const rateLimit = require('./rateLimit');
const tshark = require('./tshark');
const {
validators,
validatePcapPath,
sanitizePacketData,
sanitizeError
} = require('./validators');
// Redirect console.log to stderr (MCP requirement)
const originalConsoleLog = console.log;
console.log = (...args) => console.error(...args);
// Initialize MCP server
const server = new McpServer({
name: config.server.name,
version: config.server.version,
});
// Secure axios instance for threat intelligence
const secureAxios = axios.create({
timeout: config.threatIntel.sources.urlhaus.timeout,
maxContentLength: config.threatIntel.sources.urlhaus.maxSize,
maxBodyLength: config.threatIntel.sources.urlhaus.maxSize,
httpsAgent: new https.Agent({
rejectUnauthorized: true,
minVersion: 'TLSv1.2'
})
});
// Threat intelligence cache
const threatCache = {
data: null,
timestamp: null,
ttl: config.threatIntel.cacheTTL * 1000
};
/**
* Fetch threat intelligence data with caching
*/
async function getThreatData() {
const now = Date.now();
// Return cached data if still valid
if (threatCache.data && threatCache.timestamp &&
(now - threatCache.timestamp) < threatCache.ttl) {
console.error('[THREAT] Using cached threat data');
return threatCache.data;
}
// Fetch fresh data
console.error('[THREAT] Fetching fresh threat intelligence...');
try {
const response = await secureAxios.get(config.threatIntel.sources.urlhaus.url);
const ipRegex = /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g;
const ips = [...new Set(
(response.data.match(ipRegex) || [])
.filter(ip => {
const parts = ip.split('.');
return parts.every(part => parseInt(part) <= 255);
})
)];
// Update cache
threatCache.data = ips;
threatCache.timestamp = now;
console.error(`[THREAT] Loaded ${ips.length} threat IPs`);
return ips;
} catch (error) {
console.error(`[THREAT] Failed to fetch data: ${error.message}`);
// Return cached data even if expired, if available
if (threatCache.data) {
console.error('[THREAT] Using expired cache due to fetch failure');
return threatCache.data;
}
return [];
}
}
/**
* Helper to handle tool execution with common error handling
*/
async function executeTool(toolName, args, handler, clientId = 'unknown') {
const startTime = Date.now();
try {
// Check rate limit
rateLimit.checkRateLimit(clientId);
// Execute tool
const result = await handler(args, clientId);
// Log successful execution
const duration = Date.now() - startTime;
await audit.logToolExecution(toolName, args, clientId, duration);
return result;
} catch (error) {
// Log error
await audit.logToolError(toolName, args, error, clientId);
// Return sanitized error
return {
content: [{
type: 'text',
text: `Error: ${sanitizeError(error, config.server.environment === 'development')}`
}],
isError: true
};
}
}
// =============================================================================
// TOOL 1: Capture Packets
// =============================================================================
server.tool(
'capture_packets',
'Capture live network traffic and analyze packet data (requires elevated privileges)',
{
interface: validators.capturePackets.shape.interface,
duration: validators.capturePackets.shape.duration
},
async (args) => {
return executeTool('capture_packets', args, async (validatedArgs, clientId) => {
// Validate input
const { interface: iface, duration } = validators.capturePackets.parse(validatedArgs);
// Acquire capture slot
await rateLimit.acquireCaptureSlot(clientId);
let tempFile = null;
try {
// Capture packets
console.error(`[CAPTURE] Starting capture on ${iface} for ${duration}s`);
tempFile = await tshark.capture(iface, duration);
// Read and parse packets
const packets = await tshark.readPcap(tempFile, [
'frame.number',
'frame.time',
'ip.src',
'ip.dst',
'tcp.srcport',
'tcp.dstport',
'udp.srcport',
'udp.dstport',
'tcp.flags',
'http.request.method',
'http.response.code'
]);
// Sanitize sensitive data
const sanitized = sanitizePacketData(packets);
// Enforce size limit
let jsonString = JSON.stringify(sanitized);
if (jsonString.length > config.security.maxOutputSize) {
const ratio = config.security.maxOutputSize / jsonString.length;
const trimCount = Math.floor(sanitized.length * ratio);
jsonString = JSON.stringify(sanitized.slice(0, trimCount));
console.error(`[CAPTURE] Trimmed output from ${sanitized.length} to ${trimCount} packets`);
}
return {
content: [{
type: 'text',
text: `Captured ${packets.length} packets from ${iface} (${duration}s)\n\n${jsonString}`
}]
};
} finally {
// Always cleanup
if (tempFile) await tshark.cleanupFile(tempFile);
rateLimit.releaseCaptureSlot();
}
});
}
);
// =============================================================================
// TOOL 2: Get Summary Statistics
// =============================================================================
server.tool(
'get_summary_stats',
'Get protocol hierarchy statistics from live network traffic',
{
interface: validators.getSummaryStats.shape.interface,
duration: validators.getSummaryStats.shape.duration
},
async (args) => {
return executeTool('get_summary_stats', args, async (validatedArgs, clientId) => {
const { interface: iface, duration } = validators.getSummaryStats.parse(validatedArgs);
await rateLimit.acquireCaptureSlot(clientId);
let tempFile = null;
try {
console.error(`[STATS] Capturing on ${iface} for ${duration}s`);
tempFile = await tshark.capture(iface, duration);
const stats = await tshark.getStats(tempFile, 'phs');
return {
content: [{
type: 'text',
text: `Protocol Hierarchy Statistics (${iface}, ${duration}s):\n\n${stats}`
}]
};
} finally {
if (tempFile) await tshark.cleanupFile(tempFile);
rateLimit.releaseCaptureSlot();
}
});
}
);
// =============================================================================
// TOOL 3: Get Conversations
// =============================================================================
server.tool(
'get_conversations',
'Get TCP/UDP conversation statistics from live network traffic',
{
interface: validators.getConversations.shape.interface,
duration: validators.getConversations.shape.duration
},
async (args) => {
return executeTool('get_conversations', args, async (validatedArgs, clientId) => {
const { interface: iface, duration } = validators.getConversations.parse(validatedArgs);
await rateLimit.acquireCaptureSlot(clientId);
let tempFile = null;
try {
console.error(`[CONV] Capturing on ${iface} for ${duration}s`);
tempFile = await tshark.capture(iface, duration);
const tcpStats = await tshark.getStats(tempFile, 'conv,tcp');
return {
content: [{
type: 'text',
text: `TCP Conversation Statistics (${iface}, ${duration}s):\n\n${tcpStats}`
}]
};
} finally {
if (tempFile) await tshark.cleanupFile(tempFile);
rateLimit.releaseCaptureSlot();
}
});
}
);
// =============================================================================
// TOOL 4: Check Threats
// =============================================================================
server.tool(
'check_threats',
'Capture traffic and check IP addresses against URLhaus threat intelligence',
{
interface: validators.checkThreats.shape.interface,
duration: validators.checkThreats.shape.duration
},
async (args) => {
return executeTool('check_threats', args, async (validatedArgs, clientId) => {
if (!config.threatIntel.enabled) {
return {
content: [{
type: 'text',
text: 'Threat intelligence is disabled in configuration'
}]
};
}
const { interface: iface, duration } = validators.checkThreats.parse(validatedArgs);
await rateLimit.acquireCaptureSlot(clientId);
let tempFile = null;
try {
console.error(`[THREAT] Capturing on ${iface} for ${duration}s`);
tempFile = await tshark.capture(iface, duration);
// Extract IPs
const ipData = await tshark.extractFields(tempFile, ['ip.src', 'ip.dst']);
const ips = [...new Set(
ipData.split('\n')
.flatMap(line => line.split('\t'))
.filter(ip => ip && ip !== 'unknown' && /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ip))
)];
console.error(`[THREAT] Found ${ips.length} unique IPs`);
// Get threat data
const threatIPs = await getThreatData();
// Check for matches
const threats = ips.filter(ip => threatIPs.includes(ip));
if (threats.length > 0) {
await audit.logSecurityEvent(
'THREATS_DETECTED',
{ threats, totalIPs: ips.length },
clientId
);
}
const output = `Analyzed ${ips.length} unique IP addresses from ${iface}\n\n` +
`Captured IPs:\n${ips.slice(0, 50).join('\n')}${ips.length > 50 ? '\n... and more' : ''}\n\n` +
`URLhaus Threat Check:\n${
threats.length > 0
? `⚠️ THREATS DETECTED: ${threats.length} IP(s) found in URLhaus blacklist:\n${threats.join('\n')}`
: `✓ No threats detected (checked against ${threatIPs.length} known malicious IPs)`
}`;
return {
content: [{ type: 'text', text: output }]
};
} finally {
if (tempFile) await tshark.cleanupFile(tempFile);
rateLimit.releaseCaptureSlot();
}
});
}
);
// =============================================================================
// TOOL 5: Check IP Threats
// =============================================================================
server.tool(
'check_ip_threats',
'Check a specific IP address against URLhaus threat intelligence',
{
ip: validators.checkIpThreats.shape.ip
},
async (args) => {
return executeTool('check_ip_threats', args, async (validatedArgs, clientId) => {
if (!config.threatIntel.enabled) {
return {
content: [{
type: 'text',
text: 'Threat intelligence is disabled in configuration'
}]
};
}
const { ip } = validators.checkIpThreats.parse(validatedArgs);
console.error(`[THREAT] Checking IP: ${ip}`);
const threatIPs = await getThreatData();
const isThreat = threatIPs.includes(ip);
if (isThreat) {
await audit.logSecurityEvent(
'MALICIOUS_IP_CHECKED',
{ ip, source: 'URLhaus' },
clientId
);
}
const output = `IP Address: ${ip}\n\n` +
`URLhaus Threat Check:\n${
isThreat
? `⚠️ THREAT DETECTED: This IP is listed in the URLhaus blacklist`
: `✓ No threat detected (checked against ${threatIPs.length} known malicious IPs)`
}`;
return {
content: [{ type: 'text', text: output }]
};
});
}
);
// =============================================================================
// TOOL 6: Analyze PCAP
// =============================================================================
server.tool(
'analyze_pcap',
'Analyze an existing PCAP file and extract comprehensive packet data',
{
pcapPath: validators.analyzePcap.shape.pcapPath,
includeUrls: validators.analyzePcap.shape.includeUrls,
includeProtocols: validators.analyzePcap.shape.includeProtocols
},
async (args) => {
return executeTool('analyze_pcap', args, async (validatedArgs) => {
const validated = validators.analyzePcap.parse(validatedArgs);
const pcapPath = validatePcapPath(validated.pcapPath);
console.error(`[ANALYZE] Processing: ${pcapPath}`);
// Read packets
const packets = await tshark.readPcap(pcapPath, [
'frame.number',
'ip.src',
'ip.dst',
'tcp.srcport',
'tcp.dstport',
'udp.srcport',
'udp.dstport',
'http.host',
'http.request.uri',
'frame.protocols'
]);
// Extract unique IPs
const ips = [...new Set(packets.flatMap(p => [
p._source?.layers['ip.src']?.[0],
p._source?.layers['ip.dst']?.[0]
]).filter(ip => ip))];
// Extract URLs if requested
let urls = [];
if (validated.includeUrls) {
urls = packets
.filter(p => p._source?.layers['http.host'] && p._source?.layers['http.request.uri'])
.map(p => `http://${p._source.layers['http.host'][0]}${p._source.layers['http.request.uri'][0]}`)
.slice(0, 100); // Limit to 100 URLs
}
// Extract protocols if requested
let protocols = [];
if (validated.includeProtocols) {
protocols = [...new Set(
packets.map(p => p._source?.layers['frame.protocols']?.[0])
.filter(p => p)
)];
}
// Sanitize and limit packet data
const sanitized = sanitizePacketData(packets);
let jsonString = JSON.stringify(sanitized);
if (jsonString.length > config.security.maxOutputSize) {
const ratio = config.security.maxOutputSize / jsonString.length;
const trimCount = Math.floor(sanitized.length * ratio);
jsonString = JSON.stringify(sanitized.slice(0, trimCount));
console.error(`[ANALYZE] Trimmed from ${sanitized.length} to ${trimCount} packets`);
}
const output = `PCAP Analysis: ${path.basename(pcapPath)}\n\n` +
`Total Packets: ${packets.length}\n` +
`Unique IPs: ${ips.length}\n${ips.slice(0, 20).join('\n')}${ips.length > 20 ? '\n... and more' : ''}\n\n` +
(urls.length > 0 ? `URLs Found: ${urls.length}\n${urls.slice(0, 20).join('\n')}${urls.length > 20 ? '\n... and more' : ''}\n\n` : '') +
(protocols.length > 0 ? `Protocols: ${protocols.join(', ')}\n\n` : '') +
`Packet Data (JSON):\n${jsonString}`;
return {
content: [{ type: 'text', text: output }]
};
});
}
);
// =============================================================================
// TOOL 7: Extract Credentials (Restricted)
// =============================================================================
server.tool(
'extract_credentials',
'Extract potential credentials from PCAP file (RESTRICTED - requires explicit authorization)',
{
pcapPath: validators.extractCredentials.shape.pcapPath,
includeHashes: validators.extractCredentials.shape.includeHashes
},
async (args) => {
return executeTool('extract_credentials', args, async (validatedArgs, clientId) => {
// Check if feature is enabled
if (!config.security.enableCredentialExtraction) {
return {
content: [{
type: 'text',
text: '⚠️ CREDENTIAL EXTRACTION DISABLED\n\n' +
'This feature is disabled in the configuration for security reasons.\n' +
'To enable: Set ENABLE_CREDENTIAL_EXTRACTION=true in environment.'
}]
};
}
const validated = validators.extractCredentials.parse(validatedArgs);
const pcapPath = validatePcapPath(validated.pcapPath);
console.error(`[CREDENTIALS] Extracting from: ${pcapPath}`);
// Extract plaintext credentials
const plaintextData = await tshark.extractFields(pcapPath, [
'http.authbasic',
'ftp.request.command',
'ftp.request.arg',
'telnet.data',
'frame.number'
]);
const credentials = {
plaintext: [],
encrypted: []
};
// Parse plaintext credentials
const lines = plaintextData.split('\n').filter(line => line.trim());
lines.forEach(line => {
const [authBasic, ftpCmd, ftpArg, telnetData, frameNumber] = line.split('\t');
// HTTP Basic Auth
if (authBasic) {
try {
const [username, password] = Buffer.from(authBasic, 'base64').toString().split(':');
credentials.plaintext.push({
type: 'HTTP Basic Auth',
username: username || '',
password: password || '',
frame: frameNumber
});
} catch (e) {
// Invalid base64, skip
}
}
// FTP
if (ftpCmd === 'USER') {
credentials.plaintext.push({
type: 'FTP',
username: ftpArg || '',
password: '',
frame: frameNumber
});
}
if (ftpCmd === 'PASS') {
const lastUser = credentials.plaintext.findLast(c => c.type === 'FTP' && !c.password);
if (lastUser) lastUser.password = ftpArg || '';
}
});
// Extract Kerberos if includeHashes is true
if (validated.includeHashes) {
const kerberosData = await tshark.extractFields(pcapPath, [
'kerberos.CNameString',
'kerberos.realm',
'kerberos.cipher',
'frame.number'
]);
const kerbLines = kerberosData.split('\n').filter(line => line.trim());
kerbLines.forEach(line => {
const [cname, realm, cipher, frameNumber] = line.split('\t');
if (cipher && cname) {
credentials.encrypted.push({
type: 'Kerberos',
username: cname,
realm: realm || 'unknown',
hashPreview: cipher.substring(0, 32) + '...',
frame: frameNumber
});
}
});
}
// Log the extraction
await audit.logCredentialExtraction(
clientId,
pcapPath,
credentials.plaintext.length + credentials.encrypted.length
);
const output = `⚠️ CREDENTIAL EXTRACTION REPORT\n` +
`File: ${path.basename(pcapPath)}\n\n` +
`Plaintext Credentials: ${credentials.plaintext.length}\n` +
(credentials.plaintext.length > 0
? credentials.plaintext.map(c =>
` • ${c.type}: ${c.username}:${c.password} (Frame ${c.frame})`
).join('\n') + '\n'
: '') +
`\nEncrypted/Hashed Credentials: ${credentials.encrypted.length}\n` +
(credentials.encrypted.length > 0
? credentials.encrypted.map(c =>
` • ${c.type}: ${c.username}@${c.realm} (Frame ${c.frame})`
).join('\n') + '\n'
: '') +
`\n⚠️ WARNING: This operation has been logged for audit purposes.`;
return {
content: [{ type: 'text', text: output }]
};
});
}
);
// =============================================================================
// TOOL 8: Get Server Status (NEW)
// =============================================================================
server.tool(
'get_status',
'Get server status, rate limits, and configuration information',
{},
async (args) => {
return executeTool('get_status', args, async (_, clientId) => {
const status = rateLimit.getStatus(clientId);
const recentAudit = config.audit.enabled ? await audit.getRecentEntries(10) : [];
const output = `WireMCP Secure Server Status\n\n` +
`Version: ${config.server.version}\n` +
`Environment: ${config.server.environment}\n\n` +
`Rate Limiting:\n` +
` • Requests in window: ${status.requestsInWindow}/${status.maxRequests}\n` +
` • Remaining: ${status.remaining}\n` +
` • Window: ${status.windowMs/1000}s\n` +
` • Reset at: ${status.resetAt || 'N/A'}\n\n` +
`Resources:\n` +
` • Active captures: ${status.activeCaptureCount}/${status.maxConcurrentCaptures}\n` +
` • Max capture duration: ${config.security.maxCaptureDuration}s\n` +
` • Max output size: ${(config.security.maxOutputSize/1024/1024).toFixed(2)}MB\n\n` +
`Security:\n` +
` • Audit logging: ${config.audit.enabled ? 'enabled' : 'disabled'}\n` +
` • Threat intelligence: ${config.threatIntel.enabled ? 'enabled' : 'disabled'}\n` +
` • Credential extraction: ${config.security.enableCredentialExtraction ? 'enabled' : 'disabled'}\n` +
` • Data sanitization: ${config.security.sanitizeSensitiveData ? 'enabled' : 'disabled'}\n\n` +
(recentAudit.length > 0
? `Recent Activity (last ${recentAudit.length}):\n${recentAudit.map(e =>
` • ${new Date(e.timestamp).toLocaleString()}: ${e.action}`
).join('\n')}`
: 'Recent Activity: None');
return {
content: [{ type: 'text', text: output }]
};
});
}
);
// =============================================================================
// Add prompts for better LLM integration
// =============================================================================
server.prompt(
'network_analysis',
{
interface: validators.capturePackets.shape.interface.optional(),
duration: validators.capturePackets.shape.duration.optional()
},
({ interface: iface = config.network.defaultInterface, duration = 10 }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: `Please analyze network traffic on interface ${iface} for ${duration} seconds and provide insights about:
1. Types of traffic and protocols observed
2. Notable communication patterns
3. Key endpoints and services
4. Any security concerns or anomalies
5. Recommendations for further investigation`
}
}]
})
);
server.prompt(
'threat_hunt',
{
interface: validators.checkThreats.shape.interface.optional()
},
({ interface: iface = config.network.defaultInterface }) => ({
messages: [{
role: 'user',
content: {
type: 'text',
text: `Perform a threat hunting analysis on interface ${iface}:
1. Capture network traffic for analysis
2. Check all IP addresses against threat intelligence
3. Identify any suspicious patterns or anomalies
4. Assess the overall security posture
5. Provide actionable security recommendations`
}
}]
})
);
// =============================================================================
// Start Server
// =============================================================================
async function startServer() {
try {
console.error('='.repeat(60));
console.error(`WireMCP Secure Server v${config.server.version}`);
console.error('='.repeat(60));
// Initialize tshark
await tshark.init();
console.error('[INIT] tshark initialized successfully');
// Ensure temp directory exists
await tshark.ensureTempDir();
console.error('[INIT] Temp directory ready');
// Clean up old temp files
await tshark.cleanupOldFiles();
// Start MCP server
await server.connect(new StdioServerTransport());
console.error('[INIT] MCP server connected and ready');
console.error('='.repeat(60));
// Schedule periodic cleanup
setInterval(async () => {
await tshark.cleanupOldFiles();
}, 3600000); // Every hour
} catch (error) {
console.error(`[FATAL] Failed to start server: ${error.message}`);
await audit.log('SYSTEM', 'startup_failed', { error: error.message }, 'system', 'error');
process.exit(1);
}
}
// Handle graceful shutdown
process.on('SIGINT', async () => {
console.error('\n[SHUTDOWN] Received SIGINT, shutting down gracefully...');
await audit.log('SYSTEM', 'shutdown', { reason: 'SIGINT' });
await audit.flush();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.error('\n[SHUTDOWN] Received SIGTERM, shutting down gracefully...');
await audit.log('SYSTEM', 'shutdown', { reason: 'SIGTERM' });
await audit.flush();
process.exit(0);
});
// Start the server
startServer();