/**
 * Minified by jsDelivr using Terser v5.39.0.
 * Original file: /npm/pentest-mcp@0.9.0/dist/index.js
 *
 * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
 */
#!/usr/bin/env node
import{McpServer,ResourceTemplate}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport}from"@modelcontextprotocol/sdk/server/stdio.js";import{SSEServerTransport}from"@modelcontextprotocol/sdk/server/sse.js";import{StreamableHTTPServerTransport}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{requireBearerAuth}from"@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";import{getOAuthProtectedResourceMetadataUrl,mcpAuthMetadataRouter}from"@modelcontextprotocol/sdk/server/auth/router.js";import{ElicitResultSchema}from"@modelcontextprotocol/sdk/types.js";import express from"express";import{createRemoteJWKSet,jwtVerify}from"jose";import{z}from"zod";import{UserMode}from"./types.js";import{logScanResult,logMessage}from"./logger.js";import{spawn}from"child_process";import{createRequire}from"module";import fs from"fs/promises";import{URL}from"url";import path from"path";import{XMLParser}from"fast-xml-parser";const require=createRequire(import.meta.url);function parseCsvEnv(e,t=[]){return e?e.split(/[,\s]+/).map((e=>e.trim())).filter(Boolean):t}const transportTypeRaw=(process.argv[2]||process.env.MCP_TRANSPORT||"stdio").toLowerCase(),transportType="streamable"===transportTypeRaw?"http":transportTypeRaw,serverHost=process.env.MCP_SERVER_HOST||"0.0.0.0",serverPort=parseInt(process.env.MCP_SERVER_PORT||"8000"),serverVersion=process.env.npm_package_version||"0.8.0",authEnabled="true"===(process.env.MCP_AUTH_ENABLED??process.env.MCP_OAUTH_ENABLED??"false"),authMode=process.env.MCP_AUTH_MODE||"bearer",authIssuer=process.env.MCP_OIDC_ISSUER??process.env.MCP_OAUTH_PROVIDER_URL,authJwksUrl=process.env.MCP_OIDC_JWKS_URL,authIntrospectionUrl=process.env.MCP_OIDC_INTROSPECTION_URL,authAudience=process.env.MCP_AUTH_AUDIENCE,authScopes=parseCsvEnv(process.env.MCP_AUTH_SCOPES??process.env.MCP_OAUTH_SCOPES,[]),advertisedScopes=authScopes.length>0?authScopes:["read","write"],oauthClientId=process.env.MCP_OAUTH_CLIENT_ID,oauthClientSecret=process.env.MCP_OAUTH_CLIENT_SECRET;let currentUserSession={mode:UserMode.UNKNOWN,history:[]};const activeScans=new Map;let server;const engagementRecords=new Map;function saveEngagementRecord(e){const t=`rec-${Date.now()}-${Math.random().toString(36).slice(2,8)}`;return engagementRecords.set(t,{...e,recordId:t,createdAt:Date.now()}),t}async function verifyViaIntrospection(e){if(!authIntrospectionUrl)throw new Error("MCP_OIDC_INTROSPECTION_URL is required for introspection mode.");const t=new Headers({"Content-Type":"application/x-www-form-urlencoded"});if(oauthClientId&&oauthClientSecret){const e=Buffer.from(`${oauthClientId}:${oauthClientSecret}`).toString("base64");t.set("Authorization",`Basic ${e}`)}const r=new URLSearchParams({token:e});authAudience&&r.set("resource",authAudience);const o=await fetch(authIntrospectionUrl,{method:"POST",headers:t,body:r.toString()});if(!o.ok){const e=await o.text().catch((()=>""));throw new Error(`Introspection failed (${o.status}): ${e||"no response body"}`)}const n=await o.json();if(!n.active)throw new Error("Token is not active.");const s=parseCsvEnv(n.scope,[]),a=n.aud,i=Array.isArray(a)?a:a?[a]:[];if(authAudience&&i.length>0&&!i.includes(authAudience))throw new Error(`Token audience mismatch. Expected ${authAudience}, got ${i.join(",")}`);return{token:e,clientId:n.client_id||n.sub||"unknown-client",scopes:s,expiresAt:"number"==typeof n.exp?n.exp:void 0,extra:n}}const remoteJwks=authJwksUrl?createRemoteJWKSet(new URL(authJwksUrl)):null;async function verifyViaJwks(e){if(!remoteJwks)throw new Error("MCP_OIDC_JWKS_URL is required for JWKS mode.");const t={};authIssuer&&(t.issuer=authIssuer),authAudience&&(t.audience=authAudience);const{payload:r}=await jwtVerify(e,remoteJwks,t),o=parseCsvEnv("string"==typeof r.scope?r.scope:Array.isArray(r.scp)?r.scp.join(" "):"",[]);return{token:e,clientId:"string"==typeof r.client_id&&r.client_id||"string"==typeof r.azp&&r.azp||"string"==typeof r.sub&&r.sub||"unknown-client",scopes:o,expiresAt:"number"==typeof r.exp?r.exp:void 0,extra:r}}const tokenVerifier={verifyAccessToken:async e=>authIntrospectionUrl?verifyViaIntrospection(e):verifyViaJwks(e)};function buildOauthMetadata(){const e=authIssuer||`http://${serverHost}:${serverPort}`;return{issuer:e,authorization_endpoint:`${e.replace(/\/$/,"")}/authorize`,token_endpoint:`${e.replace(/\/$/,"")}/token`,response_types_supported:["code"],grant_types_supported:["authorization_code","client_credentials","refresh_token"],scopes_supported:advertisedScopes,token_endpoint_auth_methods_supported:["client_secret_basic","client_secret_post"],introspection_endpoint:authIntrospectionUrl}}function formatInvocation(e){if(!e)return"invocation=unknown";const t=e.authInfo?.clientId||"anonymous",r=e.authInfo?.scopes?.length?e.authInfo.scopes.join(","):"none";return`invocation.clientId=${t}; invocation.session=${e.sessionId||"stateless"}; invocation.scopes=${r}; invocation.requestId=${String(e.requestId??"unknown")}`}async function resolveInspectorBinPath(){const e=require.resolve("@modelcontextprotocol/inspector/package.json"),t=await fs.readFile(e,"utf8"),r=JSON.parse(t);let o;if("string"==typeof r.bin?o=r.bin:r.bin&&(o=r.bin["mcp-inspector"]||Object.values(r.bin)[0]),!o)throw new Error("Unable to resolve bundled MCP Inspector binary path.");const n=path.dirname(e);return{binPath:path.resolve(n,o),packageDir:n}}async function launchBundledInspector(e){const t=e.indexOf("--"),r=-1===t?e:e.slice(0,t),o=-1===t?[]:e.slice(t+1),{binPath:n,packageDir:s}=await resolveInspectorBinPath(),a=[process.argv[1],...o.length>0?o:["stdio"]],i=r.indexOf("--cli"),c=-1===i?[...r,process.execPath,...a]:[...r.slice(0,i+1),process.execPath,...a,...r.slice(i+1)];return console.error("Launching bundled MCP Inspector..."),console.error(`Inspector command: node ${n} ${c.join(" ")}`.trim()),console.error(`Target MCP server command: ${process.execPath} ${a.join(" ")}`),await new Promise(((e,t)=>{const r=spawn(process.execPath,[n,...c],{stdio:"inherit",cwd:s,env:{...process.env}});r.on("error",t),r.on("exit",(t=>e(t??1)))}))}function getScanDataById(e){const t=activeScans.get(e);return t?{scanId:e,target:t.progress.target,options:[],timestamp:t.progress.startTime,results:{}}:null}function analyzeFindings(e){const t={critical:[],high:[],medium:[],low:[],info:[]};for(const r of e)if(r&&r.results)for(const e in r.results){const o=r.results[e];if(o.ports)for(const r of o.ports)if("open"===r.state){const o={host:e,port:r.portId,service:r.service?.name||"unknown",description:`Open port ${r.portId} (${r.service?.name||"unknown"})`,details:r};["3389","5432","1433","21","23"].includes(r.portId)?t.critical.push(o):["22","445","139"].includes(r.portId)?t.high.push(o):["80","443","8080"].includes(r.portId)?t.medium.push(o):t.low.push(o)}}return t}function toArray(e){return null==e?[]:Array.isArray(e)?e:[e]}function normalizeString(e,t=""){return"string"==typeof e?e:"number"==typeof e||"boolean"==typeof e?String(e):t}function normalizeNullableString(e){return normalizeString(e)||null}function normalizeOptionalString(e){return normalizeString(e)||void 0}function parseNmapXmlOutput(e){const t=new XMLParser({ignoreAttributes:!1,attributeNamePrefix:"@",allowBooleanAttributes:!0,parseAttributeValue:!0,isArray:e=>["host","port","hostname","osmatch","cpe","address"].includes(e)}).parse(e),r=toArray(t?.nmaprun?.host),o={};for(let e=0;e<r.length;e++){const t=r[e]||{},n=toArray(t.address),s=n.find((e=>"ipv4"===normalizeString(e?.["@addrtype"]))),a=n.find((e=>"ipv6"===normalizeString(e?.["@addrtype"]))),i=n.find((e=>"mac"===normalizeString(e?.["@addrtype"]))),c=s||a||n[0],p=normalizeString(c?.["@addr"],`host-${e+1}`),d=toArray(t?.hostnames?.hostname).find((e=>normalizeString(e?.["@name"]))),u=normalizeNullableString(d?.["@name"]),l=toArray(t?.os?.osmatch),h=normalizeNullableString(l[0]?.["@name"]),m=toArray(t?.ports?.port).map((e=>{const t=e?.state||{},r=e?.service||{},o=r?.cpe,n=toArray(o).map((e=>normalizeString(e))).filter(Boolean);return{portId:normalizeString(e?.["@portid"]),protocol:normalizeString(e?.["@protocol"],"tcp"),state:normalizeString(t?.["@state"],"unknown"),reason:normalizeString(t?.["@reason"],"unknown"),service:{name:normalizeString(r?.["@name"],"unknown"),product:normalizeOptionalString(r?.["@product"]),version:normalizeOptionalString(r?.["@version"]),extrainfo:normalizeOptionalString(r?.["@extrainfo"]),method:normalizeOptionalString(r?.["@method"]),conf:normalizeOptionalString(r?.["@conf"]),cpe:n.length>0?n.join(", "):void 0}}}));o[p]={hostname:u,ip:p,mac:normalizeNullableString(i?.["@addr"]),ports:m,osNmap:h}}return o}async function runNmapScan(e,t=[]){const r=`scan-${Date.now()}-${Math.random().toString(36).substr(2,9)}`,o={scanId:r,target:e,startTime:Date.now(),progress:0,status:"initializing"};if(console.error(`Executing nmap scan: target=${e}, options=${t.join(" ")}`),!e)throw new Error("Error: Target is required.");return new Promise(((n,s)=>{const a=[...t,"-oX","-",e],i=spawn("nmap",a);activeScans.set(r,{process:i,progress:o});let c="",p="";i.stdout.on("data",(e=>{c+=e.toString()})),i.stderr.on("data",(e=>{p+=e.toString();const t=e.toString();t.includes("Initiating")?(o.status="scanning",o.progress=10):t.includes("Completed SYN Stealth Scan")?o.progress=40:t.includes("Initiating Service scan")?(o.currentStage="Service detection",o.progress=50):t.includes("Completed Service scan")?o.progress=70:t.includes("Initiating OS detection")&&(o.currentStage="OS detection",o.progress=80)})),i.on("error",(e=>{activeScans.delete(r),o.status="failed",s(e)}));let d=null;i.on("close",(async o=>{activeScans.delete(r),console.error(`Nmap process exited with code ${o}`),p&&console.warn(`Nmap stderr: ${p}`);let a=null;if(0===o&&c)try{d=parseNmapXmlOutput(c),a=d}catch(e){return console.error("Failed to parse Nmap XML output:",e),a=`XML Parse Error: ${e.message}\n${c}`,d=null,void s(new Error(`Failed to parse Nmap XML output: ${e.message}`))}else 0!==o&&(a=p||`Nmap failed: ${o}`);if(currentUserSession.mode===UserMode.PROFESSIONAL&&await logScanResult(e,t,a),0!==o){s(new Error(`Nmap scan failed with exit code ${o}.${p?" Stderr: "+p:""}`))}else n(d)}))}))}const SAFE_OPTION_REGEX=/^(?:-[a-zA-Z0-9]+|--[a-zA-Z0-9\-]+(?:=[^;&|`$\s\(\)\<\>\\]+)?|[^;&|`$\s\(\)\<\>\\]+)$/,COMMAND_TIMEOUT_MS=Number(process.env.MCP_COMMAND_TIMEOUT_MS||9e5),COMMAND_OUTPUT_LIMIT_BYTES=Number(process.env.MCP_COMMAND_OUTPUT_LIMIT_BYTES||2097152),commandAvailability=new Map,commandResolutionCache=new Map,COMMAND_CANDIDATES={httpx:["httpx-toolkit","httpx"]};function looksLikeProjectDiscoveryHttpx(e){const t=`${e.stdout}\n${e.stderr}`.toLowerCase();return!t.includes("next generation http client")&&(t.includes("projectdiscovery")||t.includes("-status-code")||t.includes("-tech-detect")||t.includes("-silent")||t.includes("httpx version"))}const COMMAND_PROBES={httpx:[{args:["-version"],timeoutMs:5e3,validate:e=>0===e.code&&looksLikeProjectDiscoveryHttpx(e)},{args:["-h"],timeoutMs:5e3,validate:e=>0===e.code&&looksLikeProjectDiscoveryHttpx(e)}]};async function runCommandProbe(e,t,r=5e3){return new Promise((o=>{const n=spawn(e,t);let s="",a="",i=!1;const c=setTimeout((()=>{i=!0,n.kill("SIGTERM"),setTimeout((()=>n.kill("SIGKILL")),1e3)}),r);n.stdout.on("data",(e=>{s=cappedAppend(s,e.toString())})),n.stderr.on("data",(e=>{a=cappedAppend(a,e.toString())})),n.on("error",(e=>{clearTimeout(c),o({stdout:s,stderr:`${a}\n${e.message}`,code:null})})),n.on("close",(e=>{clearTimeout(c),o(i?{stdout:s,stderr:`${a}\nProbe command timed out after ${r}ms`,code:null}:{stdout:s,stderr:a,code:e})}))}))}async function validateCommandCandidate(e,t){const r=COMMAND_PROBES[e];if(!r||0===r.length)return!0;for(const e of r){const r=await runCommandProbe(t,e.args,e.timeoutMs);if(e.validate(r))return!0}return!1}function sanitizeOptions(e){const t=[];for(const r of e){if(!SAFE_OPTION_REGEX.test(r))throw new Error(`Invalid or potentially unsafe option detected: "${r}". Only standard flags and simple arguments are allowed.`);t.push(r)}return t}async function checkCommandAvailable(e){if(commandAvailability.has(e))return commandAvailability.get(e);const t=await resolveCommand(e),r=Boolean(t);return commandAvailability.set(e,r),r}async function resolveCommand(e){if(commandResolutionCache.has(e))return commandResolutionCache.get(e);const t=COMMAND_CANDIDATES[e]??[e];for(const r of t){if(await new Promise((e=>{const t=spawn("which",[r]);t.on("close",(t=>e(0===t))),t.on("error",(()=>e(!1)))}))){if(await validateCommandCandidate(e,r))return commandResolutionCache.set(e,r),r;console.warn(`Command "${r}" is present but did not pass validation for "${e}".`)}}return commandResolutionCache.set(e,null),null}function cappedAppend(e,t){if(e.length>=COMMAND_OUTPUT_LIMIT_BYTES)return e;const r=COMMAND_OUTPUT_LIMIT_BYTES-e.length;return e+t.slice(0,Math.max(r,0))}async function runSpawnCommand(e,t){const r=await resolveCommand(e);if(!r){throw new Error(`Required command "${e}" is not available in PATH. Checked: ${(COMMAND_CANDIDATES[e]??[e]).join(", ")}`)}return new Promise(((e,o)=>{console.error(`Attempting to spawn: ${r} ${t.join(" ")}`);const n=spawn(r,t);let s="",a="",i=!1;const c=setTimeout((()=>{i=!0,n.kill("SIGTERM"),setTimeout((()=>n.kill("SIGKILL")),2e3)}),COMMAND_TIMEOUT_MS);n.stdout.on("data",(e=>{s=cappedAppend(s,e.toString())})),n.stderr.on("data",(e=>{a=cappedAppend(a,e.toString())})),n.on("error",(e=>{console.error(`Spawn error for command "${r}": ${e.message}`),clearTimeout(c),o(new Error(`Failed to start command "${r}": ${e.message}`))})),n.on("close",(t=>{clearTimeout(c),i?e({stdout:s,stderr:`${a}\nCommand timed out after ${COMMAND_TIMEOUT_MS}ms.`,code:null}):(console.error(`Command "${r}" exited with code ${t}`),e({stdout:s,stderr:a,code:t}))}))}))}async function runJtR(e,t=[]){let r;try{r=sanitizeOptions(t)}catch(e){throw e}if(!e)throw new Error("Error: Hash data is required.");const o=path.join("/tmp",`jtr_hashes_${Date.now()}.txt`);let n="",s=[];try{await fs.writeFile(o,e);const t=[...r,o];n+=`--- Cracking Attempt ---\nExecuting: john ${t.join(" ")}\n`;try{const e=await runSpawnCommand("john",t);n+=`Exit Code: ${e.code}\nStdout:\n${e.stdout}\nStderr:\n${e.stderr}\n`}catch(e){n+=`Cracking command failed to execute: ${e.message}\n`}const a=["--show",o];n+=`--- Show Attempt ---\nExecuting: john ${a.join(" ")}\n`;try{const e=await runSpawnCommand("john",a);n+=`Exit Code: ${e.code}\nStdout:\n${e.stdout}\nStderr:\n${e.stderr}\n`,s=e.stdout.split("\n").map((e=>e.trim())).filter((e=>e&&!e.startsWith("0 passwords cracked")&&!e.includes("guesses remaining")))}catch(e){n+=`Show command failed to execute: ${e.message}\n`}return await fs.unlink(o),currentUserSession.mode===UserMode.PROFESSIONAL&&await logMessage(`Ran John the Ripper.\nOptions: ${r.join(" ")}\nCracked: ${s.length}.`),{fullOutput:n,cracked:s}}catch(e){console.error("Fatal error setting up John the Ripper execution:",e);try{await fs.unlink(o)}catch{}throw currentUserSession.mode===UserMode.PROFESSIONAL&&await logMessage(`John the Ripper FAILED fatally before execution.\nOptions: ${r.join(" ")}\nError: ${e.message}`),new Error(`John the Ripper setup failed fatally: ${e.message}`)}}async function runHashcat(e,t=[]){let r;try{r=sanitizeOptions(t)}catch(e){throw e}if(!e)throw new Error("Error: Hash data is required.");const o=path.join("/tmp",`hashcat_hashes_${Date.now()}.txt`),n=path.join("/tmp",`hashcat_${Date.now()}.potfile`);let s="",a=[];try{await fs.writeFile(o,e);const t=[...r];r.some((e=>e.includes("--potfile-path")))||t.push("--potfile-path",n),t.push(o),s+=`--- Hashcat Cracking Attempt ---\nExecuting: hashcat ${t.join(" ")}\n`;try{const e=await runSpawnCommand("hashcat",t);s+=`Exit Code: ${e.code}\nStdout:\n${e.stdout}\nStderr:\n${e.stderr}\n`;const r=e.stdout.split("\n");for(const e of r)if(e.includes(":")&&!e.includes("Session.........")&&!e.includes("Status..........")){const t=e.trim();!t||t.startsWith("[")||t.startsWith("hashcat")||a.push(t)}}catch(e){s+=`Hashcat command failed to execute: ${e.message}\n`}s+="--- Potfile Check ---\n";try{const e=await fs.readFile(n,"utf8");s+=`Potfile content:\n${e}\n`;const t=e.split("\n").filter((e=>e.trim()));a.push(...t)}catch(e){s+=`Could not read potfile: ${e.message}\n`}const i=[...r.filter((e=>!e.includes("--potfile-path"))),"--potfile-path",n,"--show",o];s+=`--- Show Cracked Hashes ---\nExecuting: hashcat ${i.join(" ")}\n`;try{const e=await runSpawnCommand("hashcat",i);s+=`Exit Code: ${e.code}\nStdout:\n${e.stdout}\nStderr:\n${e.stderr}\n`;const t=e.stdout.split("\n").map((e=>e.trim())).filter((e=>e&&e.includes(":")));a.push(...t)}catch(e){s+=`Show command failed to execute: ${e.message}\n`}a=[...new Set(a)].filter((e=>e&&e.includes(":")));try{await fs.unlink(o)}catch{}return currentUserSession.mode===UserMode.PROFESSIONAL&&await logMessage(`Ran Hashcat.\nOptions: ${r.join(" ")}\nCracked: ${a.length} hashes.`),{fullOutput:s,cracked:a,potfileLocation:n}}catch(e){console.error("Fatal error setting up Hashcat execution:",e);try{await fs.unlink(o)}catch{}try{await fs.unlink(n)}catch{}throw currentUserSession.mode===UserMode.PROFESSIONAL&&await logMessage(`Hashcat FAILED fatally before execution.\nOptions: ${r.join(" ")}\nError: ${e.message}`),new Error(`Hashcat setup failed fatally: ${e.message}`)}}function formatResultsForStudent(e,t,r){let o=`Scan results for **${e}** (options: ${t.join(" ")||"default"}):\n\n`;const n=[];let s=!1;for(const e in r){const a=r[e];if(o+=`**Host:** ${a.hostname?`${a.hostname} (${e})`:e}\n`,a.mac&&(o+=`*   MAC Address: ${a.mac} (This is the hardware address, useful for identifying devices on a local network).\n`),a.osNmap&&(o+=`*   Operating System Guess: ${a.osNmap} (Nmap tries to guess the OS based on network responses).\n`,t.includes("-O")||n.push(`Try adding \`-O\` to the options for a more dedicated OS detection scan on ${e}.`)),a.ports&&a.ports.length>0){const t=a.ports.filter((e=>"open"===e.state));t.length>0?(s=!0,o+="*   **Open Ports Found:**\n",t.forEach((t=>{o+=`    *   **Port ${t.portId}/${t.protocol}:** State is **${t.state}**. Service detected: **${t.service?.name||"unknown"}**. Reason: ${t.reason}\n`,"80"!==t.portId&&"443"!==t.portId||(n.push(`Port ${t.portId} (${t.service?.name}) is open on ${e}. Try exploring it with a web browser or tools like \`curl\`.`),n.push(`Consider running \`nmapScan\` with scripts: \`options: ["-sV", "-sC", "-p${t.portId}"]\` on ${e} to get more service info.`)),"22"===t.portId&&n.push(`SSH (Port 22) is open on ${e}. You could try connecting if you have credentials, or check for common vulnerabilities (\`options: ["-sV", "--script=ssh-auth-methods"]\`).`),"21"!==t.portId&&"23"!==t.portId||n.push(`${t.service?.name} (Port ${t.portId}) on ${e} is often insecure. Check for anonymous login or default credentials (\`options: ["-sV", "--script=ftp-anon"]\` for FTP).`),"3389"===t.portId&&n.push(`RDP (Port 3389) on ${e} allows remote desktop access. Check for weak passwords or vulnerabilities.`)}))):o+=`*   No *open* ports were detected in the scanned range for ${e}. Filtered ports might still exist.\n`}else o+=`*   Port scanning was not performed or no ports were reported for ${e}.\n`;o+="\n"}return s||n.push("No open ports found with the current options. Try scanning all ports (`-p-` ) or using different scan types like SYN scan (`-sS`, requires root/admin) or UDP scan (`-sU`)."),!t.includes("-sV")&&s&&n.push("Run with `-sV` option to try and determine the version of the services running on the open ports."),{explanation:o,suggestions:n}}const TEMP_WORDLIST_DIR=path.join("/tmp","pentest-mcp-wordlists");async function ensureTempWordlistDirExists(){try{await fs.mkdir(TEMP_WORDLIST_DIR,{recursive:!0})}catch(e){console.error("Error creating temp wordlist directory:",e)}}function toLeet(e){return e.replace(/a/gi,"4").replace(/e/gi,"3").replace(/i/gi,"1").replace(/o/gi,"0").replace(/s/gi,"5").replace(/t/gi,"7")}async function runGobuster(e,t=[]){if(console.error(`Executing Gobuster: target=${e}, raw_options=${t.join(" ")}`),!e.startsWith("http://")&&!e.startsWith("https://"))throw new Error("Target must be a valid URL starting with http:// or https://");let r;try{r=sanitizeOptions(t)}catch(e){throw e}let o="",n=[];try{const t=["dir","-u",e,...r];o+=`--- Directory Enumeration ---\nExecuting: gobuster ${t.join(" ")}\n`;try{const e=await runSpawnCommand("gobuster",t);o+=`Exit Code: ${e.code}\nStdout:\n${e.stdout}\nStderr:\n${e.stderr}\n`,n=e.stdout.split("\n").filter((e=>e.includes("Status:")&&(e.includes("200")||e.includes("301")||e.includes("302")))).map((e=>e.match(/^\s*(\/[^\s]*)/)?.[1]||e))}catch(e){o+=`Gobuster command failed to execute: ${e.message}\n`}return currentUserSession.mode===UserMode.PROFESSIONAL&&await logMessage(`Ran Gobuster against ${e}.\nOptions: ${r.join(" ")}\nFound: ${n.length} paths.`),{fullOutput:o,foundPaths:n}}catch(t){throw console.error("Fatal error setting up Gobuster execution:",t),currentUserSession.mode===UserMode.PROFESSIONAL&&await logMessage(`Gobuster FAILED fatally before execution.\nTarget: ${e}\nOptions: ${r.join(" ")}\nError: ${t.message}`),new Error(`Gobuster setup failed fatally: ${t.message}`)}}async function runNikto(e,t=[]){if(console.error(`Executing Nikto: target=${e}, raw_options=${t.join(" ")}`),!e.startsWith("http://")&&!e.startsWith("https://"))throw new Error("Target must be a valid URL starting with http:// or https://");let r;try{r=sanitizeOptions(t)}catch(e){throw e}let o="",n=[];try{const t=["-h",e,...r];o+=`--- Vulnerability Scan ---\nExecuting: nikto ${t.join(" ")}\n`;try{const e=await runSpawnCommand("nikto",t);o+=`Exit Code: ${e.code}\nStdout:\n${e.stdout}\nStderr:\n${e.stderr}\n`,n=e.stdout.split("\n").filter((e=>e.startsWith("+")&&!e.includes("+ No web server"))).map((e=>e.trim()))}catch(e){o+=`Nikto command failed to execute: ${e.message}\n`}return currentUserSession.mode===UserMode.PROFESSIONAL&&await logMessage(`Ran Nikto against ${e}.\nOptions: ${r.join(" ")}\nFound: ${n.length} potential issues.`),{fullOutput:o,findings:n}}catch(t){throw console.error("Fatal error setting up Nikto execution:",t),currentUserSession.mode===UserMode.PROFESSIONAL&&await logMessage(`Nikto FAILED fatally before execution.\nTarget: ${e}\nOptions: ${r.join(" ")}\nError: ${t.message}`),new Error(`Nikto setup failed fatally: ${t.message}`)}}async function runSubfinder(e,t=[]){if(!e||/\s/.test(e))throw new Error("Domain must be a non-empty domain string.");const r=["-d",e,"-silent",...sanitizeOptions(t)],o=await runSpawnCommand("subfinder",r),n=o.stdout.split("\n").map((e=>e.trim())).filter(Boolean);return{fullOutput:`Executing: subfinder ${r.join(" ")}\nExit: ${o.code}\nStderr:\n${o.stderr}`,domains:n}}async function runHttpxProbe(e,t=[]){if(0===e.length)throw new Error("At least one target is required.");const r=sanitizeOptions(t),o=path.join("/tmp",`httpx-targets-${Date.now()}.txt`);try{await fs.writeFile(o,e.join("\n"));const t=["-silent","-l",o,...r],n=await runSpawnCommand("httpx",t),s=n.stdout.split("\n").map((e=>e.trim())).filter(Boolean);return{fullOutput:`Executing: httpx ${t.join(" ")}\nExit: ${n.code}\nStderr:\n${n.stderr}`,results:s}}finally{await fs.unlink(o).catch((()=>{}))}}async function runFfufScan(e,t,r=[]){if(!e.startsWith("http://")&&!e.startsWith("https://"))throw new Error("URL must start with http:// or https://");const o=sanitizeOptions(r),n=path.join("/tmp",`ffuf-${Date.now()}.json`),s=["-u",e,"-w",t,"-of","json","-o",n,...o],a=await runSpawnCommand("ffuf",s);let i=[];try{const e=await fs.readFile(n,"utf8");i=(JSON.parse(e).results||[]).map((e=>`${e.status??"n/a"} ${e.url??"unknown"} len=${e.length??"n/a"} words=${e.words??"n/a"} lines=${e.lines??"n/a"}`))}catch{i=a.stdout.split("\n").map((e=>e.trim())).filter(Boolean)}return await fs.unlink(n).catch((()=>{})),{fullOutput:`Executing: ffuf ${s.join(" ")}\nExit: ${a.code}\nStdout:\n${a.stdout}\nStderr:\n${a.stderr}`,findings:i}}async function runNucleiScan(e,t=[]){if(0===e.length)throw new Error("At least one target is required.");const r=sanitizeOptions(t),o=path.join("/tmp",`nuclei-targets-${Date.now()}.txt`),n=path.join("/tmp",`nuclei-${Date.now()}.jsonl`);try{await fs.writeFile(o,e.join("\n"));const t=["-l",o,"-jsonl","-o",n,...r],s=await runSpawnCommand("nuclei",t);let a=[];try{a=(await fs.readFile(n,"utf8")).split("\n").map((e=>e.trim())).filter(Boolean).map((e=>{try{const t=JSON.parse(e);return`[${t.info?.severity||"info"}] ${t.info?.name||t.templateID||"finding"} -> ${t.matched||"n/a"}`}catch{return e}}))}catch{a=s.stdout.split("\n").map((e=>e.trim())).filter(Boolean)}return{fullOutput:`Executing: nuclei ${t.join(" ")}\nExit: ${s.code}\nStdout:\n${s.stdout}\nStderr:\n${s.stderr}`,findings:a}}finally{await fs.unlink(o).catch((()=>{})),await fs.unlink(n).catch((()=>{}))}}async function runTrafficCapture(e,t,r,o,n=[]){if(!e)throw new Error("networkInterface is required (example: en0, eth0).");if(!t||t<=0)throw new Error("packetCount must be > 0 to avoid long-running captures.");const s=sanitizeOptions(n),a=["-i",e,"-nn","-c",String(t)];o&&a.push("-w",o),a.push(...s),r&&a.push(r);const i=await runSpawnCommand("tcpdump",a),c=i.stdout.split("\n").map((e=>e.trim())).filter(Boolean).slice(0,100);return{fullOutput:`Executing: tcpdump ${a.join(" ")}\nExit: ${i.code}\nStdout:\n${i.stdout}\nStderr:\n${i.stderr}`,findings:c}}async function runHydra(e,t,r=[]){if(!e)throw new Error("target is required.");if(!t)throw new Error("service is required (ssh, rdp, ftp, smb, etc.).");const o=[...sanitizeOptions(r),e,t],n=await runSpawnCommand("hydra",o),s=`${n.stdout}\n${n.stderr}`.split("\n").map((e=>e.trim())).filter((e=>/login:|password:/i.test(e)));return{fullOutput:`Executing: hydra ${o.join(" ")}\nExit: ${n.code}\nStdout:\n${n.stdout}\nStderr:\n${n.stderr}`,findings:s}}async function runSqlmap(e,t=[]){if(!e.startsWith("http://")&&!e.startsWith("https://"))throw new Error("targetUrl must start with http:// or https://");const r=["-u",e,"--batch",...sanitizeOptions(t)],o=await runSpawnCommand("sqlmap",r),n=`${o.stdout}\n${o.stderr}`.split("\n").map((e=>e.trim())).filter((e=>/\[(CRITICAL|WARNING|INFO)\]/.test(e))).slice(0,200);return{fullOutput:`Executing: sqlmap ${r.join(" ")}\nExit: ${o.code}\nStdout:\n${o.stdout}\nStderr:\n${o.stderr}`,findings:n}}async function runPrivEscAudit(e=[]){const t=sanitizeOptions(e),r=[],o=[],n=[{cmd:"id",args:[],label:"Identity"},{cmd:"whoami",args:[],label:"Current User"},{cmd:"uname",args:["-a"],label:"Kernel"},{cmd:"sudo",args:["-n","-l"],label:"Sudo Rights"}];for(const e of n)try{const t=await runSpawnCommand(e.cmd,e.args);r.push(`## ${e.label}\n${t.stdout}\n${t.stderr}`);const n=`${t.stdout}\n${t.stderr}`.split("\n").map((e=>e.trim())).filter((e=>/NOPASSWD|root|CAP_|SUID|sudoers|ALL=\(ALL\)/i.test(e)));o.push(...n)}catch(t){r.push(`## ${e.label}\nError: ${t.message}`)}if(await checkCommandAvailable("linpeas"))try{const e=await runSpawnCommand("linpeas",t.length>0?t:["-q"]);r.push(`## linpeas\n${e.stdout}\n${e.stderr}`);const n=`${e.stdout}\n${e.stderr}`.split("\n").map((e=>e.trim())).filter((e=>/(sudo|kernel|exploit|password|credential|SUID|writable)/i.test(e))).slice(0,200);o.push(...n)}catch(e){r.push(`## linpeas\nError: ${e.message}`)}return{fullOutput:r.join("\n\n"),findings:[...new Set(o)]}}function defaultScopeTemplate(){return["Scope source: default template (client SoW not shared).","Authorized targets: Only explicitly approved assets.","Prohibited actions: No destructive actions, no denial-of-service, no persistence.","Time window: Business-approved testing window only.","Data handling: Minimum data access; sensitive data redacted in report.","Reporting: Findings include evidence, severity, impact, and remediation."].join("\n")}async function main(){console.error("Node.js process PATH:",process.env.PATH),console.error(`Initializing Pentest MCP Server with ${transportType} transport...`);const e=["nmap","john","hashcat","gobuster","nikto","subfinder","httpx","ffuf","nuclei","tcpdump","hydra","sqlmap"],t=[];for(const r of e)await checkCommandAvailable(r)||t.push(r);t.length>0&&console.warn(`Missing tools in PATH: ${t.join(", ")}. Related MCP tools will return runtime errors until installed.`),server=new McpServer({name:"pentest-mcp",version:serverVersion},{capabilities:{resources:{},tools:{}}}),server.resource("mode",new ResourceTemplate("mcp://pentest/mode",{list:void 0}),(async e=>({contents:[{uri:e.href,text:`Current Mode: ${currentUserSession.mode}`,metadata:{currentMode:currentUserSession.mode}}]})));const r=new Map;server.resource("clientReport",new ResourceTemplate("mcp://pentest/clientReport/{reportId}",{list:void 0}),(async e=>{const t=e.href.match(/mcp:\/\/pentest\/clientReport\/(.+)/),o=t?t[1]:null;if(!o||"list"===o)return{contents:Array.from(r.values()).map((e=>({uri:`mcp://pentest/clientReport/${e.reportId}`,text:`Report: ${e.title}`})))};const n=r.get(o);if(!n)throw new Error(`Report ${o} not found`);return{contents:[{uri:e.href,text:JSON.stringify(n,null,2)}]}}));const o=z.object({mode:z.enum([UserMode.STUDENT,UserMode.PROFESSIONAL])}).describe('Switch between `student` mode (verbose guidance) and `professional` mode (concise output). Call this at the start of a session or whenever you need to adjust the level of explanation. Example: `{"mode":"professional"}`');server.tool("setMode",o.shape,(async({mode:e})=>(currentUserSession.mode=e,await logMessage(`Mode changed to ${e}.`),{content:[{type:"text",text:`Session mode set to: ${e}`}]})));const n=z.object({target:z.string(),ports:z.string().optional(),fastScan:z.boolean().optional(),topPorts:z.number().int().optional(),scanTechnique:z.enum(["SYN","Connect","ACK","Window","Maimon","FIN","Xmas","Null","Proto"]).optional(),udpScan:z.boolean().optional(),serviceVersionDetection:z.boolean().optional(),versionIntensity:z.number().int().optional(),osDetection:z.boolean().optional(),defaultScripts:z.boolean().optional(),scripts:z.array(z.string()).optional(),scriptArgs:z.string().optional(),timingTemplate:z.enum(["T0","T1","T2","T3","T4","T5"]).optional(),skipHostDiscovery:z.boolean().optional(),verbose:z.boolean().optional(),rawOptions:z.array(z.string()).optional(),userModeHint:z.enum([UserMode.STUDENT,UserMode.PROFESSIONAL]).optional()}).describe('Run an Nmap scan to discover hosts and services. Use this before other tools to identify attack surface. Options map directly to Nmap flags. Note that SYN scans or OS detection (e.g. `-sS`, `-O`) require elevated privileges. Example: `{"target":"192.168.1.0/24", "scanTechnique":"SYN", "serviceVersionDetection":true}`');server.tool("nmapScan",n.shape,(async(e,t)=>{const{target:r,ports:o,fastScan:n,topPorts:s,scanTechnique:a,udpScan:i,serviceVersionDetection:c,versionIntensity:p,osDetection:d,defaultScripts:u,scripts:l,scriptArgs:h,timingTemplate:m,skipHostDiscovery:g,verbose:f,rawOptions:w,userModeHint:y}=e;console.error("Received nmapScan request:",e),currentUserSession.mode===UserMode.UNKNOWN&&(currentUserSession.mode=y||UserMode.STUDENT);try{const e=[],y=[];g&&e.push("-Pn");let b=0;if(o&&b++,n&&b++,s&&b++,b>1?y.push("Use only one of ports, fastScan, or topPorts."):o?e.push("-p",o):n?e.push("-F"):s&&e.push("--top-ports",String(s)),a)switch(a){case"SYN":e.push("-sS");break;case"Connect":e.push("-sT");break;case"ACK":e.push("-sA");break;case"Window":e.push("-sW");break;case"Maimon":e.push("-sM");break;case"FIN":e.push("-sF");break;case"Xmas":e.push("-sX");break;case"Null":e.push("-sN");break;case"Proto":e.push("-sO")}if(i&&e.push("-sU"),c?(e.push("-sV"),void 0!==p&&e.push("--version-intensity",String(p))):void 0!==p&&y.push("Cannot set intensity without -sV."),d&&e.push("-O"),u&&l?y.push("Cannot use both -sC and --script."):u?e.push("-sC"):l&&l.length>0&&e.push("--script",l.join(",")),h&&(u||l?e.push("--script-args",h):y.push("Cannot use scriptArgs without scripts.")),m&&e.push(`-${m}`),f&&e.push("-v"),w&&e.push(...w),y.length>0)throw new Error(`Invalid params: ${y.join("; ")}`);e.some((e=>"-sS"===e||"-O"===e))&&console.warn("Nmap options may require elevated privileges.");const S=await runNmapScan(r,e);let x=[],v=[];if("string"==typeof S)x.push({type:"text",text:S});else if(S)if(currentUserSession.mode===UserMode.STUDENT){const{explanation:t,suggestions:o}=formatResultsForStudent(r,e,S);x.push({type:"text",text:t}),v.push(...o)}else{x.push({type:"text",text:JSON.stringify(S,null,2)});const e={};Object.entries(S).forEach((([t,r])=>{const o=r;e[t]||(e[t]=new Set),o.ports?.forEach((r=>{"open"===r.state&&e[t].add(r.portId)}))}));for(const t in e);0===v.length&&Object.keys(e).length>0&&v.push("Scan complete.")}else x.push({type:"text",text:"Nmap scan returned no data."});v.length>0&&x.push({type:"text",text:"\n**Suggestions:**\n* "+v.join("\n* ")});const $=formatInvocation(t),E=saveEngagementRecord({tool:"nmapScan",category:"finding",target:r,summary:`Nmap scan completed for ${r}`,findings:x.map((e=>e.text)).join("\n").split("\n").slice(0,50),rawOutput:x.map((e=>e.text)).join("\n"),invocation:$});return x.push({type:"text",text:`recordId=${E}`}),x.push({type:"text",text:$}),{content:x}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const s=z.object({baseWords:z.array(z.string()).describe("List of base words (names, pets, places, etc.)."),dates:z.array(z.string()).optional().describe("List of dates (YYYY-MM-DD, MM-DD, YYYY). Parsed for variations."),customPatterns:z.array(z.string()).optional().describe("List of custom patterns/symbols to prepend/append (e.g., '!', '123')."),minYear:z.number().int().optional().describe("Minimum year (YYYY) to include in variations."),maxYear:z.number().int().optional().describe("Maximum year (YYYY) to include in variations (defaults to current year)."),includeLeet:z.boolean().optional().default(!1).describe("Apply basic leetspeak substitutions (a=4, e=3, etc.)."),caseVariations:z.boolean().optional().default(!0).describe("Include variations like TitleCase, UPPERCASE.")}).describe('Generate a custom password wordlist from target-related words. Use this before running John the Ripper. Example: `{"baseWords":["Acme","Smith"],"dates":["1984"],"customPatterns":["!"]}`');server.tool("generateWordlist",s.shape,(async({baseWords:e,dates:t,customPatterns:r,minYear:o,maxYear:n,includeLeet:s,caseVariations:a},i)=>{console.error("Received generateWordlist:",{baseWords:`${e.length} words`,dates:t,customPatterns:r}),await ensureTempWordlistDirExists();const c=new Set,p=(new Date).getFullYear(),d=n||p,u=[];for(let e=o||p-10;e<=d;e++)u.push(String(e)),u.push(String(e).slice(-2));const l=[];t&&t.forEach((e=>{const t=e.split(/[-/]/);3===t.length?l.push(t[0],t[0].slice(-2),t[1],t[2],t[1]+t[2],t[1]+t[2]+t[0],t[1]+t[2]+t[0].slice(-2)):2===t.length?l.push(t[0],t[1],t[0]+t[1]):1===t.length&&4===t[0].length&&l.push(t[0],t[0].slice(-2))}));const h=r||[],m=[...h,...u,...l],g=[...h];e.forEach((e=>{const t=new Set([e]);if(a&&(t.add(e.toLowerCase()),t.add(e.toUpperCase()),t.add(e.charAt(0).toUpperCase()+e.slice(1).toLowerCase())),s){const r=toLeet(e);t.add(r),a&&(t.add(r.toLowerCase()),t.add(r.toUpperCase()),t.add(r.charAt(0).toUpperCase()+r.slice(1).toLowerCase()))}t.forEach((e=>{c.add(e),g.forEach((t=>{c.add(t+e)})),m.forEach((t=>{c.add(e+t)})),g.forEach((t=>{m.forEach((r=>{c.add(t+e+r)}))}))}))})),[...u,...l].forEach((e=>c.add(e)));const f=`wordlist_${Date.now()}.txt`,w=path.join(TEMP_WORDLIST_DIR,f);try{await fs.writeFile(w,Array.from(c).join("\n"));const e=formatInvocation(i),t=saveEngagementRecord({tool:"generateWordlist",category:"other",summary:`Generated custom wordlist with ${c.size} entries`,findings:[`wordCount=${c.size}`,`path=${w}`],rawOutput:Array.from(c).slice(0,200).join("\n"),invocation:e});return{content:[{type:"text",text:`Generated ${c.size} words.`},{type:"text",text:`Path: ${w}`},{type:"text",text:`recordId=${t}`},{type:"text",text:e}]}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const a=z.object({hashData:z.string().describe("String containing the password hashes, one per line."),options:z.array(z.string()).optional().describe("Array of command-line options for JtR.")}).describe('Crack password hashes using John the Ripper. Provide hashes and any JtR options. Run this after generating a wordlist. Example: `{"hashData":"user:$1$hash", "options":["--wordlist=/tmp/list.txt"]}`');server.tool("runJohnTheRipper",a.shape,(async({hashData:e,options:t},r)=>{console.error("Received JtR:",{hashData:`len=${e.length}`,options:t}),currentUserSession.mode===UserMode.STUDENT&&console.warn("[Student Mode] Executing JtR.");try{const{fullOutput:o,cracked:n}=await runJtR(e,t||[]),s=[{type:"text",text:`JtR finished. Found ${n.length} cracked.`}];n.length>0&&s.push({type:"text",text:"\n**Cracked:**\n"+n.join("\n")}),s.push({type:"text",text:"\n--- Full JtR Output ---\n"+o});const a=formatInvocation(r),i=saveEngagementRecord({tool:"runJohnTheRipper",category:"cracking",summary:`John the Ripper completed with ${n.length} cracked entries.`,findings:n,rawOutput:o,invocation:a});return s.push({type:"text",text:`recordId=${i}`}),s.push({type:"text",text:a}),{content:s}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const i=z.object({hashData:z.string().describe("String containing the password hashes, one per line."),attackMode:z.enum(["0","1","3","6","7"]).optional().describe("Attack mode: 0=Straight, 1=Combination, 3=Brute-force, 6=Hybrid Wordlist + Mask, 7=Hybrid Mask + Wordlist"),hashType:z.string().optional().describe("Hash-type, e.g., 0=MD5, 100=SHA1, 1000=NTLM, 1400=SHA2-256, 1800=sha512crypt, 22000=WPA*01/WPA*02"),wordlist:z.string().optional().describe("Path to wordlist file for dictionary attacks"),mask:z.string().optional().describe("Mask for brute-force attacks (e.g., '?a?a?a?a?a?a?a?a' for 8 chars)"),increment:z.boolean().optional().describe("Enable incremental mode (start with shorter passwords)"),incrementMin:z.number().int().optional().describe("Minimum password length for incremental mode"),incrementMax:z.number().int().optional().describe("Maximum password length for incremental mode"),rules:z.string().optional().describe("Rules file to apply to wordlist"),session:z.string().optional().describe("Session name for resuming attacks"),restore:z.boolean().optional().describe("Restore a previous session"),optimizedKernels:z.boolean().optional().describe("Enable optimized kernels (-O)"),workloadProfile:z.enum(["1","2","3","4"]).optional().describe("Workload profile: 1=Low, 2=Default, 3=High, 4=Nightmare"),deviceTypes:z.array(z.enum(["1","2","3"])).optional().describe("Device types: 1=CPU, 2=GPU, 3=FPGA"),force:z.boolean().optional().describe("Ignore warnings"),potfilePath:z.string().optional().describe("Path to custom potfile"),outfile:z.string().optional().describe("Output file for cracked hashes"),outfileFormat:z.number().int().optional().describe("Output format: 1=hash, 2=plain, 3=hex-plain, etc."),runtime:z.number().int().optional().describe("Abort session after X seconds"),showProgress:z.boolean().optional().describe("Show progress every X seconds"),quiet:z.boolean().optional().describe("Suppress output"),loopback:z.boolean().optional().describe("Add new plains to induct directory"),markovThreshold:z.number().int().optional().describe("Threshold X when to stop accepting new Markov-chains"),customCharset1:z.string().optional().describe("User-defined charset ?1"),customCharset2:z.string().optional().describe("User-defined charset ?2"),customCharset3:z.string().optional().describe("User-defined charset ?3"),customCharset4:z.string().optional().describe("User-defined charset ?4"),options:z.array(z.string()).optional().describe("Additional raw hashcat options")}).describe('Crack password hashes using Hashcat. More powerful and faster than John the Ripper for many hash types, especially with GPU acceleration. Supports various attack modes and hash types. Example: `{"hashData":"5d41402abc4b2a76b9719d911017c592", "hashType":"0", "attackMode":"0", "wordlist":"/tmp/wordlist.txt"}`');server.tool("runHashcat",i.shape,(async(e,t)=>{const{hashData:r,attackMode:o,hashType:n,wordlist:s,mask:a,increment:i,incrementMin:c,incrementMax:p,rules:d,session:u,restore:l,optimizedKernels:h,workloadProfile:m,deviceTypes:g,force:f,potfilePath:w,outfile:y,outfileFormat:b,runtime:S,showProgress:x,quiet:v,loopback:$,markovThreshold:E,customCharset1:z,customCharset2:O,customCharset3:C,customCharset4:k,options:T}=e;console.error("Received Hashcat:",{hashData:`len=${r.length}`,attackMode:o,hashType:n,wordlist:s}),currentUserSession.mode===UserMode.STUDENT&&console.warn("[Student Mode] Executing Hashcat.");try{const e=[],P=[];if(o?e.push("-a",o):e.push("-a","0"),n&&e.push("-m",n),"0"!==o&&"1"!==o&&o||!s?"0"!==o&&o||s||a||P.push("Dictionary attack requires a wordlist"):e.push(s),"3"===o&&a?e.push(a):"3"!==o||a||P.push("Brute-force attack requires a mask"),"6"===o&&s&&a?e.push(s,a):"6"===o&&P.push("Hybrid Wordlist + Mask attack requires both wordlist and mask"),"7"===o&&a&&s?e.push(a,s):"7"===o&&P.push("Hybrid Mask + Wordlist attack requires both mask and wordlist"),i&&(e.push("-i"),c&&e.push("--increment-min",c.toString()),p&&e.push("--increment-max",p.toString())),d&&e.push("-r",d),u&&e.push("--session",u),l&&e.push("--restore"),h&&e.push("-O"),m&&e.push("-w",m),g&&g.length>0&&e.push("-d",g.join(",")),f&&e.push("--force"),w&&e.push("--potfile-path",w),y&&(e.push("-o",y),b&&e.push("--outfile-format",b.toString())),S&&e.push("--runtime",S.toString()),x&&e.push("--status"),v&&e.push("--quiet"),$&&e.push("--loopback"),E&&e.push("--markov-threshold",E.toString()),z&&e.push("-1",z),O&&e.push("-2",O),C&&e.push("-3",C),k&&e.push("-4",k),T&&e.push(...T),P.length>0)throw new Error(`Invalid parameters: ${P.join("; ")}`);const{fullOutput:I,cracked:M,potfileLocation:R}=await runHashcat(r,e),A=[];currentUserSession.mode===UserMode.STUDENT?(A.push({type:"text",text:`Hashcat finished! Found ${M.length} cracked passwords.`}),M.length>0?(A.push({type:"text",text:"\n**🎉 Cracked Passwords:**\n"+M.join("\n")}),A.push({type:"text",text:"\n**What this means:** These are the plain-text passwords that correspond to your hashes. You can now use these for further testing or to demonstrate the importance of strong passwords."})):A.push({type:"text",text:"\n**No passwords cracked.** This could mean:\n- The passwords are very strong\n- You need a better wordlist\n- The hash type might be incorrect\n- Try different attack modes or longer runtime"}),R&&A.push({type:"text",text:`\n**Tip:** Results are saved in: ${R}`})):(A.push({type:"text",text:`Hashcat completed. Cracked: ${M.length} hashes.`}),M.length>0&&A.push({type:"text",text:"\n**Cracked:**\n"+M.join("\n")}),R&&A.push({type:"text",text:`Potfile: ${R}`})),A.push({type:"text",text:"\n--- Full Hashcat Output ---\n"+I});const N=formatInvocation(t),U=saveEngagementRecord({tool:"runHashcat",category:"cracking",summary:`Hashcat completed with ${M.length} cracked entries.`,findings:M,rawOutput:I,invocation:N});return A.push({type:"text",text:`recordId=${U}`}),A.push({type:"text",text:N}),{content:A}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const c=z.object({scanId:z.string().describe("The ID of the scan to cancel")}).describe("Stop an Nmap scan that is currently running. Pass the scanId returned when the scan was started.");server.tool("cancelScan",c.shape,(async({scanId:e})=>{const t=activeScans.get(e);if(!t)return{content:[{type:"text",text:`Error: No active scan found with ID: ${e}`}],isError:!0};try{return t.process.kill(),t.progress.status="cancelled",activeScans.delete(e),{content:[{type:"text",text:`Successfully cancelled scan ${e}`}]}}catch(e){return{content:[{type:"text",text:`Error: Failed to cancel scan: ${e.message}`}],isError:!0}}}));const p=z.object({category:z.enum(["sniffing","finding","bruteforce","cracking","privilege-escalation","extraction","reporting","other"]).optional().describe("Optional workflow category filter"),targetContains:z.string().optional().describe("Optional case-insensitive target filter"),limit:z.number().int().positive().max(500).optional().describe("Maximum records to return")}).describe("List captured engagement records so reports can be assembled without manual copy/paste.");server.tool("listEngagementRecords",p.shape,(async({category:e,targetContains:t,limit:r})=>{const o=t?.toLowerCase(),n=Array.from(engagementRecords.values()).filter((t=>!e||t.category===e)).filter((e=>!o||(e.target||"").toLowerCase().includes(o))).sort(((e,t)=>t.createdAt-e.createdAt)).slice(0,r||100);return{content:[{type:"text",text:`Returned ${n.length} engagement records.`},{type:"text",text:JSON.stringify(n,null,2)}]}}));const d=z.object({recordId:z.string().describe("Engagement record ID (recordId=...)")}).describe("Fetch a single engagement record by ID.");server.tool("getEngagementRecord",d.shape,(async({recordId:e})=>{const t=engagementRecords.get(e);return t?{content:[{type:"text",text:JSON.stringify(t,null,2)}]}:{content:[{type:"text",text:`Record ${e} not found.`}],isError:!0}}));const u=z.object({client:z.string().optional().describe("Client name for the report"),title:z.string().describe("Title of the assessment report"),assessmentType:z.string().describe("Type of assessment"),scanIds:z.array(z.string()).optional().describe("IDs of scans to include"),recordIds:z.array(z.string()).optional().describe("Specific engagement record IDs to include"),scopeMode:z.enum(["ask","provided","template"]).optional().default("ask").describe("How to include scope-of-work notes"),scopeOfWork:z.string().optional().describe("Scope-of-work notes when scopeMode=provided"),summary:z.string().optional().describe("Executive summary"),recommendations:z.array(z.string()).optional().describe("List of recommendations")}).describe("Create a full client report from collected findings. Scope notes can be asked via MCP elicitation, supplied directly, or generated from a safe default template.");server.tool("createClientReport",u.shape,(async(e,t)=>{const{client:o,title:n,assessmentType:s,scanIds:a,recordIds:i,scopeMode:c,scopeOfWork:p,summary:d,recommendations:u}=e,l=`report-${Date.now()}`,h=(a||[]).map(getScanDataById).filter(Boolean),m=analyzeFindings(h),g=formatInvocation(t),f=c||"ask";let w=defaultScopeTemplate();if("provided"===f&&p?.trim())w=p.trim();else if("ask"===f)try{const e=await t.sendRequest({method:"elicitation/create",params:{mode:"form",message:"Provide scope-of-work details for this report (or decline to use default template).",requestedSchema:{type:"object",properties:{authorizedTargets:{type:"string",title:"Authorized Targets"},prohibitedActions:{type:"string",title:"Prohibited Actions"},objective:{type:"string",title:"Assessment Objective"},timeWindow:{type:"string",title:"Testing Window"},dataHandling:{type:"string",title:"Data Handling Requirements"}},required:["authorizedTargets","objective"]}}},ElicitResultSchema);if("accept"===e.action){const t=e.content;w=["Scope source: elicited via user invocation.",`Authorized targets: ${t.authorizedTargets||"Not provided"}`,`Objective: ${t.objective||"Not provided"}`,`Prohibited actions: ${t.prohibitedActions||"Not provided"}`,`Testing window: ${t.timeWindow||"Not provided"}`,`Data handling: ${t.dataHandling||"Not provided"}`].join("\n")}}catch(e){console.warn(`Scope elicitation failed, using default template: ${e.message}`)}const y=i&&i.length>0?i.map((e=>engagementRecords.get(e))).filter(Boolean):Array.from(engagementRecords.values()).sort(((e,t)=>t.createdAt-e.createdAt)).slice(0,200),b={sniffing:0,finding:0,bruteforce:0,cracking:0,"privilege-escalation":0,extraction:0,reporting:0,other:0};for(const e of y)b[e.category]=(b[e.category]||0)+1;const S={reportId:l,title:n,client:o||"Client withheld",createdAt:Date.now(),assessmentType:s,findings:m,scans:h,engagementRecords:y,workflowSummary:b,scopeNotes:w,invocationContext:g,summary:d||"",recommendations:u||[]};r.set(l,S);const x=Object.entries(b).map((([e,t])=>`${e}: ${t}`)).join("\n");return{content:[{type:"text",text:`Client report created: ${l}`},{type:"text",text:`recordId=${saveEngagementRecord({tool:"createClientReport",category:"reporting",target:o||"Client withheld",summary:`Generated client report ${l} for ${s}`,findings:[`reportId=${l}`,`recordsIncluded=${y.length}`,`scopeMode=${f}`],rawOutput:JSON.stringify(S,null,2),invocation:g})}`},{type:"text",text:`URI: mcp://pentest/clientReport/${l}`},{type:"text",text:`Scope Notes:\n${w}`},{type:"text",text:`Workflow Coverage:\n${x}`},{type:"text",text:`Invocation Context:\n${g}`}]}}));const l=z.object({target:z.string().url().describe("Target URL"),wordlist:z.string().describe("Path to wordlist"),extensions:z.string().optional().describe("File extensions (comma-separated)"),threads:z.number().int().positive().optional().describe("Number of threads"),statusCodes:z.string().optional().describe("Valid status codes (comma-separated)"),useragent:z.string().optional().describe("User-Agent string"),timeout:z.string().optional().describe("Timeout for requests"),basicAuth:z.string().optional().describe("Basic authentication credentials (username:password)"),cookie:z.string().optional().describe("Cookie to include in requests"),excludeLength:z.array(z.number()).optional().describe("Exclude paths of specific lengths"),followRedirect:z.boolean().optional().describe("Follow HTTP redirects"),noTLSValidation:z.boolean().optional().describe("Skip TLS certificate validation"),rawOptions:z.array(z.string()).optional().describe("Raw gobuster options")}).describe('Enumerate hidden directories and files on a web server. Run this after confirming the target hosts a web service. Provide a wordlist and optional extensions. Example: `{"target":"http://example.com", "wordlist":"/usr/share/wordlists/common.txt"}`');server.tool("gobuster",l.shape,(async(e,t)=>{const{target:r,wordlist:o,extensions:n,threads:s,statusCodes:a,useragent:i,timeout:c,basicAuth:p,cookie:d,excludeLength:u,followRedirect:l,noTLSValidation:h,rawOptions:m}=e;console.error("Received gobuster request:",e);try{const e=[];if(e.push("-w",o),n&&e.push("-x",n),s&&e.push("-t",s.toString()),a&&e.push("-s",a),i&&e.push("-a",i),c&&e.push("--timeout",c),p){const[t,r]=p.split(":");e.push("-U",t,"-P",r)}d&&e.push("-c",d),u&&e.push("--exclude-length",u.join(",")),l&&e.push("-r"),h&&e.push("-k"),m&&e.push(...m);const{fullOutput:g,foundPaths:f}=await runGobuster(r,e),w=[];currentUserSession.mode===UserMode.STUDENT?(w.push({type:"text",text:`Found ${f.length} paths/files at ${r}:\n\n${f.join("\n")}`}),f.length>0?w.push({type:"text",text:"\n**Next Steps:** ..."}):w.push({type:"text",text:"\n**No paths found...**"})):(w.push({type:"text",text:`Found ${f.length} paths at ${r}`}),f.length>0&&w.push({type:"text",text:"\n**Paths:**\n"+f.join("\n")}),w.push({type:"text",text:"\n**Full Output:**\n"+g}));const y=formatInvocation(t),b=saveEngagementRecord({tool:"gobuster",category:"finding",target:r,summary:`Gobuster found ${f.length} paths on ${r}`,findings:f,rawOutput:g,invocation:y});return w.push({type:"text",text:`recordId=${b}`}),w.push({type:"text",text:y}),{content:w}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const h=z.object({target:z.string().url().describe("Target URL"),port:z.string().optional().describe("Port(s) to scan"),ssl:z.boolean().optional().describe("Force SSL mode"),timeout:z.number().int().optional().describe("Timeout for requests"),useragent:z.string().optional().describe("User-Agent string"),tuning:z.string().optional().describe("Tuning mode"),output:z.string().optional().describe("Output file"),proxy:z.string().optional().describe("Use proxy"),basicAuth:z.string().optional().describe("Basic authentication credentials (username:password)"),root:z.string().optional().describe("Root directory"),cookies:z.string().optional().describe("Cookies to include"),rawOptions:z.array(z.string()).optional().describe("Raw nikto options")}).describe('Scan a web server for common vulnerabilities with Nikto. Use after identifying a live site. Some checks may be noisy. Example: `{"target":"http://192.168.1.10", "ssl":true, "port":"443"}`');server.tool("nikto",h.shape,(async(e,t)=>{const{target:r,port:o,ssl:n,timeout:s,useragent:a,tuning:i,output:c,proxy:p,basicAuth:d,root:u,cookies:l,rawOptions:h}=e;console.error("Received nikto request:",e);try{const e=[];e.push("-nointeractive"),o&&e.push("-p",o),n&&e.push("-ssl"),s&&e.push("-Timeout",s.toString()),a&&e.push("-useragent",a),i&&e.push("-Tuning",i),c&&e.push("-o",c),p&&e.push("-useproxy",p),d&&e.push("-id",d),u&&e.push("-root",u),l&&e.push("-Cookies",l),h&&e.push(...h);const{fullOutput:m,findings:g}=await runNikto(r,e),f=[];currentUserSession.mode===UserMode.STUDENT?(f.push({type:"text",text:`Found ${g.length} issues at ${r}`}),g.length>0||f.push({type:"text",text:"\n**No significant issues found...**"})):(f.push({type:"text",text:`Found ${g.length} issues at ${r}`}),g.length>0&&f.push({type:"text",text:"\n**Findings:**\n- "+g.join("\n- ")}),f.push({type:"text",text:"\n**Full Output:**\n"+m}),g.some((e=>e.toLowerCase().includes("injection"))));const w=formatInvocation(t),y=saveEngagementRecord({tool:"nikto",category:"finding",target:r,summary:`Nikto found ${g.length} findings on ${r}`,findings:g,rawOutput:m,invocation:w});return f.push({type:"text",text:`recordId=${y}`}),f.push({type:"text",text:w}),{content:f}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const m=z.object({domain:z.string().describe("Root domain to enumerate (example.com)"),recursive:z.boolean().optional().describe("Enable recursive subdomain discovery"),allSources:z.boolean().optional().describe("Use all available passive sources"),timeoutSeconds:z.number().int().positive().optional().describe("Global timeout in seconds"),rawOptions:z.array(z.string()).optional().describe("Additional safe subfinder options")}).describe("Enumerate subdomains for a target domain using subfinder.");server.tool("subfinderEnum",m.shape,(async({domain:e,recursive:t,allSources:r,timeoutSeconds:o,rawOptions:n},s)=>{try{const a=[];t&&a.push("-recursive"),r&&a.push("-all"),o&&a.push("-timeout",String(o)),n&&a.push(...n);const{fullOutput:i,domains:c}=await runSubfinder(e,a),p=formatInvocation(s),d=saveEngagementRecord({tool:"subfinderEnum",category:"finding",target:e,summary:`Subfinder enumerated ${c.length} domains for ${e}`,findings:c,rawOutput:i,invocation:p});return{content:[{type:"text",text:`Subfinder discovered ${c.length} domains for ${e}.`},{type:"text",text:`recordId=${d}`},{type:"text",text:p},{type:"text",text:c.join("\n")||"No subdomains found."},{type:"text",text:`\n--- Full Subfinder Output ---\n${i}`}]}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const g=z.object({targets:z.array(z.string()).min(1).describe("List of domains/hosts/URLs to probe"),includeTitle:z.boolean().optional().describe("Include page title"),includeStatusCode:z.boolean().optional().describe("Include HTTP status code"),includeTechDetect:z.boolean().optional().describe("Enable technology detection"),followRedirects:z.boolean().optional().describe("Follow redirects"),timeoutSeconds:z.number().int().positive().optional().describe("HTTP timeout in seconds"),rawOptions:z.array(z.string()).optional().describe("Additional safe httpx options")}).describe("Probe hosts with httpx and return live web endpoints.");server.tool("httpxProbe",g.shape,(async({targets:e,includeTitle:t,includeStatusCode:r,includeTechDetect:o,followRedirects:n,timeoutSeconds:s,rawOptions:a},i)=>{try{const c=[];t&&c.push("-title"),r&&c.push("-status-code"),o&&c.push("-tech-detect"),n&&c.push("-follow-redirects"),s&&c.push("-timeout",String(s)),a&&c.push(...a);const{fullOutput:p,results:d}=await runHttpxProbe(e,c),u=formatInvocation(i),l=saveEngagementRecord({tool:"httpxProbe",category:"finding",summary:`httpx probe completed for ${e.length} targets`,findings:d,rawOutput:p,invocation:u});return{content:[{type:"text",text:`httpx found ${d.length} responsive endpoints.`},{type:"text",text:`recordId=${l}`},{type:"text",text:u},{type:"text",text:d.join("\n")||"No responsive targets found."},{type:"text",text:`\n--- Full httpx Output ---\n${p}`}]}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const f=z.object({targetUrl:z.string().url().describe("Target URL with optional FUZZ marker"),wordlist:z.string().describe("Wordlist path for ffuf"),threads:z.number().int().positive().optional().describe("Worker threads"),matchStatusCodes:z.string().optional().describe("Comma-separated status codes to include"),filterStatusCodes:z.string().optional().describe("Comma-separated status codes to exclude"),recursion:z.boolean().optional().describe("Enable recursion"),timeoutSeconds:z.number().int().positive().optional().describe("HTTP timeout in seconds"),rawOptions:z.array(z.string()).optional().describe("Additional safe ffuf options")}).describe("Run ffuf for web content discovery and fuzzing.");server.tool("ffufScan",f.shape,(async({targetUrl:e,wordlist:t,threads:r,matchStatusCodes:o,filterStatusCodes:n,recursion:s,timeoutSeconds:a,rawOptions:i},c)=>{try{const p=[];r&&p.push("-t",String(r)),o&&p.push("-mc",o),n&&p.push("-fc",n),s&&p.push("-recursion"),a&&p.push("-timeout",String(a)),i&&p.push(...i);const d=e.includes("FUZZ")?e:`${e.replace(/\/$/,"")}/FUZZ`,{fullOutput:u,findings:l}=await runFfufScan(d,t,p),h=formatInvocation(c),m=saveEngagementRecord({tool:"ffufScan",category:"finding",target:d,summary:`ffuf produced ${l.length} findings for ${d}`,findings:l,rawOutput:u,invocation:h});return{content:[{type:"text",text:`ffuf produced ${l.length} findings.`},{type:"text",text:`recordId=${m}`},{type:"text",text:h},{type:"text",text:l.join("\n")||"No matches found."},{type:"text",text:`\n--- Full ffuf Output ---\n${u}`}]}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const w=z.object({targets:z.array(z.string()).min(1).describe("List of targets (URLs/domains/IPs)"),templates:z.array(z.string()).optional().describe("Template IDs or directories"),severities:z.array(z.enum(["info","low","medium","high","critical","unknown"])).optional().describe("Severity filters"),tags:z.array(z.string()).optional().describe("Nuclei tag filters"),rateLimit:z.number().int().positive().optional().describe("Requests per second"),headless:z.boolean().optional().describe("Enable headless templates"),rawOptions:z.array(z.string()).optional().describe("Additional safe nuclei options")}).describe("Run nuclei template scanning against target endpoints.");server.tool("nucleiScan",w.shape,(async({targets:e,templates:t,severities:r,tags:o,rateLimit:n,headless:s,rawOptions:a},i)=>{try{const c=[];t&&t.length>0&&t.forEach((e=>c.push("-t",e))),r&&r.length>0&&c.push("-severity",r.join(",")),o&&o.length>0&&c.push("-tags",o.join(",")),n&&c.push("-rl",String(n)),s&&c.push("-headless"),a&&c.push(...a);const{fullOutput:p,findings:d}=await runNucleiScan(e,c),u=formatInvocation(i),l=saveEngagementRecord({tool:"nucleiScan",category:"finding",summary:`Nuclei scan completed for ${e.length} targets`,findings:d,rawOutput:p,invocation:u});return{content:[{type:"text",text:`Nuclei produced ${d.length} findings.`},{type:"text",text:`recordId=${l}`},{type:"text",text:u},{type:"text",text:d.join("\n")||"No findings produced."},{type:"text",text:`\n--- Full Nuclei Output ---\n${p}`}]}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const y=z.object({networkInterface:z.string().describe("Network interface (en0, eth0, wlan0)"),packetCount:z.number().int().positive().describe("Number of packets to capture"),bpfFilter:z.string().optional().describe("Optional BPF filter (simple form, rawOptions for advanced)"),outputPcap:z.string().optional().describe("Optional output pcap path"),rawOptions:z.array(z.string()).optional().describe("Additional safe tcpdump options")}).describe("Capture traffic packets for reconnaissance/evidence collection.");server.tool("trafficCapture",y.shape,(async({networkInterface:e,packetCount:t,bpfFilter:r,outputPcap:o,rawOptions:n},s)=>{try{const{fullOutput:a,findings:i}=await runTrafficCapture(e,t,r,o,n||[]),c=formatInvocation(s);return{content:[{type:"text",text:`Traffic capture complete on ${e}.`},{type:"text",text:`recordId=${saveEngagementRecord({tool:"trafficCapture",category:"sniffing",target:e,summary:`Captured ${i.length} lines on ${e}`,findings:i,rawOutput:a,invocation:c})}`},{type:"text",text:c},{type:"text",text:i.join("\n")||"No packet lines captured in stdout (check pcap output)."},{type:"text",text:`\n--- Full tcpdump Output ---\n${a}`}]}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const b=z.object({target:z.string().describe("Target host/IP"),service:z.string().describe("Service name (ssh, ftp, rdp, smb, http-get, etc.)"),username:z.string().optional().describe("Single username"),password:z.string().optional().describe("Single password"),usernameList:z.string().optional().describe("Path to username list"),passwordList:z.string().optional().describe("Path to password list"),port:z.number().int().positive().optional().describe("Override service port"),tasks:z.number().int().positive().optional().describe("Parallel tasks"),rawOptions:z.array(z.string()).optional().describe("Additional safe hydra options")}).describe("Run brute-force authentication checks with Hydra.");server.tool("hydraBruteforce",b.shape,(async({target:e,service:t,username:r,password:o,usernameList:n,passwordList:s,port:a,tasks:i,rawOptions:c},p)=>{try{const d=(c||[]).some((e=>"-C"===e||e.startsWith("-C")||"-x"===e||e.startsWith("-x"))),u=Number(Boolean(r))+Number(Boolean(n)),l=Number(Boolean(o))+Number(Boolean(s));if(!d){if(1!==u)throw new Error("Provide exactly one of username or usernameList unless using raw credential mode (-C/-x).");if(1!==l)throw new Error("Provide exactly one of password or passwordList unless using raw credential mode (-C/-x).")}const h=[];r&&h.push("-l",r),o&&h.push("-p",o),n&&h.push("-L",n),s&&h.push("-P",s),a&&h.push("-s",String(a)),i&&h.push("-t",String(i)),c&&h.push(...c);const{fullOutput:m,findings:g}=await runHydra(e,t,h),f=formatInvocation(p),w=saveEngagementRecord({tool:"hydraBruteforce",category:"bruteforce",target:e,summary:`Hydra brute-force completed against ${t}://${e}.`,findings:g,rawOutput:m,invocation:f});return{content:[{type:"text",text:`Hydra completed. Potential credential hits: ${g.length}.`},{type:"text",text:`recordId=${w}`},{type:"text",text:f},{type:"text",text:g.join("\n")||"No credential hits detected."},{type:"text",text:`\n--- Full Hydra Output ---\n${m}`}]}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const S=z.object({rawOptions:z.array(z.string()).optional().describe("Optional linpeas flags when linpeas is available")}).describe("Run local privilege-escalation audit checks and summarize potential escalation paths.");server.tool("privEscAudit",S.shape,(async({rawOptions:e},t)=>{try{const{fullOutput:r,findings:o}=await runPrivEscAudit(e||[]),n=formatInvocation(t);return{content:[{type:"text",text:"Privilege escalation audit complete."},{type:"text",text:`recordId=${saveEngagementRecord({tool:"privEscAudit",category:"privilege-escalation",summary:`Privilege escalation audit generated ${o.length} flagged items.`,findings:o,rawOutput:r,invocation:n})}`},{type:"text",text:n},{type:"text",text:o.join("\n")||"No high-signal privilege escalation indicators found."},{type:"text",text:`\n--- PrivEsc Audit Output ---\n${r}`}]}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const x=z.object({targetUrl:z.string().url().describe("Target URL with injectable parameter"),dumpData:z.boolean().optional().describe("Attempt table dump via sqlmap"),risk:z.number().int().min(1).max(3).optional().describe("sqlmap risk level"),level:z.number().int().min(1).max(5).optional().describe("sqlmap level"),rawOptions:z.array(z.string()).optional().describe("Additional safe sqlmap options")}).describe("Run SQL injection extraction workflow with sqlmap.");server.tool("extractionSweep",x.shape,(async({targetUrl:e,dumpData:t,risk:r,level:o,rawOptions:n},s)=>{try{const a=[];t&&a.push("--dump"),r&&a.push("--risk",String(r)),o&&a.push("--level",String(o)),n&&a.push(...n);const{fullOutput:i,findings:c}=await runSqlmap(e,a),p=formatInvocation(s);return{content:[{type:"text",text:"Extraction sweep complete."},{type:"text",text:`recordId=${saveEngagementRecord({tool:"extractionSweep",category:"extraction",target:e,summary:`Extraction sweep completed for ${e}`,findings:c,rawOutput:i,invocation:p})}`},{type:"text",text:p},{type:"text",text:c.join("\n")||"No extraction findings produced."},{type:"text",text:`\n--- Full sqlmap Output ---\n${i}`}]}}catch(e){return{content:[{type:"text",text:`Error: ${e.message}`}],isError:!0}}}));const v=new URL(`http://${serverHost}:${serverPort}/mcp`),$=authEnabled?requireBearerAuth({verifier:tokenVerifier,requiredScopes:authScopes,resourceMetadataUrl:getOAuthProtectedResourceMetadataUrl(v)}):(e,t,r)=>r();switch(transportType){case"stdio":{const e=new StdioServerTransport;await server.connect(e),console.error("Pentest MCP Server connected via stdio."),await new Promise((()=>{}));break}case"http":{if(console.error("Starting HTTP Streamable transport..."),authEnabled&&"bearer"!==authMode)throw new Error(`Unsupported MCP_AUTH_MODE=${authMode}. Only "bearer" is supported.`);if(authEnabled&&!authIntrospectionUrl&&!authJwksUrl)throw new Error("Auth enabled but no token verification configured. Set MCP_OIDC_INTROSPECTION_URL or MCP_OIDC_JWKS_URL.");(process.env.MCP_OAUTH_ENABLED||process.env.MCP_OAUTH_PROVIDER_URL||process.env.MCP_OAUTH_SCOPES)&&console.warn("Legacy MCP_OAUTH_* env vars detected. Prefer MCP_AUTH_* and MCP_OIDC_* vars.");const e=express();e.use(express.json({limit:"10mb"})),authEnabled&&e.use(mcpAuthMetadataRouter({oauthMetadata:buildOauthMetadata(),resourceServerUrl:v,scopesSupported:advertisedScopes,resourceName:"Pentest MCP",serviceDocumentationUrl:new URL("https://github.com/dmontgomery40/pentest-mcp")}));const t=new StreamableHTTPServerTransport({sessionIdGenerator:void 0});await server.connect(t),e.all("/mcp",$,(async(e,r)=>{try{await t.handleRequest(e,r,e.body)}catch(e){console.error("Failed to handle HTTP MCP request:",e),r.headersSent||r.status(500).json({error:e.message||"internal error"})}})),e.get("/health",((e,t)=>{t.json({status:"healthy",transport_mode:"streamable-http",server:"pentest-mcp",version:serverVersion,auth_mode:authEnabled?authMode:"none",deprecated:{sseTransport:!0,legacyOAuthEnvAliases:!0}})})),e.listen(serverPort,serverHost,(()=>{console.error(`MCP Streamable HTTP server running at http://${serverHost}:${serverPort}/mcp`),console.error(`Health check at http://${serverHost}:${serverPort}/health`)}));break}case"sse":{console.error("Starting deprecated SSE transport compatibility mode...");const e=express();e.use(express.json({limit:"10mb"}));let t=null,r=!1;e.get("/sse",$,(async(e,o)=>{o.setHeader("Warning",'299 - "SSE transport is deprecated; use MCP_TRANSPORT=http"'),r?o.status(409).json({error:"SSE transport already connected."}):(t=new SSEServerTransport("/messages",o),await server.connect(t),t.onclose=()=>{r=!1,t=null},r=!0)})),e.post("/messages",$,(async(e,r)=>{r.setHeader("Warning",'299 - "SSE transport is deprecated; use MCP_TRANSPORT=http"'),t?"function"!=typeof t.handlePostMessage?r.status(202).json({success:!0}):await t.handlePostMessage(e,r):r.status(503).json({error:"SSE transport not initialized"})})),e.get("/health",((e,t)=>{t.json({status:"healthy",transport_mode:"sse",server:"pentest-mcp",version:serverVersion,deprecated:!0,auth_mode:authEnabled?authMode:"none"})})),e.listen(serverPort,serverHost,(()=>{console.error(`MCP SSE compatibility server running at http://${serverHost}:${serverPort}/sse`),console.error(`Messages endpoint at http://${serverHost}:${serverPort}/messages`),console.error("SSE is deprecated; migrate to MCP_TRANSPORT=http.")}));break}default:console.error(`Unknown transport type: ${transportType}`),console.error("Valid options are: stdio, http, sse"),process.exit(1)}console.error("Pentest MCP Server initialized.")}"inspector"===process.argv[2]?.toLowerCase()?launchBundledInspector(process.argv.slice(3)).then((e=>process.exit(e))).catch((e=>{console.error("Failed to launch bundled MCP Inspector:",e),process.exit(1)})):main().catch((e=>{console.error("Unhandled error in main:",e),process.exit(1)}));
//# sourceMappingURL=/sm/730b23caa0eda6644af65e8482b234d2abaeceb4472b10de8a56cb3a14a8117c.map