Skip to main content
Glama

generate_pwd_wordlist

Generate custom password wordlists for penetration testing when standard dictionaries fail, using provided seed words to create targeted combinations for security assessments.

Instructions

generate some password wordlist based on initial input of word(s), let this be the last resort if no other wordlist worked

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
inpYes

Implementation Reference

  • Handler function for the 'generate_pwd_wordlist' tool, decorated with @mcp.tool for registration. It invokes a subprocess running psudohash.py to generate password variations based on input words (with common paddings after, length 10-13, max 2 words combined), saving to project directory.
    @mcp.tool(name="generate_pwd_wordlist",description="generate some password wordlist based on initial input of word(s), let this be the last resort if no other wordlist worked") def generate_pwd_wordlist(inp:List[str]): return run_command([ "python", str(Path(__file__).parent.resolve()/"psudohash/psudohash.py"), "-w", ",".join(inp), "-cpa", "-q", "-o", config.PROJECT_DIRECTORY+"/passwords/"+inp[0].strip()+".txt", "--minlen", "10", "--maxlen", "13", "--max-combine", "2"], communicate=True)
  • Supporting Python script 'psudohash.py' that performs the actual password wordlist generation through various mutations (case, substitutions, paddings, etc.) based on keyword inputs. Invoked by the handler with specific parameters for length-constrained, padded combinations.
    #!/bin/python3 # # Author: Panagiotis Chartas (t3l3machus) # https://github.com/t3l3machus from pathlib import Path import argparse, sys, itertools from tqdm import tqdm # Colors MAIN = '\033[38;5;50m' LOGO = '\033[38;5;41m' LOGO2 = '\033[38;5;42m' GREEN = '\033[38;5;82m' ORANGE = '\033[0;38;5;214m' PRPL = '\033[0;38;5;26m' PRPL2 = '\033[0;38;5;25m' RED = '\033[1;31m' END = '\033[0m' BOLD = '\033[1m' # -------------- Arguments & Usage -------------- # parser = argparse.ArgumentParser( formatter_class=argparse.RawTextHelpFormatter, epilog=''' Usage examples: # 1) No multi-word: treat each keyword separately python3 psudohash.py -w foo,bar,baz -cpa # → foo, bar, baz # 2) In-order joins (-i): up to 2 words by default python3 psudohash.py -w foo,bar,baz -i # → foo, bar, baz, foobar, foobaz, barbaz # 3) All-order combinations (-c): up to 2 words by default python3 psudohash.py -w foo,bar,baz -c # → foo, bar, baz, foobar, foobaz, barfoo, barbaz, bazfoo, bazbar # 4) Change separator between joined words python3 psudohash.py -w foo,bar,baz -i --sep "_" # → foo, bar, baz, foo_bar, foo_baz, bar_baz, foo_bar_baz # 5) Combine up to 3 words (instead of default 2) python3 psudohash.py -w foo,bar,baz -i --max-combine 3 # → foo, bar, baz, foobar, foobaz, barbaz, foobarbaz, ... ''' ) parser.add_argument("-w", "--words", action="store", help = "Comma seperated keywords to mutate", required = True) parser.add_argument("-i", "--inorder", action="store_true", help="Join keywords only in the order given: for each 1≤r≤max_combine, concatenate each r-subset in original sequence.") parser.add_argument("-c", "--combinations", action="store_true", help="Generate every ordering of every subset (up to max_combine) of the provided keywords.") parser.add_argument("--max-combine", type=int, default=2, help="Maximum number of raw keywords to join into one base string (default: 2). Applies when using -i or -c.") parser.add_argument("--minlen", type=int, help="Minimum length (inclusive) of any resulting password. Mutations shorter than this are skipped.") parser.add_argument("--maxlen", type=int, help="Maximum length (inclusive) of any resulting password. Mutations longer than this are skipped.") parser.add_argument("--sep", type=str, default="", help="Separator to insert between joined keywords (default: no separator).") parser.add_argument("-an", "--append-numbering", action="store", help = "Append numbering range at the end of each word mutation (before appending year or common paddings).\nThe LEVEL value represents the minimum number of digits. LEVEL must be >= 1. \nSet to 1 will append range: 1,2,3..100\nSet to 2 will append range: 01,02,03..100 + previous\nSet to 3 will append range: 001,002,003..100 + previous.\n\n", type = int, metavar='LEVEL') parser.add_argument("-nl", "--numbering-limit", action="store", help = "Change max numbering limit value of option -an. Default is 50. Must be used with -an.", type = int, metavar='LIMIT') parser.add_argument("-y", "--years", action="store", help = "Singe OR comma seperated OR range of years to be appended to each word mutation (Example: 2022 OR 1990,2017,2022 OR 1990-2000)") parser.add_argument("-ap", "--append-padding", action="store", help = "Add comma seperated values to common paddings (must be used with -cpb OR -cpa)", metavar='VALUES') parser.add_argument("-cpb", "--common-paddings-before", action="store_true", help = "Append common paddings before each mutated word") parser.add_argument("-cpa", "--common-paddings-after", action="store_true", help = "Append common paddings after each mutated word") parser.add_argument("-cpo", "--custom-paddings-only", action="store_true", help = "Use only user provided paddings for word mutations (must be used with -ap AND (-cpb OR -cpa))") parser.add_argument("-o", "--output", action="store", help = "Output filename (default: output.txt)", metavar='FILENAME') parser.add_argument("-q", "--quiet", action="store_true", help = "Do not print the banner on startup") args = parser.parse_args() def exit_with_msg(msg): parser.print_help() print(f'\n[{RED}Debug{END}] {msg}\n') sys.exit(1) def unique(l): unique_list = [] for i in l: if i not in unique_list: unique_list.append(i) return unique_list # Append numbering if args.numbering_limit and not args.append_numbering: exit_with_msg('Option -nl must be used with -an.') if args.append_numbering: if args.append_numbering <= 0: exit_with_msg('Numbering level must be > 0.') _max = args.numbering_limit + 1 if args.numbering_limit and isinstance(args.numbering_limit, int) else 51 # Create years list if args.years: years = [] if args.years.count(',') == 0 and args.years.count('-') == 0 and args.years.isdecimal() and int(args.years) >= 1000 and int(args.years) <= 3200: years.append(str(args.years)) elif args.years.count(',') > 0: for year in args.years.split(','): if year.strip() != '' and year.isdecimal() and int(year) >= 1000 and int(year) <= 3200: years.append(year) else: exit_with_msg('Illegal year(s) input. Acceptable years range: 1000 - 3200.') elif args.years.count('-') == 1: years_range = args.years.split('-') start_year = years_range[0] end_year = years_range[1] if (start_year.isdecimal() and int(start_year) < int(end_year) and int(start_year) >= 1000) and (end_year.isdecimal() and int(end_year) <= 3200): for y in range(int(years_range[0]), int(years_range[1])+1): years.append(str(y)) else: exit_with_msg('Illegal year(s) input. Acceptable years range: 1000 - 3200.') else: exit_with_msg('Illegal year(s) input. Acceptable years range: 1000 - 3200.') def banner(): padding = ' ' P = [[' ', '┌', '─', '┐'], [' ', '├','─','┘'], [' ', '┴',' ',' ']] S = [[' ', '┌','─','┐'], [' ', '└','─','┐'], [' ', '└','─','┘']] U = [[' ', '┬',' ','┬'], [' ', '│',' ','│'], [' ', '└','─','┘']] D = [[' ', '┌','┬','┐'], [' ', ' ','│','│'], [' ', '─','┴','┘']] O = [[' ', '┌','─','┐'], [' ', '│',' ','│'], [' ', '└','─','┘']] H = [[' ', '┐', ' ', '┌'], [' ', '├','╫','┤'], [' ', '┘',' ','└']] A = [[' ', '┌','─','┐'], [' ', '├','─','┤'], [' ', '┴',' ','┴']] S = [[' ', '┌','─','┐'], [' ', '└','─','┐'], [' ', '└','─','┘']] H = [[' ', '┬',' ','┬'], [' ', '├','─','┤'], [' ', '┴',' ','┴']] banner = [P,S,U,D,O,H,A,S,H] final = [] print('\r') init_color = 37 txt_color = init_color cl = 0 for charset in range(0, 3): for pos in range(0, len(banner)): for i in range(0, len(banner[pos][charset])): clr = f'\033[38;5;{txt_color}m' char = f'{clr}{banner[pos][charset][i]}' final.append(char) cl += 1 txt_color = txt_color + 36 if cl <= 3 else txt_color cl = 0 txt_color = init_color init_color += 31 if charset < 2: final.append('\n ') print(f" {''.join(final)}") print(f'{END}{padding} by t3l3machus\n') # ----------------( Base Settings )---------------- # mutations_cage = [] basic_mutations = [] outfile = args.output if args.output else 'output.txt' trans_keys = [] transformations = [ {'a' : ['@', '4']}, {'b' : '8'}, {'e' : '3'}, {'g' : ['9', '6']}, {'i' : ['1', '!']}, {'o' : '0'}, {'s' : ['$', '5']}, {'t' : '7'} ] for t in transformations: for key in t.keys(): trans_keys.append(key) # Common Padding Values if (args.custom_paddings_only or args.append_padding) and not (args.common_paddings_before or args.common_paddings_after): exit_with_msg('Options -ap and -cpo must be used with -cpa or -cpb.') elif (args.common_paddings_before or args.common_paddings_after) and not args.custom_paddings_only: try: f = open(Path(__file__).parent/'common_padding_values.txt', 'r') content = f.readlines() common_paddings = [val.strip() for val in content] f.close() except: exit_with_msg('File "common_padding_values.txt" not found.') elif (args.common_paddings_before or args.common_paddings_after) and (args.custom_paddings_only and args.append_padding): common_paddings = [] elif not (args.common_paddings_before or args.common_paddings_after): common_paddings = [] else: exit_with_msg('\nIllegal padding settings.\n') if args.append_padding: for val in args.append_padding.split(','): if val.strip() != '' and val not in common_paddings: common_paddings.append(val) if (args.common_paddings_before or args.common_paddings_after): common_paddings = list(set(common_paddings)) # ----------------( Functions )---------------- # # The following list is used to create variations of password values and appended years. # For example, a passwd value {passwd} will be mutated to "{passwd}{seperator}{year}" # for each of the symbols included in the list below. year_seperators = ['', '_', '-', '@'] # ----------------( Functions )---------------- # def evalTransformations(w): trans_chars = [] total = 1 c = 0 w = list(w) for char in w: for t in transformations: if char in t.keys(): trans_chars.append(c) if isinstance(t[char], list): total *= 3 else: total *= 2 c += 1 return [trans_chars, total] def mutate(tc, word): global trans_keys, mutations_cage, basic_mutations i = trans_keys.index(word[tc].lower()) trans = transformations[i][word[tc].lower()] limit = len(trans) * len(mutations_cage) c = 0 for m in mutations_cage: w = list(m) if isinstance(trans, list): for tt in trans: w[tc] = tt transformed = ''.join(w) mutations_cage.append(transformed) c += 1 else: w[tc] = trans transformed = ''.join(w) mutations_cage.append(transformed) c += 1 if limit == c: break return mutations_cage def mutations_handler(kword, trans_chars, total): """ Perform character→symbol/number substitutions and write each new mutation. """ global mutations_cage, basic_mutations container = [] for word in basic_mutations: mutations_cage = [word.strip()] for tc in trans_chars: results = mutate(tc, kword) container.append(results) for m_set in container: for m in m_set: basic_mutations.append(m) basic_mutations = list(set(basic_mutations)) desc = " ├─ Mutating word based on commonly used char‐to‐symbol/number substitutions" with open(outfile, 'a') as wordlist, \ tqdm(total=len(basic_mutations), desc=desc, leave=False) as pbar: for m in basic_mutations: # Only final‐filter if no numbering/years/padding follow if not args.append_numbering and not args.years and not (args.common_paddings_after or args.common_paddings_before): if not (args.minlen and len(m) < args.minlen) \ and not (args.maxlen and len(m) > args.maxlen): wordlist.write(m + '\n') else: wordlist.write(m + '\n') pbar.update(1) print(f"{desc}... [100.0%]") def mutateCase(word): trans = list(map(''.join, itertools.product(*zip(word.upper(), word.lower())))) return trans def caseMutationsHandler(word, mutability): """ Generate all upper/lower combos, add to basic_mutations, and write to file immediately if mutability is False. """ global basic_mutations case_mutations = mutateCase(word) desc = " ├─ Producing character case‐based transformations" with open(outfile, 'a') as wordlist, \ tqdm(total=len(case_mutations), desc=desc, leave=False) as pbar: for m in case_mutations: basic_mutations.append(m) if not mutability: # Only final‐filter if no substitutions/numbering/years/padding follow if not args.combinations and not args.inorder \ and not args.append_numbering and not args.years \ and not (args.common_paddings_after or args.common_paddings_before): if not (args.minlen and len(m) < args.minlen) \ and not (args.maxlen and len(m) > args.maxlen): wordlist.write(m + '\n') else: wordlist.write(m + '\n') pbar.update(1) print(f"{desc}... [100.0%]") def append_numbering(): """ For each word in basic_mutations, append numbering variants (zfilled up to LEVEL). """ global _max, basic_mutations lvl = args.append_numbering first_cycle = True previous_list = [] # total lines = len(basic_mutations) * lvl * (_max - 1) * 2 total_lines = len(basic_mutations) * lvl * (_max - 1) * 2 desc = " ├─ Appending numbering to each word mutation" with open(outfile, 'a') as wordlist, \ tqdm(total=total_lines, desc=desc, leave=False) as pbar: for word in basic_mutations: for i in range(1, lvl + 1): for k in range(1, _max): num_z = str(k).zfill(i) line1 = f"{word}{num_z}\n" line2 = f"{word}_{num_z}\n" if first_cycle: # Only final‐filter if no years or padding follow if not args.years and not (args.common_paddings_after or args.common_paddings_before): if not (args.minlen and len(line1) < args.minlen) \ and not (args.maxlen and len(line1) > args.maxlen): wordlist.write(line1) pbar.update(1) else: pbar.update(1) if not (args.minlen and len(line2) < args.minlen) \ and not (args.maxlen and len(line2) > args.maxlen): wordlist.write(line2) pbar.update(1) else: pbar.update(1) else: if not args.years and not (args.common_paddings_after or args.common_paddings_before): if not (args.minlen and len(line1) < args.minlen) \ and not (args.maxlen and len(line1) > args.maxlen): wordlist.write(line1) pbar.update(1) else: pbar.update(1) if not (args.minlen and len(line2) < args.minlen) \ and not (args.maxlen and len(line2) > args.maxlen): wordlist.write(line2) pbar.update(1) else: pbar.update(1) else: wordlist.write(line1) wordlist.write(line2) pbar.update(2) previous_list.append(f"{word}{num_z}") else: if previous_list[k - 1] != f"{word}{num_z}": wordlist.write(line1) wordlist.write(line2) previous_list[k - 1] = f"{word}{num_z}" pbar.update(2) first_cycle = False print(f"{desc}... [100.0%]") def mutate_years(): """ For each entry in basic_mutations, append year variants (full YYYY + short YY). """ global basic_mutations current_mutations = basic_mutations.copy() # total lines = len(current_mutations) * len(years) * len(year_seperators) * 2 total_lines = len(current_mutations) * len(years) * len(year_seperators) * 2 desc = " ├─ Appending year patterns after each word mutation" with open(outfile, 'a') as wordlist, \ tqdm(total=total_lines, desc=desc, leave=False) as pbar: for word in current_mutations: for y in years: for sep in year_seperators: full = f"{word}{sep}{y}\n" short = f"{word}{sep}{y[2:]}\n" # Only final-filter if no padding follows if not (args.common_paddings_after or args.common_paddings_before): if not (args.minlen and len(full) < args.minlen) \ and not (args.maxlen and len(full) > args.maxlen): wordlist.write(full) basic_mutations.append(full.strip()) # count one for pbar regardless pbar.update(1) if not (args.minlen and len(short) < args.minlen) \ and not (args.maxlen and len(short) > args.maxlen): wordlist.write(short) basic_mutations.append(short.strip()) pbar.update(1) else: wordlist.write(full) basic_mutations.append(full.strip()) wordlist.write(short) basic_mutations.append(short.strip()) pbar.update(2) print(f"{desc}... [100.0%]") def check_underscore(word, pos): if word[pos] == '_': return True else: return False def append_paddings_before(): """ Prepend each common_paddings value before each word in basic_mutations. """ global basic_mutations current_mutations = basic_mutations.copy() # total lines = sum(len(common_paddings)*2 for each word) total_lines = sum(len(common_paddings) * 2 for _ in current_mutations) desc = " ├─ Appending common paddings before each word mutation" with open(outfile, 'a') as wordlist, \ tqdm(total=total_lines, desc=desc, leave=False) as pbar: for word in current_mutations: for val in common_paddings: line1 = f"{val}{word}\n" wordlist.write(line1) pbar.update(1) if not check_underscore(val, -1): line2 = f"{val}_{word}\n" wordlist.write(line2) pbar.update(1) print(f"{desc}... [100.0%]") def append_paddings_after(): """ Append each common_paddings value after each word in basic_mutations. """ global basic_mutations current_mutations = basic_mutations.copy() # total lines = sum(len(common_paddings)*2 for each word) total_lines = sum(len(common_paddings) * 2 for _ in current_mutations) desc = " ├─ Appending common paddings after each word mutation" with open(outfile, 'a') as wordlist, \ tqdm(total=total_lines, desc=desc, leave=False) as pbar: for word in current_mutations: for val in common_paddings: line1 = f"{word}{val}\n" # Only final-filter if no before-padding if not args.common_paddings_before: if not (args.minlen and len(line1) < args.minlen) \ and not (args.maxlen and len(line1) > args.maxlen): wordlist.write(line1) pbar.update(1) else: pbar.update(1) else: wordlist.write(line1) pbar.update(1) if not check_underscore(val, 0): line2 = f"{word}_{val}\n" if not args.common_paddings_before: if not (args.minlen and len(line2) < args.minlen) \ and not (args.maxlen and len(line2) > args.maxlen): wordlist.write(line2) pbar.update(1) else: pbar.update(1) else: wordlist.write(line2) pbar.update(1) print(f"{desc}... [100.0%]") def calculate_output(keyw): global trans_keys c = 0 total = 1 basic_total = 1 basic_size = 0 size = 0 numbering_count = 0 numbering_size = 0 # Basic mutations calc for char in keyw: if char in trans_keys: i = trans_keys.index(keyw[c].lower()) trans = transformations[i][keyw[c].lower()] basic_total *= (len(trans) + 2) else: basic_total = basic_total * 2 if char.isalpha() else basic_total c += 1 total = basic_total basic_size = total * (len(keyw) + 1) size = basic_size # Words numbering mutations calc if args.append_numbering: global _max word_len = len(keyw) + 1 first_cycle = True previous_list = [] lvl = args.append_numbering for w in range(0, total): for i in range(1, lvl+1): for k in range(1, _max): n = str(k).zfill(i) if first_cycle: numbering_count += 2 numbering_size += (word_len * 2) + (len(n) * 2) + 1 previous_list.append(f'{w}{n}') else: if previous_list[k - 1] != f'{w}{n}': numbering_size += (word_len * 2) + (len(n) * 2) + 1 numbering_count += 2 previous_list[k - 1] = f'{w}{n}' first_cycle = False del previous_list # Adding years mutations calc if args.years: patterns = len(year_seperators) * 2 year_chars = 4 year_short = 2 years_len = len(years) size += (basic_size * patterns * years_len) for sep in year_seperators: size += (basic_total * (year_chars + len(sep)) * years_len) size += (basic_total * (year_short + len(sep)) * years_len) total += total * len(years) * patterns basic_total = total basic_size = size # Common paddings mutations calc patterns = 2 if args.common_paddings_after or args.common_paddings_before: paddings_len = len(common_paddings) pads_wlen_sum = sum([basic_total*len(w) for w in common_paddings]) _pads_wlen_sum = sum([basic_total*(len(w)+1) for w in common_paddings]) if args.common_paddings_after and args.common_paddings_before: size += ((basic_size * patterns * paddings_len) + pads_wlen_sum + _pads_wlen_sum) * 2 total += (total * len(common_paddings) * 2) * 2 elif args.common_paddings_after or args.common_paddings_before: size += (basic_size * patterns * paddings_len) + pads_wlen_sum + _pads_wlen_sum total += total * len(common_paddings) * 2 return [total + numbering_count, size + numbering_size] def check_mutability(word): global trans_keys m = 0 for char in word: if char in trans_keys: m += 1 return m def chill(): pass def main(): banner() if not args.quiet else chill() global basic_mutations, mutations_cage # 1) Read raw keywords (ignore empty or digit-only) raw = [] for w in args.words.split(','): w = w.strip() if not w: continue if w.isdecimal(): exit_with_msg('Unable to mutate digit-only keywords.') raw.append(w) # 2) Build "base" keywords according to flags: # - If --inorder: join each r-subset in the given order, for r=1..max_combine. # - Elif --combinations: for each r-subset (1..max_combine), generate every permutation. # - Else: treat each raw word on its own. from itertools import combinations, permutations keywords = [] limit = min(len(raw), args.max_combine) if args.inorder: for r in range(1, limit + 1): for combo in combinations(raw, r): keywords.append(args.sep.join(combo)) elif args.combinations: for r in range(1, limit + 1): for combo in combinations(raw, r): for perm in permutations(combo): keywords.append(args.sep.join(perm)) else: keywords = list(raw) # Calculate total words and size of output total_size = [0, 0] for keyw in keywords: count_size = calculate_output(keyw.strip().lower()) total_size[0] += count_size[0] total_size[1] += count_size[1] size = round(((total_size[1]/1000)/1000), 1) if total_size[1] > 100000 else total_size[1] prefix = 'bytes' if total_size[1] <= 100000 else 'MB' fsize = f'{size} {prefix}' print(f'[{MAIN}Info{END}] Calculating output length and size...') # Inform user about the output size if args.minlen or args.maxlen: prompt = (f'[{ORANGE}Warning{END}] Exact final size cannot be determined because min/max-length filtering is active. Without filtering, this would produce {BOLD}{total_size[0]}{END} words, {BOLD}{fsize}{END}. Continue? [y/n]: ') else: prompt = (f'[{ORANGE}Warning{END}] This operation will produce {BOLD}{total_size[0]}{END} words, {BOLD}{fsize}{END}. Are you sure you want to proceed? [y/n]: ') try: concent = input(prompt) except KeyboardInterrupt: exit('\n') if concent.lower() not in ['y', 'yes']: sys.exit(f'\n[{RED}X{END}] Aborting.') else: # ensure parent directory exists (works fine if outfile has no directory component) out_path = Path(outfile) out_path.parent.mkdir(parents=True, exist_ok=True) # create or truncate the file out_path.open("w", encoding="utf-8").close() for word in keywords: print(f'[{GREEN}*{END}] Mutating keyword: {GREEN}{word}{END} ') mutability = check_mutability(word.lower()) # Stage 1: Case mutations caseMutationsHandler(word.lower(), mutability) # Stage 2: Substitution mutations if mutability: trans = evalTransformations(word.lower()) mutations_handler(word, trans[0], trans[1]) else: print(f" ├─ {ORANGE}No character substitution instructions match this word.{END}") # Stage 3: Numbering if args.append_numbering: append_numbering() # Stage 4: Years if args.years: mutate_years() # Stage 5: Common paddings after if args.common_paddings_after or args.custom_paddings_only: append_paddings_after() # Stage 6: Common paddings before if args.common_paddings_before: append_paddings_before() basic_mutations = [] mutations_cage = [] print(f' └─ Done!') print(f'\n[{MAIN}Info{END}] Completed! List saved in {outfile}\n') if __name__ == '__main__': main()
  • General helper function 'run_command' used by the tool handler to execute the psudohash.py subprocess interactively.
    def run_command(command: List[str], timeout: int = 150,communicate:bool=False) -> Dict[str, Union[str, int, bool]]: try: logger.info(f"communicate :{communicate}") logger.info(f"Running command: {' '.join(command)}") if communicate: # Use Popen for interactive communication process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, text=True ) stdout, stderr = process.communicate(input="y\n", timeout=timeout) returncode = process.returncode else: # Use run for non-interactive commands result = subprocess.run( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True, timeout=timeout ) stdout = result.stdout stderr = result.stderr returncode = result.returncode logger.info(f"Command completed with return code {returncode}") return { "success": True, "stdout": stdout, "stderr": stderr, "returncode": returncode } except subprocess.CalledProcessError as e: logger.error(f"Command failed with return code {e.returncode}: {e.stderr}") return { "success": False, "stdout": e.stdout, "stderr": e.stderr, "returncode": e.returncode, "error": f"Command failed with return code {e.returncode}" } except subprocess.TimeoutExpired as e: logger.error(f"Command timed out after {timeout} seconds") return { "success": False, "error": f"Command timed out after {timeout} seconds" } except Exception as e: logger.error(f"Error running command: {str(e)}") return { "success": False, "error": str(e) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/YoussefSahnoun/PentestMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server