@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.
- package/dist/index.js +218 -15
- package/package.json +1 -1
- 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
|
-
|
|
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}`,
|
|
@@ -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.
|
|
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
|
-
//
|
|
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}`,
|
|
@@ -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
|
}
|