@ebowwa/mcp-nm 2.0.0 → 2.0.1

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 +83 -15
  2. package/package.json +1 -1
  3. package/src/index.ts +108 -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}`,
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.1",
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}`,