@ebowwa/mcp-nm 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +218 -15
  2. package/package.json +1 -1
  3. package/src/index.ts +286 -21
package/dist/index.js CHANGED
@@ -13965,32 +13965,100 @@ async function handleXxdExtract(args) {
13965
13965
  return { content: [{ type: "text", text: summary }] };
13966
13966
  }
13967
13967
  async function handleXxdFindPattern(args) {
13968
- const result = await runXxd(args.filePath, {
13969
- plainHex: true,
13970
- length: args.maxLength
13971
- });
13972
- const fileHex = result.output.replace(/\s/g, "").toLowerCase();
13973
13968
  let searchPattern;
13974
13969
  if (args.patternFormat === "text" || !args.patternFormat) {
13975
13970
  searchPattern = args.pattern.split("").map((c) => c.charCodeAt(0).toString(16).padStart(2, "0")).join("");
13976
13971
  } else {
13977
13972
  searchPattern = args.pattern.replace(/\s/g, "").toLowerCase();
13978
13973
  }
13974
+ const { stdout: sizeStr } = await execAsync(`stat -f%z "${args.filePath}" 2>/dev/null || stat -c%s "${args.filePath}"`);
13975
+ const fileSize = parseInt(sizeStr.trim(), 10);
13976
+ const maxInMemory = args.maxLength ?? 50 * 1024 * 1024;
13979
13977
  const matches = [];
13980
- let idx = 0;
13981
- while ((idx = fileHex.indexOf(searchPattern, idx)) !== -1) {
13982
- const byteOffset = Math.floor(idx / 2);
13983
- const contextStart = Math.max(0, idx - 20);
13984
- const contextEnd = Math.min(fileHex.length, idx + searchPattern.length + 20);
13985
- const contextHex = fileHex.slice(contextStart, contextEnd);
13986
- matches.push({
13987
- offset: byteOffset,
13988
- context: contextHex.match(/.{1,2}/g)?.join(" ") || contextHex
13978
+ if (fileSize <= maxInMemory) {
13979
+ const result = await runXxd(args.filePath, {
13980
+ plainHex: true,
13981
+ length: args.maxLength
13989
13982
  });
13990
- idx++;
13983
+ const fileHex = result.output.replace(/\s/g, "").toLowerCase();
13984
+ let idx = 0;
13985
+ while ((idx = fileHex.indexOf(searchPattern, idx)) !== -1) {
13986
+ const byteOffset = Math.floor(idx / 2);
13987
+ const contextStart = Math.max(0, idx - 20);
13988
+ const contextEnd = Math.min(fileHex.length, idx + searchPattern.length + 20);
13989
+ const contextHex = fileHex.slice(contextStart, contextEnd);
13990
+ matches.push({
13991
+ offset: byteOffset,
13992
+ context: contextHex.match(/.{1,2}/g)?.join(" ") || contextHex
13993
+ });
13994
+ idx++;
13995
+ if (matches.length >= 1000)
13996
+ break;
13997
+ }
13998
+ } else {
13999
+ try {
14000
+ const patternBytes = searchPattern.match(/.{2}/g) ?? [];
14001
+ const escapedPattern = patternBytes.map((b) => `\\x${b}`).join("");
14002
+ const { stdout: grepResult } = await execAsync(`grep -abo "${escapedPattern}" "${args.filePath}" 2>/dev/null | head -1000`, { maxBuffer: 10 * 1024 * 1024 });
14003
+ const lines = grepResult.trim().split(`
14004
+ `).filter(Boolean);
14005
+ for (const line of lines) {
14006
+ const match = line.match(/^(\d+):/);
14007
+ if (match) {
14008
+ const offset = parseInt(match[1], 10);
14009
+ try {
14010
+ const contextOffset = Math.max(0, offset - 10);
14011
+ const contextLen = searchPattern.length / 2 + 20;
14012
+ const { stdout: contextHex } = await execAsync(`dd if="${args.filePath}" bs=1 skip=${contextOffset} count=${contextLen} 2>/dev/null | xxd -p | tr -d '\\n'`);
14013
+ matches.push({
14014
+ offset,
14015
+ context: contextHex.match(/.{1,2}/g)?.join(" ") || contextHex
14016
+ });
14017
+ } catch {
14018
+ matches.push({ offset, context: "(context unavailable)" });
14019
+ }
14020
+ }
14021
+ }
14022
+ } catch {
14023
+ try {
14024
+ const { stdout: stringsResult } = await execAsync(`strings -t x -n ${Math.floor(searchPattern.length / 2)} "${args.filePath}" | grep -i "${args.pattern}" | head -1000`, { maxBuffer: 10 * 1024 * 1024 });
14025
+ const lines = stringsResult.trim().split(`
14026
+ `).filter(Boolean);
14027
+ for (const line of lines) {
14028
+ const match = line.match(/^\s*([0-9a-fA-F]+)\s+(.+)$/);
14029
+ if (match) {
14030
+ matches.push({
14031
+ offset: parseInt(match[1], 16),
14032
+ context: match[2].slice(0, 40)
14033
+ });
14034
+ }
14035
+ }
14036
+ } catch {
14037
+ const chunkSize = 10 * 1024 * 1024;
14038
+ for (let chunkStart = 0;chunkStart < fileSize; chunkStart += chunkSize) {
14039
+ try {
14040
+ const { stdout: chunkHex } = await execAsync(`dd if="${args.filePath}" bs=1 skip=${chunkStart} count=${chunkSize} 2>/dev/null | xxd -p | tr -d '\\n'`);
14041
+ const hex = chunkHex.toLowerCase();
14042
+ let idx = 0;
14043
+ while ((idx = hex.indexOf(searchPattern, idx)) !== -1) {
14044
+ matches.push({
14045
+ offset: chunkStart + Math.floor(idx / 2),
14046
+ context: hex.slice(Math.max(0, idx - 20), idx + searchPattern.length + 20).match(/.{1,2}/g)?.join(" ") || ""
14047
+ });
14048
+ idx++;
14049
+ if (matches.length >= 1000)
14050
+ break;
14051
+ }
14052
+ if (matches.length >= 1000)
14053
+ break;
14054
+ } catch {}
14055
+ }
14056
+ }
14057
+ }
13991
14058
  }
13992
14059
  const summary = [
13993
14060
  `Pattern search in: ${args.filePath}`,
14061
+ `File size: ${(fileSize / 1024 / 1024).toFixed(2)} MB`,
13994
14062
  `Pattern: ${args.pattern} (${args.patternFormat || "text"})`,
13995
14063
  `Hex pattern: ${searchPattern}`,
13996
14064
  `Matches found: ${matches.length}`,
@@ -14001,6 +14069,112 @@ async function handleXxdFindPattern(args) {
14001
14069
  `);
14002
14070
  return { content: [{ type: "text", text: summary }] };
14003
14071
  }
14072
+ async function handleConvertNumber(args) {
14073
+ let num;
14074
+ const value = args.value.trim().toLowerCase();
14075
+ try {
14076
+ switch (args.fromFormat) {
14077
+ case "hex":
14078
+ num = BigInt(value.startsWith("0x") ? value : `0x${value}`);
14079
+ break;
14080
+ case "decimal":
14081
+ num = BigInt(value);
14082
+ break;
14083
+ case "binary":
14084
+ num = BigInt(value.startsWith("0b") ? value.slice(2) : value, 2);
14085
+ break;
14086
+ case "octal":
14087
+ num = BigInt(value.startsWith("0o") ? value.slice(2) : value, 8);
14088
+ break;
14089
+ case "auto":
14090
+ default:
14091
+ if (value.startsWith("0x") || /^[0-9a-f]+$/i.test(value) && value.length > 4) {
14092
+ num = BigInt(value.startsWith("0x") ? value : `0x${value}`);
14093
+ } else if (value.startsWith("0b")) {
14094
+ num = BigInt(value.slice(2), 2);
14095
+ } else if (value.startsWith("0o")) {
14096
+ num = BigInt(value.slice(2), 8);
14097
+ } else {
14098
+ num = BigInt(value);
14099
+ }
14100
+ break;
14101
+ }
14102
+ } catch {
14103
+ return {
14104
+ content: [{ type: "text", text: `Error: Invalid number format "${args.value}"` }],
14105
+ isError: true
14106
+ };
14107
+ }
14108
+ let signedNum = num;
14109
+ if (args.signed && args.byteSize) {
14110
+ const bits = args.byteSize;
14111
+ const maxUnsigned = (1n << BigInt(bits)) - 1n;
14112
+ const signBit = 1n << BigInt(bits - 1);
14113
+ if (num & signBit) {
14114
+ signedNum = num - (1n << BigInt(bits));
14115
+ }
14116
+ }
14117
+ const toFormat = args.toFormat ?? "all";
14118
+ const results = [];
14119
+ if (toFormat === "all" || toFormat === "decimal") {
14120
+ results.push(`Decimal: ${num.toString()}`);
14121
+ if (args.signed && args.byteSize && signedNum !== num) {
14122
+ results.push(`Signed: ${signedNum.toString()}`);
14123
+ }
14124
+ }
14125
+ if (toFormat === "all" || toFormat === "hex") {
14126
+ const hexStr = num.toString(16).toUpperCase();
14127
+ const paddedHex = args.byteSize ? hexStr.padStart(args.byteSize / 4, "0") : hexStr;
14128
+ results.push(`Hex: 0x${paddedHex}`);
14129
+ }
14130
+ if (toFormat === "all" || toFormat === "binary") {
14131
+ const binStr = num.toString(2);
14132
+ const paddedBin = args.byteSize ? binStr.padStart(args.byteSize, "0") : binStr;
14133
+ const formattedBin = paddedBin.match(/.{1,8}/g)?.join(" ") ?? paddedBin;
14134
+ results.push(`Binary: 0b${formattedBin}`);
14135
+ }
14136
+ if (toFormat === "all" || toFormat === "octal") {
14137
+ results.push(`Octal: 0o${num.toString(8)}`);
14138
+ }
14139
+ if (toFormat === "all" || toFormat === "ascii") {
14140
+ let asciiStr = "";
14141
+ let tempNum = num;
14142
+ while (tempNum > 0n) {
14143
+ const charCode = Number(tempNum & 0xffn);
14144
+ if (charCode >= 32 && charCode <= 126) {
14145
+ asciiStr = String.fromCharCode(charCode) + asciiStr;
14146
+ } else {
14147
+ asciiStr = `\\x${charCode.toString(16).padStart(2, "0")}` + asciiStr;
14148
+ }
14149
+ tempNum = tempNum >> 8n;
14150
+ }
14151
+ results.push(`ASCII: ${asciiStr || "(non-printable)"}`);
14152
+ }
14153
+ if (args.byteSize) {
14154
+ results.push("");
14155
+ results.push(`Size: ${args.byteSize}-bit (${args.byteSize / 8} bytes)`);
14156
+ }
14157
+ if (toFormat === "all" && num > 0xffn) {
14158
+ const bytes = [];
14159
+ let tempNum = num;
14160
+ while (tempNum > 0n) {
14161
+ bytes.unshift(tempNum & 0xffn);
14162
+ tempNum = tempNum >> 8n;
14163
+ }
14164
+ results.push("");
14165
+ results.push("Bytes (big-endian):");
14166
+ results.push(` ${bytes.map((b, i) => `[${i}] 0x${b.toString(16).padStart(2, "0")} (${b})`).join(`
14167
+ `)}`);
14168
+ }
14169
+ const summary = [
14170
+ `Number Conversion: ${args.value}`,
14171
+ `From: ${args.fromFormat}`,
14172
+ "",
14173
+ ...results
14174
+ ].join(`
14175
+ `);
14176
+ return { content: [{ type: "text", text: summary }] };
14177
+ }
14004
14178
  var SYMBOL_TYPE_DESCRIPTIONS = {
14005
14179
  A: "Global absolute symbol",
14006
14180
  B: "Global BSS (uninitialized data)",
@@ -15024,6 +15198,33 @@ var TOOLS = [
15024
15198
  required: ["filePath", "pattern"]
15025
15199
  }
15026
15200
  },
15201
+ {
15202
+ name: "bin_convert",
15203
+ description: "Convert numbers between hex, decimal, binary, octal, and ASCII formats",
15204
+ inputSchema: {
15205
+ type: "object",
15206
+ properties: {
15207
+ value: { type: "string", description: "Number value to convert" },
15208
+ fromFormat: {
15209
+ type: "string",
15210
+ enum: ["hex", "decimal", "binary", "octal", "auto"],
15211
+ description: "Input format (default: auto-detect)"
15212
+ },
15213
+ toFormat: {
15214
+ type: "string",
15215
+ enum: ["all", "hex", "decimal", "binary", "octal", "ascii"],
15216
+ description: "Output format (default: all)"
15217
+ },
15218
+ byteSize: {
15219
+ type: "number",
15220
+ enum: [8, 16, 32, 64],
15221
+ description: "Byte size for padding (8/16/32/64-bit)"
15222
+ },
15223
+ signed: { type: "boolean", description: "Treat as signed integer" }
15224
+ },
15225
+ required: ["value"]
15226
+ }
15227
+ },
15027
15228
  {
15028
15229
  name: "bin_strings",
15029
15230
  description: "Extract readable strings (ASCII, Unicode) from a binary file",
@@ -15313,6 +15514,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
15313
15514
  return await handleNopSled(args);
15314
15515
  case "bin_hex_editor":
15315
15516
  return await handleHexEditor(args);
15517
+ case "bin_convert":
15518
+ return await handleConvertNumber(args);
15316
15519
  default:
15317
15520
  throw new Error(`Unknown tool: ${name}`);
15318
15521
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ebowwa/mcp-nm",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Comprehensive binary analysis MCP server - symbols (nm), hex dumps (xxd), strings, disassembly, security audit, entropy analysis, ELF/Mach-O inspection, and binary patching",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -537,17 +537,9 @@ async function handleXxdFindPattern(args: {
537
537
  patternFormat?: "hex" | "text";
538
538
  maxLength?: number;
539
539
  }) {
540
- // Read file as hex
541
- const result = await runXxd(args.filePath, {
542
- plainHex: true,
543
- length: args.maxLength,
544
- });
545
-
546
- const fileHex = result.output.replace(/\s/g, "").toLowerCase();
540
+ // Convert pattern to hex if text
547
541
  let searchPattern: string;
548
-
549
542
  if (args.patternFormat === "text" || !args.patternFormat) {
550
- // Convert text to hex
551
543
  searchPattern = args.pattern
552
544
  .split("")
553
545
  .map((c) => c.charCodeAt(0).toString(16).padStart(2, "0"))
@@ -556,24 +548,119 @@ async function handleXxdFindPattern(args: {
556
548
  searchPattern = args.pattern.replace(/\s/g, "").toLowerCase();
557
549
  }
558
550
 
551
+ // Get file size to determine approach
552
+ const { stdout: sizeStr } = await execAsync(`stat -f%z "${args.filePath}" 2>/dev/null || stat -c%s "${args.filePath}"`);
553
+ const fileSize = parseInt(sizeStr.trim(), 10);
554
+ const maxInMemory = args.maxLength ?? 50 * 1024 * 1024; // 50MB default limit
555
+
559
556
  const matches: { offset: number; context: string }[] = [];
560
- let idx = 0;
561
- while ((idx = fileHex.indexOf(searchPattern, idx)) !== -1) {
562
- const byteOffset = Math.floor(idx / 2);
563
- // Get context (10 bytes before and after)
564
- const contextStart = Math.max(0, idx - 20);
565
- const contextEnd = Math.min(fileHex.length, idx + searchPattern.length + 20);
566
- const contextHex = fileHex.slice(contextStart, contextEnd);
567
-
568
- matches.push({
569
- offset: byteOffset,
570
- context: contextHex.match(/.{1,2}/g)?.join(" ") || contextHex,
557
+
558
+ if (fileSize <= maxInMemory) {
559
+ // Small file: use in-memory approach
560
+ const result = await runXxd(args.filePath, {
561
+ plainHex: true,
562
+ length: args.maxLength,
571
563
  });
572
- idx++;
564
+
565
+ const fileHex = result.output.replace(/\s/g, "").toLowerCase();
566
+
567
+ let idx = 0;
568
+ while ((idx = fileHex.indexOf(searchPattern, idx)) !== -1) {
569
+ const byteOffset = Math.floor(idx / 2);
570
+ const contextStart = Math.max(0, idx - 20);
571
+ const contextEnd = Math.min(fileHex.length, idx + searchPattern.length + 20);
572
+ const contextHex = fileHex.slice(contextStart, contextEnd);
573
+
574
+ matches.push({
575
+ offset: byteOffset,
576
+ context: contextHex.match(/.{1,2}/g)?.join(" ") || contextHex,
577
+ });
578
+ idx++;
579
+ if (matches.length >= 1000) break; // Limit matches
580
+ }
581
+ } else {
582
+ // Large file: use streaming grep approach
583
+ // Use grep -abo for byte offset (Linux) or grep -b with binary mode (macOS)
584
+ try {
585
+ // Try using grep with binary pattern
586
+ // Convert hex pattern to escaped bytes for grep
587
+ const patternBytes = searchPattern.match(/.{2}/g) ?? [];
588
+ const escapedPattern = patternBytes.map(b => `\\x${b}`).join("");
589
+
590
+ // Use grep -b to get byte offsets, with binary search
591
+ const { stdout: grepResult } = await execAsync(
592
+ `grep -abo "${escapedPattern}" "${args.filePath}" 2>/dev/null | head -1000`,
593
+ { maxBuffer: 10 * 1024 * 1024 }
594
+ );
595
+
596
+ const lines = grepResult.trim().split("\n").filter(Boolean);
597
+ for (const line of lines) {
598
+ const match = line.match(/^(\d+):/);
599
+ if (match) {
600
+ const offset = parseInt(match[1], 10);
601
+ // Get context using dd
602
+ try {
603
+ const contextOffset = Math.max(0, offset - 10);
604
+ const contextLen = searchPattern.length / 2 + 20;
605
+ const { stdout: contextHex } = await execAsync(
606
+ `dd if="${args.filePath}" bs=1 skip=${contextOffset} count=${contextLen} 2>/dev/null | xxd -p | tr -d '\\n'`
607
+ );
608
+ matches.push({
609
+ offset,
610
+ context: contextHex.match(/.{1,2}/g)?.join(" ") || contextHex,
611
+ });
612
+ } catch {
613
+ matches.push({ offset, context: "(context unavailable)" });
614
+ }
615
+ }
616
+ }
617
+ } catch {
618
+ // grep failed, try alternative: use strings with offset
619
+ try {
620
+ const { stdout: stringsResult } = await execAsync(
621
+ `strings -t x -n ${Math.floor(searchPattern.length / 2)} "${args.filePath}" | grep -i "${args.pattern}" | head -1000`,
622
+ { maxBuffer: 10 * 1024 * 1024 }
623
+ );
624
+ const lines = stringsResult.trim().split("\n").filter(Boolean);
625
+ for (const line of lines) {
626
+ const match = line.match(/^\s*([0-9a-fA-F]+)\s+(.+)$/);
627
+ if (match) {
628
+ matches.push({
629
+ offset: parseInt(match[1], 16),
630
+ context: match[2].slice(0, 40),
631
+ });
632
+ }
633
+ }
634
+ } catch {
635
+ // Last resort: chunked reading
636
+ const chunkSize = 10 * 1024 * 1024; // 10MB chunks
637
+ for (let chunkStart = 0; chunkStart < fileSize; chunkStart += chunkSize) {
638
+ try {
639
+ const { stdout: chunkHex } = await execAsync(
640
+ `dd if="${args.filePath}" bs=1 skip=${chunkStart} count=${chunkSize} 2>/dev/null | xxd -p | tr -d '\\n'`
641
+ );
642
+ const hex = chunkHex.toLowerCase();
643
+ let idx = 0;
644
+ while ((idx = hex.indexOf(searchPattern, idx)) !== -1) {
645
+ matches.push({
646
+ offset: chunkStart + Math.floor(idx / 2),
647
+ context: hex.slice(Math.max(0, idx - 20), idx + searchPattern.length + 20).match(/.{1,2}/g)?.join(" ") || "",
648
+ });
649
+ idx++;
650
+ if (matches.length >= 1000) break;
651
+ }
652
+ if (matches.length >= 1000) break;
653
+ } catch {
654
+ // Skip failed chunk
655
+ }
656
+ }
657
+ }
658
+ }
573
659
  }
574
660
 
575
661
  const summary = [
576
662
  `Pattern search in: ${args.filePath}`,
663
+ `File size: ${(fileSize / 1024 / 1024).toFixed(2)} MB`,
577
664
  `Pattern: ${args.pattern} (${args.patternFormat || "text"})`,
578
665
  `Hex pattern: ${searchPattern}`,
579
666
  `Matches found: ${matches.length}`,
@@ -587,6 +674,147 @@ async function handleXxdFindPattern(args: {
587
674
  return { content: [{ type: "text", text: summary }] };
588
675
  }
589
676
 
677
+ // ============================================================================
678
+ // Number Format Conversion Utility
679
+ // ============================================================================
680
+
681
+ async function handleConvertNumber(args: {
682
+ value: string;
683
+ fromFormat: "hex" | "decimal" | "binary" | "octal" | "auto";
684
+ toFormat?: "all" | "hex" | "decimal" | "binary" | "octal" | "ascii";
685
+ byteSize?: 8 | 16 | 32 | 64;
686
+ signed?: boolean;
687
+ }) {
688
+ let num: bigint;
689
+
690
+ // Parse input value
691
+ const value = args.value.trim().toLowerCase();
692
+
693
+ try {
694
+ switch (args.fromFormat) {
695
+ case "hex":
696
+ num = BigInt(value.startsWith("0x") ? value : `0x${value}`);
697
+ break;
698
+ case "decimal":
699
+ num = BigInt(value);
700
+ break;
701
+ case "binary":
702
+ num = BigInt(value.startsWith("0b") ? value.slice(2) : value, 2);
703
+ break;
704
+ case "octal":
705
+ num = BigInt(value.startsWith("0o") ? value.slice(2) : value, 8);
706
+ break;
707
+ case "auto":
708
+ default:
709
+ // Auto-detect format
710
+ if (value.startsWith("0x") || /^[0-9a-f]+$/i.test(value) && value.length > 4) {
711
+ num = BigInt(value.startsWith("0x") ? value : `0x${value}`);
712
+ } else if (value.startsWith("0b")) {
713
+ num = BigInt(value.slice(2), 2);
714
+ } else if (value.startsWith("0o")) {
715
+ num = BigInt(value.slice(2), 8);
716
+ } else {
717
+ num = BigInt(value);
718
+ }
719
+ break;
720
+ }
721
+ } catch {
722
+ return {
723
+ content: [{ type: "text", text: `Error: Invalid number format "${args.value}"` }],
724
+ isError: true,
725
+ };
726
+ }
727
+
728
+ // Handle signed conversion if requested
729
+ let signedNum: bigint = num;
730
+ if (args.signed && args.byteSize) {
731
+ const bits = args.byteSize;
732
+ const maxUnsigned = (1n << BigInt(bits)) - 1n;
733
+ const signBit = 1n << BigInt(bits - 1);
734
+
735
+ if (num & signBit) {
736
+ signedNum = num - (1n << BigInt(bits));
737
+ }
738
+ }
739
+
740
+ // Format outputs
741
+ const toFormat = args.toFormat ?? "all";
742
+ const results: string[] = [];
743
+
744
+ if (toFormat === "all" || toFormat === "decimal") {
745
+ results.push(`Decimal: ${num.toString()}`);
746
+ if (args.signed && args.byteSize && signedNum !== num) {
747
+ results.push(`Signed: ${signedNum.toString()}`);
748
+ }
749
+ }
750
+
751
+ if (toFormat === "all" || toFormat === "hex") {
752
+ const hexStr = num.toString(16).toUpperCase();
753
+ const paddedHex = args.byteSize
754
+ ? hexStr.padStart(args.byteSize / 4, "0")
755
+ : hexStr;
756
+ results.push(`Hex: 0x${paddedHex}`);
757
+ }
758
+
759
+ if (toFormat === "all" || toFormat === "binary") {
760
+ const binStr = num.toString(2);
761
+ const paddedBin = args.byteSize
762
+ ? binStr.padStart(args.byteSize, "0")
763
+ : binStr;
764
+ // Add spaces every 8 bits for readability
765
+ const formattedBin = paddedBin.match(/.{1,8}/g)?.join(" ") ?? paddedBin;
766
+ results.push(`Binary: 0b${formattedBin}`);
767
+ }
768
+
769
+ if (toFormat === "all" || toFormat === "octal") {
770
+ results.push(`Octal: 0o${num.toString(8)}`);
771
+ }
772
+
773
+ if (toFormat === "all" || toFormat === "ascii") {
774
+ // Try to convert to ASCII string
775
+ let asciiStr = "";
776
+ let tempNum = num;
777
+ while (tempNum > 0n) {
778
+ const charCode = Number(tempNum & 0xffn);
779
+ if (charCode >= 32 && charCode <= 126) {
780
+ asciiStr = String.fromCharCode(charCode) + asciiStr;
781
+ } else {
782
+ asciiStr = `\\x${charCode.toString(16).padStart(2, "0")}` + asciiStr;
783
+ }
784
+ tempNum = tempNum >> 8n;
785
+ }
786
+ results.push(`ASCII: ${asciiStr || "(non-printable)"}`);
787
+ }
788
+
789
+ // Add byte size info
790
+ if (args.byteSize) {
791
+ results.push("");
792
+ results.push(`Size: ${args.byteSize}-bit (${args.byteSize / 8} bytes)`);
793
+ }
794
+
795
+ // Add byte breakdown for multi-byte values
796
+ if (toFormat === "all" && num > 0xffn) {
797
+ const bytes: string[] = [];
798
+ let tempNum = num;
799
+ while (tempNum > 0n) {
800
+ bytes.unshift(tempNum & 0xffn);
801
+ tempNum = tempNum >> 8n;
802
+ }
803
+ results.push("");
804
+ results.push("Bytes (big-endian):");
805
+ results.push(` ${bytes.map((b, i) => `[${i}] 0x${b.toString(16).padStart(2, "0")} (${b})`).join("\n ")}`);
806
+ }
807
+
808
+ const summary = [
809
+ `Number Conversion: ${args.value}`,
810
+ `From: ${args.fromFormat}`,
811
+ "",
812
+ ...results,
813
+ ].join("\n");
814
+
815
+ return { content: [{ type: "text", text: summary }] };
816
+ }
817
+
590
818
  // ============================================================================
591
819
  // Symbol type descriptions
592
820
  // ============================================================================
@@ -1899,6 +2127,34 @@ const TOOLS = [
1899
2127
  required: ["filePath", "pattern"],
1900
2128
  },
1901
2129
  },
2130
+ // Number Format Conversion
2131
+ {
2132
+ name: "bin_convert",
2133
+ description: "Convert numbers between hex, decimal, binary, octal, and ASCII formats",
2134
+ inputSchema: {
2135
+ type: "object" as const,
2136
+ properties: {
2137
+ value: { type: "string", description: "Number value to convert" },
2138
+ fromFormat: {
2139
+ type: "string",
2140
+ enum: ["hex", "decimal", "binary", "octal", "auto"],
2141
+ description: "Input format (default: auto-detect)",
2142
+ },
2143
+ toFormat: {
2144
+ type: "string",
2145
+ enum: ["all", "hex", "decimal", "binary", "octal", "ascii"],
2146
+ description: "Output format (default: all)",
2147
+ },
2148
+ byteSize: {
2149
+ type: "number",
2150
+ enum: [8, 16, 32, 64],
2151
+ description: "Byte size for padding (8/16/32/64-bit)",
2152
+ },
2153
+ signed: { type: "boolean", description: "Treat as signed integer" },
2154
+ },
2155
+ required: ["value"],
2156
+ },
2157
+ },
1902
2158
  // Extended Binary Analysis Tools
1903
2159
  {
1904
2160
  name: "bin_strings",
@@ -2235,6 +2491,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2235
2491
  return await handleNopSled(args as { filePath: string; offset: number; count: number; createBackup?: boolean });
2236
2492
  case "bin_hex_editor":
2237
2493
  return await handleHexEditor(args as { filePath: string; offset: number; length: number; newHex?: string });
2494
+ // Number conversion utility
2495
+ case "bin_convert":
2496
+ return await handleConvertNumber(args as {
2497
+ value: string;
2498
+ fromFormat: "hex" | "decimal" | "binary" | "octal" | "auto";
2499
+ toFormat?: "all" | "hex" | "decimal" | "binary" | "octal" | "ascii";
2500
+ byteSize?: 8 | 16 | 32 | 64;
2501
+ signed?: boolean;
2502
+ });
2238
2503
  default:
2239
2504
  throw new Error(`Unknown tool: ${name}`);
2240
2505
  }