@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.
- package/dist/index.js +83 -15
- package/package.json +1 -1
- 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
|
-
|
|
13981
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
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}`,
|