@ebowwa/mcp-nm 1.0.2 → 1.1.0
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 +4433 -9723
- package/package.json +8 -4
- package/src/index.ts +976 -393
- package/bun.lock +0 -233
package/src/index.ts
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* @ebowwa/nm-mcp - Binary
|
|
3
|
+
* @ebowwa/nm-mcp - Binary analysis MCP server
|
|
4
4
|
*
|
|
5
|
-
* Provides tools for analyzing symbols
|
|
5
|
+
* Provides tools for analyzing binaries using nm (symbols) and xxd (hex dumps)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
import {
|
|
11
|
+
CallToolRequestSchema,
|
|
12
|
+
ListToolsRequestSchema,
|
|
13
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
14
|
import { exec } from "child_process";
|
|
11
15
|
import { promisify } from "util";
|
|
12
|
-
import { z } from "zod";
|
|
13
16
|
|
|
14
17
|
const execAsync = promisify(exec);
|
|
15
18
|
|
|
16
19
|
// Server instance
|
|
17
|
-
const server = new
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
const server = new Server(
|
|
21
|
+
{
|
|
22
|
+
name: "@ebowwa/nm-mcp",
|
|
23
|
+
version: "1.1.0",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
capabilities: {
|
|
27
|
+
tools: {},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
);
|
|
21
31
|
|
|
22
32
|
// ============================================================================
|
|
23
33
|
// Types
|
|
@@ -37,6 +47,13 @@ interface NmResult {
|
|
|
37
47
|
filteredCount: number;
|
|
38
48
|
}
|
|
39
49
|
|
|
50
|
+
interface XxdResult {
|
|
51
|
+
filePath: string;
|
|
52
|
+
output: string;
|
|
53
|
+
bytesProcessed: number;
|
|
54
|
+
startOffset: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
40
57
|
// ============================================================================
|
|
41
58
|
// nm execution
|
|
42
59
|
// ============================================================================
|
|
@@ -130,6 +147,365 @@ async function runNm(
|
|
|
130
147
|
}
|
|
131
148
|
}
|
|
132
149
|
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// xxd execution
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
async function runXxd(
|
|
155
|
+
filePath: string,
|
|
156
|
+
options: {
|
|
157
|
+
length?: number;
|
|
158
|
+
seek?: number;
|
|
159
|
+
cols?: number;
|
|
160
|
+
groupSize?: number;
|
|
161
|
+
plainHex?: boolean;
|
|
162
|
+
cInclude?: boolean;
|
|
163
|
+
binary?: boolean;
|
|
164
|
+
uppercase?: boolean;
|
|
165
|
+
littleEndian?: boolean;
|
|
166
|
+
reverse?: boolean;
|
|
167
|
+
} = {}
|
|
168
|
+
): Promise<XxdResult> {
|
|
169
|
+
const args: string[] = [];
|
|
170
|
+
|
|
171
|
+
// Length (how many bytes to read)
|
|
172
|
+
if (options.length !== undefined) {
|
|
173
|
+
args.push("-l", options.length.toString());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Seek offset (where to start)
|
|
177
|
+
if (options.seek !== undefined) {
|
|
178
|
+
args.push("-s", options.seek.toString());
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Columns per line
|
|
182
|
+
if (options.cols !== undefined) {
|
|
183
|
+
args.push("-c", options.cols.toString());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Group size
|
|
187
|
+
if (options.groupSize !== undefined) {
|
|
188
|
+
args.push("-g", options.groupSize.toString());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Output formats (mutually exclusive)
|
|
192
|
+
if (options.plainHex) args.push("-p");
|
|
193
|
+
if (options.cInclude) args.push("-i");
|
|
194
|
+
if (options.binary) args.push("-b");
|
|
195
|
+
if (options.reverse) args.push("-r");
|
|
196
|
+
|
|
197
|
+
// Other options
|
|
198
|
+
if (options.uppercase) args.push("-u");
|
|
199
|
+
if (options.littleEndian) args.push("-e");
|
|
200
|
+
|
|
201
|
+
// Build command
|
|
202
|
+
const cmd = `xxd ${args.join(" ")} "${filePath}"`;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
206
|
+
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for larger files
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (stderr && !stdout) {
|
|
210
|
+
throw new Error(stderr);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Estimate bytes processed
|
|
214
|
+
let bytesProcessed = 0;
|
|
215
|
+
if (options.length !== undefined) {
|
|
216
|
+
bytesProcessed = options.length;
|
|
217
|
+
} else if (options.plainHex) {
|
|
218
|
+
// Plain hex: 2 chars per byte
|
|
219
|
+
bytesProcessed = Math.floor(stdout.length / 2);
|
|
220
|
+
} else {
|
|
221
|
+
// Standard hexdump: estimate from output lines
|
|
222
|
+
const cols = options.cols || 16;
|
|
223
|
+
const lines = stdout.split("\n").filter((l) => l.trim()).length;
|
|
224
|
+
bytesProcessed = lines * cols;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
filePath,
|
|
229
|
+
output: stdout,
|
|
230
|
+
bytesProcessed,
|
|
231
|
+
startOffset: options.seek || 0,
|
|
232
|
+
};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`xxd command failed: ${error instanceof Error ? error.message : error}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// xxd tool handlers
|
|
242
|
+
// ============================================================================
|
|
243
|
+
|
|
244
|
+
async function handleXxdHexdump(args: {
|
|
245
|
+
filePath: string;
|
|
246
|
+
length?: number;
|
|
247
|
+
seek?: number;
|
|
248
|
+
cols?: number;
|
|
249
|
+
groupSize?: number;
|
|
250
|
+
uppercase?: boolean;
|
|
251
|
+
}) {
|
|
252
|
+
const result = await runXxd(args.filePath, {
|
|
253
|
+
length: args.length,
|
|
254
|
+
seek: args.seek,
|
|
255
|
+
cols: args.cols,
|
|
256
|
+
groupSize: args.groupSize,
|
|
257
|
+
uppercase: args.uppercase,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const summary = [
|
|
261
|
+
`Hex dump: ${result.filePath}`,
|
|
262
|
+
`Start offset: 0x${result.startOffset.toString(16)} (${result.startOffset})`,
|
|
263
|
+
`Bytes shown: ~${result.bytesProcessed.toLocaleString()}`,
|
|
264
|
+
"",
|
|
265
|
+
result.output,
|
|
266
|
+
].join("\n");
|
|
267
|
+
|
|
268
|
+
return { content: [{ type: "text", text: summary }] };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function handleXxdPlain(args: {
|
|
272
|
+
filePath: string;
|
|
273
|
+
length?: number;
|
|
274
|
+
seek?: number;
|
|
275
|
+
uppercase?: boolean;
|
|
276
|
+
}) {
|
|
277
|
+
const result = await runXxd(args.filePath, {
|
|
278
|
+
length: args.length,
|
|
279
|
+
seek: args.seek,
|
|
280
|
+
uppercase: args.uppercase,
|
|
281
|
+
plainHex: true,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const summary = [
|
|
285
|
+
`Plain hex: ${result.filePath}`,
|
|
286
|
+
`Start offset: 0x${result.startOffset.toString(16)}`,
|
|
287
|
+
`Bytes: ~${result.bytesProcessed.toLocaleString()}`,
|
|
288
|
+
"",
|
|
289
|
+
result.output.slice(0, 10000),
|
|
290
|
+
result.output.length > 10000
|
|
291
|
+
? `\n... truncated (${result.output.length - 10000} more chars)`
|
|
292
|
+
: "",
|
|
293
|
+
].join("\n");
|
|
294
|
+
|
|
295
|
+
return { content: [{ type: "text", text: summary }] };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function handleXxdCInclude(args: {
|
|
299
|
+
filePath: string;
|
|
300
|
+
length?: number;
|
|
301
|
+
seek?: number;
|
|
302
|
+
variableName?: string;
|
|
303
|
+
}) {
|
|
304
|
+
const result = await runXxd(args.filePath, {
|
|
305
|
+
length: args.length,
|
|
306
|
+
seek: args.seek,
|
|
307
|
+
cInclude: true,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Optionally rename the variable
|
|
311
|
+
let output = result.output;
|
|
312
|
+
if (args.variableName) {
|
|
313
|
+
output = output.replace(/unsigned char/g, `unsigned char ${args.variableName}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const summary = [
|
|
317
|
+
`C include style: ${result.filePath}`,
|
|
318
|
+
`Bytes: ~${result.bytesProcessed.toLocaleString()}`,
|
|
319
|
+
"",
|
|
320
|
+
output,
|
|
321
|
+
].join("\n");
|
|
322
|
+
|
|
323
|
+
return { content: [{ type: "text", text: summary }] };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function handleXxdBinary(args: {
|
|
327
|
+
filePath: string;
|
|
328
|
+
length?: number;
|
|
329
|
+
seek?: number;
|
|
330
|
+
cols?: number;
|
|
331
|
+
}) {
|
|
332
|
+
const result = await runXxd(args.filePath, {
|
|
333
|
+
length: args.length,
|
|
334
|
+
seek: args.seek,
|
|
335
|
+
cols: args.cols || 8,
|
|
336
|
+
binary: true,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const summary = [
|
|
340
|
+
`Binary dump: ${result.filePath}`,
|
|
341
|
+
`Start offset: 0x${result.startOffset.toString(16)}`,
|
|
342
|
+
`Bytes: ~${result.bytesProcessed.toLocaleString()}`,
|
|
343
|
+
"",
|
|
344
|
+
result.output,
|
|
345
|
+
].join("\n");
|
|
346
|
+
|
|
347
|
+
return { content: [{ type: "text", text: summary }] };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function handleXxdReverse(args: {
|
|
351
|
+
inputHex: string;
|
|
352
|
+
outputPath: string;
|
|
353
|
+
}) {
|
|
354
|
+
// Write hex to temp file, then use xxd -r
|
|
355
|
+
const tempFile = `/tmp/xxd_input_${Date.now()}.hex`;
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
// Write hex input to temp file
|
|
359
|
+
await execAsync(`echo '${args.inputHex}' > "${tempFile}"`);
|
|
360
|
+
|
|
361
|
+
// Convert hex to binary
|
|
362
|
+
const cmd = `xxd -r -p "${tempFile}" > "${args.outputPath}"`;
|
|
363
|
+
await execAsync(cmd);
|
|
364
|
+
|
|
365
|
+
// Get file info
|
|
366
|
+
const { stdout: fileInfo } = await execAsync(
|
|
367
|
+
`ls -la "${args.outputPath}" | awk '{print $5}'`
|
|
368
|
+
);
|
|
369
|
+
const bytesWritten = parseInt(fileInfo.trim(), 10);
|
|
370
|
+
|
|
371
|
+
const summary = [
|
|
372
|
+
`Converted hex to binary`,
|
|
373
|
+
`Output: ${args.outputPath}`,
|
|
374
|
+
`Bytes written: ${bytesWritten.toLocaleString()}`,
|
|
375
|
+
"",
|
|
376
|
+
"Success!",
|
|
377
|
+
].join("\n");
|
|
378
|
+
|
|
379
|
+
return { content: [{ type: "text", text: summary }] };
|
|
380
|
+
} finally {
|
|
381
|
+
// Cleanup temp file
|
|
382
|
+
await execAsync(`rm -f "${tempFile}"`).catch(() => {});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function handleXxdExtract(args: {
|
|
387
|
+
filePath: string;
|
|
388
|
+
offset: number;
|
|
389
|
+
length: number;
|
|
390
|
+
format?: "hex" | "binary" | "decimal" | "c-array";
|
|
391
|
+
}) {
|
|
392
|
+
const format = args.format || "hex";
|
|
393
|
+
const result = await runXxd(args.filePath, {
|
|
394
|
+
seek: args.offset,
|
|
395
|
+
length: args.length,
|
|
396
|
+
plainHex: format === "hex" || format === "c-array",
|
|
397
|
+
cols: args.length,
|
|
398
|
+
groupSize: 1,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
let output: string;
|
|
402
|
+
const rawHex = result.output.replace(/\s/g, "");
|
|
403
|
+
|
|
404
|
+
switch (format) {
|
|
405
|
+
case "hex":
|
|
406
|
+
output = rawHex;
|
|
407
|
+
break;
|
|
408
|
+
case "binary": {
|
|
409
|
+
// Convert hex to binary string
|
|
410
|
+
let bin = "";
|
|
411
|
+
for (let i = 0; i < rawHex.length; i += 2) {
|
|
412
|
+
const byte = parseInt(rawHex.slice(i, i + 2), 16);
|
|
413
|
+
bin += byte.toString(2).padStart(8, "0") + " ";
|
|
414
|
+
}
|
|
415
|
+
output = bin.trim();
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case "decimal": {
|
|
419
|
+
// Convert hex to decimal bytes
|
|
420
|
+
const decimals: string[] = [];
|
|
421
|
+
for (let i = 0; i < rawHex.length; i += 2) {
|
|
422
|
+
const byte = parseInt(rawHex.slice(i, i + 2), 16);
|
|
423
|
+
decimals.push(byte.toString().padStart(3, " "));
|
|
424
|
+
}
|
|
425
|
+
output = decimals.join(" ");
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
case "c-array": {
|
|
429
|
+
// Format as C array
|
|
430
|
+
const bytes: string[] = [];
|
|
431
|
+
for (let i = 0; i < rawHex.length; i += 2) {
|
|
432
|
+
bytes.push(`0x${rawHex.slice(i, i + 2)}`);
|
|
433
|
+
}
|
|
434
|
+
output = `{ ${bytes.join(", ")} }`;
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
default:
|
|
438
|
+
output = rawHex;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const summary = [
|
|
442
|
+
`Extracted bytes from: ${result.filePath}`,
|
|
443
|
+
`Offset: 0x${args.offset.toString(16)} (${args.offset})`,
|
|
444
|
+
`Length: ${args.length} bytes`,
|
|
445
|
+
`Format: ${format}`,
|
|
446
|
+
"",
|
|
447
|
+
output,
|
|
448
|
+
].join("\n");
|
|
449
|
+
|
|
450
|
+
return { content: [{ type: "text", text: summary }] };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function handleXxdFindPattern(args: {
|
|
454
|
+
filePath: string;
|
|
455
|
+
pattern: string;
|
|
456
|
+
patternFormat?: "hex" | "text";
|
|
457
|
+
maxLength?: number;
|
|
458
|
+
}) {
|
|
459
|
+
// Read file as hex
|
|
460
|
+
const result = await runXxd(args.filePath, {
|
|
461
|
+
plainHex: true,
|
|
462
|
+
length: args.maxLength,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const fileHex = result.output.replace(/\s/g, "").toLowerCase();
|
|
466
|
+
let searchPattern: string;
|
|
467
|
+
|
|
468
|
+
if (args.patternFormat === "text" || !args.patternFormat) {
|
|
469
|
+
// Convert text to hex
|
|
470
|
+
searchPattern = args.pattern
|
|
471
|
+
.split("")
|
|
472
|
+
.map((c) => c.charCodeAt(0).toString(16).padStart(2, "0"))
|
|
473
|
+
.join("");
|
|
474
|
+
} else {
|
|
475
|
+
searchPattern = args.pattern.replace(/\s/g, "").toLowerCase();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const matches: { offset: number; context: string }[] = [];
|
|
479
|
+
let idx = 0;
|
|
480
|
+
while ((idx = fileHex.indexOf(searchPattern, idx)) !== -1) {
|
|
481
|
+
const byteOffset = Math.floor(idx / 2);
|
|
482
|
+
// Get context (10 bytes before and after)
|
|
483
|
+
const contextStart = Math.max(0, idx - 20);
|
|
484
|
+
const contextEnd = Math.min(fileHex.length, idx + searchPattern.length + 20);
|
|
485
|
+
const contextHex = fileHex.slice(contextStart, contextEnd);
|
|
486
|
+
|
|
487
|
+
matches.push({
|
|
488
|
+
offset: byteOffset,
|
|
489
|
+
context: contextHex.match(/.{1,2}/g)?.join(" ") || contextHex,
|
|
490
|
+
});
|
|
491
|
+
idx++;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const summary = [
|
|
495
|
+
`Pattern search in: ${args.filePath}`,
|
|
496
|
+
`Pattern: ${args.pattern} (${args.patternFormat || "text"})`,
|
|
497
|
+
`Hex pattern: ${searchPattern}`,
|
|
498
|
+
`Matches found: ${matches.length}`,
|
|
499
|
+
"",
|
|
500
|
+
...matches.slice(0, 50).map(
|
|
501
|
+
(m) => ` Offset 0x${m.offset.toString(16).padStart(8, "0")}: ...${m.context}...`
|
|
502
|
+
),
|
|
503
|
+
matches.length > 50 ? ` ... and ${matches.length - 50} more` : "",
|
|
504
|
+
].join("\n");
|
|
505
|
+
|
|
506
|
+
return { content: [{ type: "text", text: summary }] };
|
|
507
|
+
}
|
|
508
|
+
|
|
133
509
|
// ============================================================================
|
|
134
510
|
// Symbol type descriptions
|
|
135
511
|
// ============================================================================
|
|
@@ -162,437 +538,644 @@ const SYMBOL_TYPE_DESCRIPTIONS: Record<string, string> = {
|
|
|
162
538
|
};
|
|
163
539
|
|
|
164
540
|
// ============================================================================
|
|
165
|
-
//
|
|
541
|
+
// Tool handlers
|
|
166
542
|
// ============================================================================
|
|
167
543
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"nm_list_symbols",
|
|
171
|
-
"List all symbols in a binary file",
|
|
172
|
-
{
|
|
173
|
-
filePath: z.string().describe("Path to the binary file to analyze"),
|
|
174
|
-
demangle: z
|
|
175
|
-
.boolean()
|
|
176
|
-
.optional()
|
|
177
|
-
.default(false)
|
|
178
|
-
.describe("Demangle C++ symbol names"),
|
|
179
|
-
},
|
|
180
|
-
async ({ filePath, demangle }) => {
|
|
181
|
-
const result = await runNm(filePath, { demangle });
|
|
544
|
+
async function handleListSymbols(args: { filePath: string; demangle?: boolean }) {
|
|
545
|
+
const result = await runNm(args.filePath, { demangle: args.demangle ?? false });
|
|
182
546
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
547
|
+
const summary = [
|
|
548
|
+
`File: ${result.filePath}`,
|
|
549
|
+
`Total symbols: ${result.totalCount}`,
|
|
550
|
+
"",
|
|
551
|
+
"Symbols:",
|
|
552
|
+
...result.symbols.slice(0, 100).map(
|
|
553
|
+
(s) =>
|
|
554
|
+
` ${s.value} ${s.type} ${s.name}${
|
|
555
|
+
SYMBOL_TYPE_DESCRIPTIONS[s.type]
|
|
556
|
+
? ` (${SYMBOL_TYPE_DESCRIPTIONS[s.type]})`
|
|
557
|
+
: ""
|
|
558
|
+
}`
|
|
559
|
+
),
|
|
560
|
+
result.symbols.length > 100
|
|
561
|
+
? ` ... and ${result.symbols.length - 100} more`
|
|
562
|
+
: "",
|
|
563
|
+
].join("\n");
|
|
200
564
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
565
|
+
return { content: [{ type: "text", text: summary }] };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function handleExternalSymbols(args: { filePath: string; demangle?: boolean; showSize?: boolean }) {
|
|
569
|
+
const result = await runNm(args.filePath, {
|
|
570
|
+
externalOnly: true,
|
|
571
|
+
demangle: args.demangle ?? true,
|
|
572
|
+
showSize: args.showSize ?? false,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// Group by type
|
|
576
|
+
const byType: Record<string, NmSymbol[]> = {};
|
|
577
|
+
for (const sym of result.symbols) {
|
|
578
|
+
const type = sym.type;
|
|
579
|
+
if (!byType[type]) byType[type] = [];
|
|
580
|
+
byType[type].push(sym);
|
|
204
581
|
}
|
|
205
|
-
);
|
|
206
582
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
showSize,
|
|
229
|
-
});
|
|
583
|
+
const summary = [
|
|
584
|
+
`File: ${result.filePath}`,
|
|
585
|
+
`External symbols: ${result.totalCount}`,
|
|
586
|
+
"",
|
|
587
|
+
"By type:",
|
|
588
|
+
...Object.entries(byType).map(([type, syms]) => {
|
|
589
|
+
const desc = SYMBOL_TYPE_DESCRIPTIONS[type] || "Unknown";
|
|
590
|
+
return `\n ${type} (${desc}): ${syms.length} symbols`;
|
|
591
|
+
}),
|
|
592
|
+
"",
|
|
593
|
+
"All external symbols:",
|
|
594
|
+
...result.symbols.slice(0, 50).map(
|
|
595
|
+
(s) =>
|
|
596
|
+
` ${s.value} ${s.type} ${s.name}${
|
|
597
|
+
s.size ? ` [${s.size} bytes]` : ""
|
|
598
|
+
}`
|
|
599
|
+
),
|
|
600
|
+
result.symbols.length > 50
|
|
601
|
+
? ` ... and ${result.symbols.length - 50} more`
|
|
602
|
+
: "",
|
|
603
|
+
].join("\n");
|
|
230
604
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
for (const sym of result.symbols) {
|
|
234
|
-
const type = sym.type;
|
|
235
|
-
if (!byType[type]) byType[type] = [];
|
|
236
|
-
byType[type].push(sym);
|
|
237
|
-
}
|
|
605
|
+
return { content: [{ type: "text", text: summary }] };
|
|
606
|
+
}
|
|
238
607
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
...Object.entries(byType).map(([type, syms]) => {
|
|
245
|
-
const desc = SYMBOL_TYPE_DESCRIPTIONS[type] || "Unknown";
|
|
246
|
-
return `\n ${type} (${desc}): ${syms.length} symbols`;
|
|
247
|
-
}),
|
|
248
|
-
"",
|
|
249
|
-
"All external symbols:",
|
|
250
|
-
...result.symbols.slice(0, 50).map(
|
|
251
|
-
(s) =>
|
|
252
|
-
` ${s.value} ${s.type} ${s.name}${
|
|
253
|
-
s.size ? ` [${s.size} bytes]` : ""
|
|
254
|
-
}`
|
|
255
|
-
),
|
|
256
|
-
result.symbols.length > 50
|
|
257
|
-
? ` ... and ${result.symbols.length - 50} more`
|
|
258
|
-
: "",
|
|
259
|
-
].join("\n");
|
|
608
|
+
async function handleUndefinedSymbols(args: { filePath: string; demangle?: boolean }) {
|
|
609
|
+
const result = await runNm(args.filePath, {
|
|
610
|
+
undefinedOnly: true,
|
|
611
|
+
demangle: args.demangle ?? true,
|
|
612
|
+
});
|
|
260
613
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
614
|
+
const summary = [
|
|
615
|
+
`File: ${result.filePath}`,
|
|
616
|
+
`Undefined symbols (imports): ${result.totalCount}`,
|
|
617
|
+
"",
|
|
618
|
+
"Dependencies:",
|
|
619
|
+
...result.symbols.map((s) => ` - ${s.name}`),
|
|
620
|
+
].join("\n");
|
|
266
621
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
"nm_undefined_symbols",
|
|
270
|
-
"List undefined symbols in a binary - these are external dependencies the binary needs",
|
|
271
|
-
{
|
|
272
|
-
filePath: z.string().describe("Path to the binary file"),
|
|
273
|
-
demangle: z.boolean().optional().default(true),
|
|
274
|
-
},
|
|
275
|
-
async ({ filePath, demangle }) => {
|
|
276
|
-
const result = await runNm(filePath, {
|
|
277
|
-
undefinedOnly: true,
|
|
278
|
-
demangle,
|
|
279
|
-
});
|
|
622
|
+
return { content: [{ type: "text", text: summary }] };
|
|
623
|
+
}
|
|
280
624
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
].join("\n");
|
|
625
|
+
async function handleDefinedSymbols(args: { filePath: string; demangle?: boolean; showSize?: boolean }) {
|
|
626
|
+
const result = await runNm(args.filePath, {
|
|
627
|
+
definedOnly: true,
|
|
628
|
+
demangle: args.demangle ?? true,
|
|
629
|
+
showSize: args.showSize ?? true,
|
|
630
|
+
});
|
|
288
631
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
632
|
+
// Separate code and data symbols
|
|
633
|
+
const codeSymbols = result.symbols.filter(
|
|
634
|
+
(s) => s.type === "T" || s.type === "t" || s.type === "W" || s.type === "w"
|
|
635
|
+
);
|
|
636
|
+
const dataSymbols = result.symbols.filter(
|
|
637
|
+
(s) =>
|
|
638
|
+
s.type === "D" ||
|
|
639
|
+
s.type === "d" ||
|
|
640
|
+
s.type === "B" ||
|
|
641
|
+
s.type === "b" ||
|
|
642
|
+
s.type === "R" ||
|
|
643
|
+
s.type === "r"
|
|
644
|
+
);
|
|
645
|
+
const otherSymbols = result.symbols.filter(
|
|
646
|
+
(s) =>
|
|
647
|
+
!codeSymbols.includes(s) && !dataSymbols.includes(s)
|
|
648
|
+
);
|
|
294
649
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
650
|
+
const summary = [
|
|
651
|
+
`File: ${result.filePath}`,
|
|
652
|
+
`Defined symbols: ${result.totalCount}`,
|
|
653
|
+
"",
|
|
654
|
+
`Code symbols (${codeSymbols.length}):`,
|
|
655
|
+
...codeSymbols.slice(0, 30).map(
|
|
656
|
+
(s) => ` ${s.value} ${s.type} ${s.name}${s.size ? ` [${s.size}]` : ""}`
|
|
657
|
+
),
|
|
658
|
+
codeSymbols.length > 30
|
|
659
|
+
? ` ... and ${codeSymbols.length - 30} more`
|
|
660
|
+
: "",
|
|
661
|
+
"",
|
|
662
|
+
`Data symbols (${dataSymbols.length}):`,
|
|
663
|
+
...dataSymbols.slice(0, 20).map(
|
|
664
|
+
(s) => ` ${s.value} ${s.type} ${s.name}${s.size ? ` [${s.size}]` : ""}`
|
|
665
|
+
),
|
|
666
|
+
dataSymbols.length > 20
|
|
667
|
+
? ` ... and ${dataSymbols.length - 20} more`
|
|
668
|
+
: "",
|
|
669
|
+
"",
|
|
670
|
+
`Other symbols (${otherSymbols.length}):`,
|
|
671
|
+
...otherSymbols.slice(0, 10).map(
|
|
672
|
+
(s) => ` ${s.value} ${s.type} ${s.name}`
|
|
673
|
+
),
|
|
674
|
+
].join("\n");
|
|
310
675
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
(s) => s.type === "T" || s.type === "t" || s.type === "W" || s.type === "w"
|
|
314
|
-
);
|
|
315
|
-
const dataSymbols = result.symbols.filter(
|
|
316
|
-
(s) =>
|
|
317
|
-
s.type === "D" ||
|
|
318
|
-
s.type === "d" ||
|
|
319
|
-
s.type === "B" ||
|
|
320
|
-
s.type === "b" ||
|
|
321
|
-
s.type === "R" ||
|
|
322
|
-
s.type === "r"
|
|
323
|
-
);
|
|
324
|
-
const otherSymbols = result.symbols.filter(
|
|
325
|
-
(s) =>
|
|
326
|
-
!codeSymbols.includes(s) && !dataSymbols.includes(s)
|
|
327
|
-
);
|
|
676
|
+
return { content: [{ type: "text", text: summary }] };
|
|
677
|
+
}
|
|
328
678
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
(s) => ` ${s.value} ${s.type} ${s.name}${s.size ? ` [${s.size}]` : ""}`
|
|
336
|
-
),
|
|
337
|
-
codeSymbols.length > 30
|
|
338
|
-
? ` ... and ${codeSymbols.length - 30} more`
|
|
339
|
-
: "",
|
|
340
|
-
"",
|
|
341
|
-
`Data symbols (${dataSymbols.length}):`,
|
|
342
|
-
...dataSymbols.slice(0, 20).map(
|
|
343
|
-
(s) => ` ${s.value} ${s.type} ${s.name}${s.size ? ` [${s.size}]` : ""}`
|
|
344
|
-
),
|
|
345
|
-
dataSymbols.length > 20
|
|
346
|
-
? ` ... and ${dataSymbols.length - 20} more`
|
|
347
|
-
: "",
|
|
348
|
-
"",
|
|
349
|
-
`Other symbols (${otherSymbols.length}):`,
|
|
350
|
-
...otherSymbols.slice(0, 10).map(
|
|
351
|
-
(s) => ` ${s.value} ${s.type} ${s.name}`
|
|
352
|
-
),
|
|
353
|
-
].join("\n");
|
|
679
|
+
async function handleDynamicSymbols(args: { filePath: string; demangle?: boolean; definedOnly?: boolean }) {
|
|
680
|
+
const result = await runNm(args.filePath, {
|
|
681
|
+
dynamicSymbols: true,
|
|
682
|
+
demangle: args.demangle ?? true,
|
|
683
|
+
definedOnly: args.definedOnly ?? false,
|
|
684
|
+
});
|
|
354
685
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
686
|
+
const summary = [
|
|
687
|
+
`File: ${result.filePath}`,
|
|
688
|
+
`Dynamic symbols: ${result.totalCount}`,
|
|
689
|
+
"",
|
|
690
|
+
"Symbols:",
|
|
691
|
+
...result.symbols.slice(0, 100).map(
|
|
692
|
+
(s) => ` ${s.value} ${s.type} ${s.name}`
|
|
693
|
+
),
|
|
694
|
+
result.symbols.length > 100
|
|
695
|
+
? ` ... and ${result.symbols.length - 100} more`
|
|
696
|
+
: "",
|
|
697
|
+
].join("\n");
|
|
360
698
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
"nm_dynamic_symbols",
|
|
364
|
-
"List dynamic symbols from shared libraries (.so, .dylib)",
|
|
365
|
-
{
|
|
366
|
-
filePath: z.string().describe("Path to the shared library"),
|
|
367
|
-
demangle: z.boolean().optional().default(true),
|
|
368
|
-
definedOnly: z.boolean().optional().default(false),
|
|
369
|
-
},
|
|
370
|
-
async ({ filePath, demangle, definedOnly }) => {
|
|
371
|
-
const result = await runNm(filePath, {
|
|
372
|
-
dynamicSymbols: true,
|
|
373
|
-
demangle,
|
|
374
|
-
definedOnly,
|
|
375
|
-
});
|
|
699
|
+
return { content: [{ type: "text", text: summary }] };
|
|
700
|
+
}
|
|
376
701
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
`Dynamic symbols: ${result.totalCount}`,
|
|
380
|
-
"",
|
|
381
|
-
"Symbols:",
|
|
382
|
-
...result.symbols.slice(0, 100).map(
|
|
383
|
-
(s) => ` ${s.value} ${s.type} ${s.name}`
|
|
384
|
-
),
|
|
385
|
-
result.symbols.length > 100
|
|
386
|
-
? ` ... and ${result.symbols.length - 100} more`
|
|
387
|
-
: "",
|
|
388
|
-
].join("\n");
|
|
702
|
+
async function handleSearchSymbols(args: { filePath: string; pattern: string; demangle?: boolean; caseSensitive?: boolean }) {
|
|
703
|
+
const result = await runNm(args.filePath, { demangle: args.demangle ?? true });
|
|
389
704
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
);
|
|
705
|
+
const regex = new RegExp(
|
|
706
|
+
args.pattern,
|
|
707
|
+
(args.caseSensitive ?? false) ? "g" : "gi"
|
|
708
|
+
);
|
|
709
|
+
const matches = result.symbols.filter((s) => regex.test(s.name));
|
|
395
710
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
711
|
+
const summary = [
|
|
712
|
+
`File: ${result.filePath}`,
|
|
713
|
+
`Pattern: /${args.pattern}/${(args.caseSensitive ?? false) ? "" : "i"}`,
|
|
714
|
+
`Matches: ${matches.length} of ${result.totalCount} symbols`,
|
|
715
|
+
"",
|
|
716
|
+
"Matching symbols:",
|
|
717
|
+
...matches.map(
|
|
718
|
+
(s) =>
|
|
719
|
+
` ${s.value} ${s.type} ${s.name}${
|
|
720
|
+
SYMBOL_TYPE_DESCRIPTIONS[s.type]
|
|
721
|
+
? ` (${SYMBOL_TYPE_DESCRIPTIONS[s.type]})`
|
|
722
|
+
: ""
|
|
723
|
+
}`
|
|
724
|
+
),
|
|
725
|
+
].join("\n");
|
|
410
726
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
caseSensitive ? "g" : "gi"
|
|
414
|
-
);
|
|
415
|
-
const matches = result.symbols.filter((s) => regex.test(s.name));
|
|
727
|
+
return { content: [{ type: "text", text: summary }] };
|
|
728
|
+
}
|
|
416
729
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
...matches.map(
|
|
424
|
-
(s) =>
|
|
425
|
-
` ${s.value} ${s.type} ${s.name}${
|
|
426
|
-
SYMBOL_TYPE_DESCRIPTIONS[s.type]
|
|
427
|
-
? ` (${SYMBOL_TYPE_DESCRIPTIONS[s.type]})`
|
|
428
|
-
: ""
|
|
429
|
-
}`
|
|
430
|
-
),
|
|
431
|
-
].join("\n");
|
|
730
|
+
async function handleCompareBinaries(args: { file1: string; file2: string; demangle?: boolean }) {
|
|
731
|
+
const dm = args.demangle ?? true;
|
|
732
|
+
const [result1, result2] = await Promise.all([
|
|
733
|
+
runNm(args.file1, { demangle: dm, definedOnly: true }),
|
|
734
|
+
runNm(args.file2, { demangle: dm, definedOnly: true }),
|
|
735
|
+
]);
|
|
432
736
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
);
|
|
737
|
+
const symbols1 = new Set(result1.symbols.map((s) => s.name));
|
|
738
|
+
const symbols2 = new Set(result2.symbols.map((s) => s.name));
|
|
438
739
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
"Compare symbols between two binary files - find unique and shared symbols",
|
|
443
|
-
{
|
|
444
|
-
file1: z.string().describe("First binary file"),
|
|
445
|
-
file2: z.string().describe("Second binary file"),
|
|
446
|
-
demangle: z.boolean().optional().default(true),
|
|
447
|
-
},
|
|
448
|
-
async ({ file1, file2, demangle }) => {
|
|
449
|
-
const [result1, result2] = await Promise.all([
|
|
450
|
-
runNm(file1, { demangle, definedOnly: true }),
|
|
451
|
-
runNm(file2, { demangle, definedOnly: true }),
|
|
452
|
-
]);
|
|
740
|
+
const onlyIn1 = result1.symbols.filter((s) => !symbols2.has(s.name));
|
|
741
|
+
const onlyIn2 = result2.symbols.filter((s) => !symbols1.has(s.name));
|
|
742
|
+
const inBoth = result1.symbols.filter((s) => symbols2.has(s.name));
|
|
453
743
|
|
|
454
|
-
|
|
455
|
-
|
|
744
|
+
const summary = [
|
|
745
|
+
`Comparison: ${args.file1} vs ${args.file2}`,
|
|
746
|
+
"",
|
|
747
|
+
`File 1: ${result1.totalCount} symbols`,
|
|
748
|
+
`File 2: ${result2.totalCount} symbols`,
|
|
749
|
+
"",
|
|
750
|
+
`Shared symbols: ${inBoth.length}`,
|
|
751
|
+
`Only in file 1: ${onlyIn1.length}`,
|
|
752
|
+
`Only in file 2: ${onlyIn2.length}`,
|
|
753
|
+
"",
|
|
754
|
+
"Only in file 1:",
|
|
755
|
+
...onlyIn1.slice(0, 20).map((s) => ` - ${s.name}`),
|
|
756
|
+
onlyIn1.length > 20 ? ` ... and ${onlyIn1.length - 20} more` : "",
|
|
757
|
+
"",
|
|
758
|
+
"Only in file 2:",
|
|
759
|
+
...onlyIn2.slice(0, 20).map((s) => ` - ${s.name}`),
|
|
760
|
+
onlyIn2.length > 20 ? ` ... and ${onlyIn2.length - 20} more` : "",
|
|
761
|
+
].join("\n");
|
|
456
762
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
const inBoth = result1.symbols.filter((s) => symbols2.has(s.name));
|
|
763
|
+
return { content: [{ type: "text", text: summary }] };
|
|
764
|
+
}
|
|
460
765
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
"",
|
|
464
|
-
`File 1: ${result1.totalCount} symbols`,
|
|
465
|
-
`File 2: ${result2.totalCount} symbols`,
|
|
466
|
-
"",
|
|
467
|
-
`Shared symbols: ${inBoth.length}`,
|
|
468
|
-
`Only in file 1: ${onlyIn1.length}`,
|
|
469
|
-
`Only in file 2: ${onlyIn2.length}`,
|
|
470
|
-
"",
|
|
471
|
-
"Only in file 1:",
|
|
472
|
-
...onlyIn1.slice(0, 20).map((s) => ` - ${s.name}`),
|
|
473
|
-
onlyIn1.length > 20 ? ` ... and ${onlyIn1.length - 20} more` : "",
|
|
474
|
-
"",
|
|
475
|
-
"Only in file 2:",
|
|
476
|
-
...onlyIn2.slice(0, 20).map((s) => ` - ${s.name}`),
|
|
477
|
-
onlyIn2.length > 20 ? ` ... and ${onlyIn2.length - 20} more` : "",
|
|
478
|
-
].join("\n");
|
|
766
|
+
async function handleSymbolInfo(args: { filePath: string; symbolName: string; demangle?: boolean }) {
|
|
767
|
+
const result = await runNm(args.filePath, { demangle: args.demangle ?? true, showSize: true });
|
|
479
768
|
|
|
769
|
+
const matches = result.symbols.filter(
|
|
770
|
+
(s) =>
|
|
771
|
+
s.name === args.symbolName ||
|
|
772
|
+
s.name.includes(args.symbolName)
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
if (matches.length === 0) {
|
|
480
776
|
return {
|
|
481
|
-
content: [
|
|
777
|
+
content: [
|
|
778
|
+
{
|
|
779
|
+
type: "text",
|
|
780
|
+
text: `Symbol "${args.symbolName}" not found in ${args.filePath}`,
|
|
781
|
+
},
|
|
782
|
+
],
|
|
482
783
|
};
|
|
483
784
|
}
|
|
484
|
-
);
|
|
485
785
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
symbolName: z.string().describe("Name of the symbol to look up"),
|
|
493
|
-
demangle: z.boolean().optional().default(true),
|
|
494
|
-
},
|
|
495
|
-
async ({ filePath, symbolName, demangle }) => {
|
|
496
|
-
const result = await runNm(filePath, { demangle, showSize: true });
|
|
497
|
-
|
|
498
|
-
const matches = result.symbols.filter(
|
|
786
|
+
const summary = [
|
|
787
|
+
`Symbol: ${args.symbolName}`,
|
|
788
|
+
`File: ${args.filePath}`,
|
|
789
|
+
"",
|
|
790
|
+
"Matches:",
|
|
791
|
+
...matches.map(
|
|
499
792
|
(s) =>
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
793
|
+
[
|
|
794
|
+
` Name: ${s.name}`,
|
|
795
|
+
` Value: 0x${s.value}`,
|
|
796
|
+
` Type: ${s.type} (${SYMBOL_TYPE_DESCRIPTIONS[s.type] || "Unknown"})`,
|
|
797
|
+
s.size ? ` Size: ${s.size} bytes` : "",
|
|
798
|
+
"",
|
|
799
|
+
].join("\n")
|
|
800
|
+
),
|
|
801
|
+
].join("\n");
|
|
503
802
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
content: [
|
|
507
|
-
{
|
|
508
|
-
type: "text",
|
|
509
|
-
text: `Symbol "${symbolName}" not found in ${filePath}`,
|
|
510
|
-
},
|
|
511
|
-
],
|
|
512
|
-
};
|
|
513
|
-
}
|
|
803
|
+
return { content: [{ type: "text", text: summary }] };
|
|
804
|
+
}
|
|
514
805
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
`File: ${filePath}`,
|
|
518
|
-
"",
|
|
519
|
-
"Matches:",
|
|
520
|
-
...matches.map(
|
|
521
|
-
(s) =>
|
|
522
|
-
[
|
|
523
|
-
` Name: ${s.name}`,
|
|
524
|
-
` Value: 0x${s.value}`,
|
|
525
|
-
` Type: ${s.type} (${SYMBOL_TYPE_DESCRIPTIONS[s.type] || "Unknown"})`,
|
|
526
|
-
s.size ? ` Size: ${s.size} bytes` : "",
|
|
527
|
-
"",
|
|
528
|
-
].join("\n")
|
|
529
|
-
),
|
|
530
|
-
].join("\n");
|
|
806
|
+
async function handleSummary(args: { filePath: string }) {
|
|
807
|
+
const result = await runNm(args.filePath, { showSize: true });
|
|
531
808
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
809
|
+
// Count by type
|
|
810
|
+
const byType: Record<string, { count: number; totalSize: number }> = {};
|
|
811
|
+
for (const sym of result.symbols) {
|
|
812
|
+
const type = sym.type;
|
|
813
|
+
if (!byType[type]) byType[type] = { count: 0, totalSize: 0 };
|
|
814
|
+
byType[type].count++;
|
|
815
|
+
if (sym.size) {
|
|
816
|
+
byType[type].totalSize += parseInt(sym.size, 10);
|
|
817
|
+
}
|
|
535
818
|
}
|
|
536
|
-
);
|
|
537
819
|
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
820
|
+
// Classify
|
|
821
|
+
const external = result.symbols.filter(
|
|
822
|
+
(s) => s.type === s.type.toUpperCase() && s.type !== "?"
|
|
823
|
+
).length;
|
|
824
|
+
const local = result.symbols.filter(
|
|
825
|
+
(s) => s.type === s.type.toLowerCase()
|
|
826
|
+
).length;
|
|
827
|
+
const undefined = result.symbols.filter((s) => s.type === "U").length;
|
|
828
|
+
const weak = result.symbols.filter(
|
|
829
|
+
(s) => s.type === "W" || s.type === "w" || s.type === "V" || s.type === "v"
|
|
830
|
+
).length;
|
|
831
|
+
|
|
832
|
+
const summary = [
|
|
833
|
+
`Binary Summary: ${args.filePath}`,
|
|
834
|
+
"",
|
|
835
|
+
`Total symbols: ${result.totalCount}`,
|
|
836
|
+
`External: ${external}`,
|
|
837
|
+
`Local: ${local}`,
|
|
838
|
+
`Undefined: ${undefined}`,
|
|
839
|
+
`Weak: ${weak}`,
|
|
840
|
+
"",
|
|
841
|
+
"By symbol type:",
|
|
842
|
+
...Object.entries(byType)
|
|
843
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
844
|
+
.map(([type, data]) => {
|
|
845
|
+
const desc = SYMBOL_TYPE_DESCRIPTIONS[type] || "Unknown";
|
|
846
|
+
return ` ${type} (${desc}): ${data.count} symbols${
|
|
847
|
+
data.totalSize ? `, ${data.totalSize.toLocaleString()} bytes` : ""
|
|
848
|
+
}`;
|
|
849
|
+
}),
|
|
850
|
+
].join("\n");
|
|
851
|
+
|
|
852
|
+
return { content: [{ type: "text", text: summary }] };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// ============================================================================
|
|
856
|
+
// Tool definitions
|
|
857
|
+
// ============================================================================
|
|
858
|
+
|
|
859
|
+
const TOOLS = [
|
|
542
860
|
{
|
|
543
|
-
|
|
861
|
+
name: "nm_list_symbols",
|
|
862
|
+
description: "List all symbols in a binary file",
|
|
863
|
+
inputSchema: {
|
|
864
|
+
type: "object" as const,
|
|
865
|
+
properties: {
|
|
866
|
+
filePath: { type: "string", description: "Path to the binary file to analyze" },
|
|
867
|
+
demangle: { type: "boolean", description: "Demangle C++ symbol names" },
|
|
868
|
+
},
|
|
869
|
+
required: ["filePath"],
|
|
870
|
+
},
|
|
544
871
|
},
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
872
|
+
{
|
|
873
|
+
name: "nm_external_symbols",
|
|
874
|
+
description: "List external (global) symbols in a binary file - symbols visible to other modules",
|
|
875
|
+
inputSchema: {
|
|
876
|
+
type: "object" as const,
|
|
877
|
+
properties: {
|
|
878
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
879
|
+
demangle: { type: "boolean", description: "Demangle C++ symbol names" },
|
|
880
|
+
showSize: { type: "boolean", description: "Show symbol sizes" },
|
|
881
|
+
},
|
|
882
|
+
required: ["filePath"],
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
name: "nm_undefined_symbols",
|
|
887
|
+
description: "List undefined symbols in a binary - these are external dependencies the binary needs",
|
|
888
|
+
inputSchema: {
|
|
889
|
+
type: "object" as const,
|
|
890
|
+
properties: {
|
|
891
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
892
|
+
demangle: { type: "boolean", description: "Demangle C++ symbol names" },
|
|
893
|
+
},
|
|
894
|
+
required: ["filePath"],
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
name: "nm_defined_symbols",
|
|
899
|
+
description: "List defined symbols in a binary - symbols this binary provides to others",
|
|
900
|
+
inputSchema: {
|
|
901
|
+
type: "object" as const,
|
|
902
|
+
properties: {
|
|
903
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
904
|
+
demangle: { type: "boolean", description: "Demangle C++ symbol names" },
|
|
905
|
+
showSize: { type: "boolean", description: "Show symbol sizes" },
|
|
906
|
+
},
|
|
907
|
+
required: ["filePath"],
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
name: "nm_dynamic_symbols",
|
|
912
|
+
description: "List dynamic symbols from shared libraries (.so, .dylib)",
|
|
913
|
+
inputSchema: {
|
|
914
|
+
type: "object" as const,
|
|
915
|
+
properties: {
|
|
916
|
+
filePath: { type: "string", description: "Path to the shared library" },
|
|
917
|
+
demangle: { type: "boolean", description: "Demangle C++ symbol names" },
|
|
918
|
+
definedOnly: { type: "boolean", description: "Show only defined symbols" },
|
|
919
|
+
},
|
|
920
|
+
required: ["filePath"],
|
|
921
|
+
},
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
name: "nm_search_symbols",
|
|
925
|
+
description: "Search for symbols matching a pattern in a binary",
|
|
926
|
+
inputSchema: {
|
|
927
|
+
type: "object" as const,
|
|
928
|
+
properties: {
|
|
929
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
930
|
+
pattern: { type: "string", description: "Regex pattern to search for in symbol names" },
|
|
931
|
+
demangle: { type: "boolean", description: "Demangle C++ symbol names" },
|
|
932
|
+
caseSensitive: { type: "boolean", description: "Case sensitive search" },
|
|
933
|
+
},
|
|
934
|
+
required: ["filePath", "pattern"],
|
|
935
|
+
},
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
name: "nm_compare_binaries",
|
|
939
|
+
description: "Compare symbols between two binary files - find unique and shared symbols",
|
|
940
|
+
inputSchema: {
|
|
941
|
+
type: "object" as const,
|
|
942
|
+
properties: {
|
|
943
|
+
file1: { type: "string", description: "First binary file" },
|
|
944
|
+
file2: { type: "string", description: "Second binary file" },
|
|
945
|
+
demangle: { type: "boolean", description: "Demangle C++ symbol names" },
|
|
946
|
+
},
|
|
947
|
+
required: ["file1", "file2"],
|
|
948
|
+
},
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
name: "nm_symbol_info",
|
|
952
|
+
description: "Get detailed information about a specific symbol by name",
|
|
953
|
+
inputSchema: {
|
|
954
|
+
type: "object" as const,
|
|
955
|
+
properties: {
|
|
956
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
957
|
+
symbolName: { type: "string", description: "Name of the symbol to look up" },
|
|
958
|
+
demangle: { type: "boolean", description: "Demangle C++ symbol names" },
|
|
959
|
+
},
|
|
960
|
+
required: ["filePath", "symbolName"],
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
name: "nm_summary",
|
|
965
|
+
description: "Get a quick summary of a binary's symbol table - counts by type",
|
|
966
|
+
inputSchema: {
|
|
967
|
+
type: "object" as const,
|
|
968
|
+
properties: {
|
|
969
|
+
filePath: { type: "string", description: "Path to the binary file" },
|
|
970
|
+
},
|
|
971
|
+
required: ["filePath"],
|
|
972
|
+
},
|
|
973
|
+
},
|
|
974
|
+
// xxd tools
|
|
975
|
+
{
|
|
976
|
+
name: "xxd_hexdump",
|
|
977
|
+
description: "Create a hex dump of a file with offset and ASCII representation",
|
|
978
|
+
inputSchema: {
|
|
979
|
+
type: "object" as const,
|
|
980
|
+
properties: {
|
|
981
|
+
filePath: { type: "string", description: "Path to the file to dump" },
|
|
982
|
+
length: { type: "number", description: "Number of bytes to read" },
|
|
983
|
+
seek: { type: "number", description: "Start offset in bytes" },
|
|
984
|
+
cols: { type: "number", description: "Columns (bytes) per line (default: 16)" },
|
|
985
|
+
groupSize: { type: "number", description: "Number of bytes to group together (default: 2)" },
|
|
986
|
+
uppercase: { type: "boolean", description: "Use uppercase hex letters" },
|
|
987
|
+
},
|
|
988
|
+
required: ["filePath"],
|
|
989
|
+
},
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
name: "xxd_plain",
|
|
993
|
+
description: "Output continuous plain hex string (no offsets or ASCII)",
|
|
994
|
+
inputSchema: {
|
|
995
|
+
type: "object" as const,
|
|
996
|
+
properties: {
|
|
997
|
+
filePath: { type: "string", description: "Path to the file" },
|
|
998
|
+
length: { type: "number", description: "Number of bytes to read" },
|
|
999
|
+
seek: { type: "number", description: "Start offset in bytes" },
|
|
1000
|
+
uppercase: { type: "boolean", description: "Use uppercase hex letters" },
|
|
1001
|
+
},
|
|
1002
|
+
required: ["filePath"],
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
name: "xxd_c_include",
|
|
1007
|
+
description: "Output in C include file style (unsigned char array)",
|
|
1008
|
+
inputSchema: {
|
|
1009
|
+
type: "object" as const,
|
|
1010
|
+
properties: {
|
|
1011
|
+
filePath: { type: "string", description: "Path to the file" },
|
|
1012
|
+
length: { type: "number", description: "Number of bytes to read" },
|
|
1013
|
+
seek: { type: "number", description: "Start offset in bytes" },
|
|
1014
|
+
variableName: { type: "string", description: "Custom variable name for the array" },
|
|
1015
|
+
},
|
|
1016
|
+
required: ["filePath"],
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
name: "xxd_binary",
|
|
1021
|
+
description: "Output binary digit dump (ones and zeros)",
|
|
1022
|
+
inputSchema: {
|
|
1023
|
+
type: "object" as const,
|
|
1024
|
+
properties: {
|
|
1025
|
+
filePath: { type: "string", description: "Path to the file" },
|
|
1026
|
+
length: { type: "number", description: "Number of bytes to read" },
|
|
1027
|
+
seek: { type: "number", description: "Start offset in bytes" },
|
|
1028
|
+
cols: { type: "number", description: "Columns per line (default: 8)" },
|
|
1029
|
+
},
|
|
1030
|
+
required: ["filePath"],
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
name: "xxd_reverse",
|
|
1035
|
+
description: "Convert hex string back to binary and save to file",
|
|
1036
|
+
inputSchema: {
|
|
1037
|
+
type: "object" as const,
|
|
1038
|
+
properties: {
|
|
1039
|
+
inputHex: { type: "string", description: "Hex string to convert" },
|
|
1040
|
+
outputPath: { type: "string", description: "Path to save the binary output" },
|
|
1041
|
+
},
|
|
1042
|
+
required: ["inputHex", "outputPath"],
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
name: "xxd_extract",
|
|
1047
|
+
description: "Extract bytes at a specific offset with multiple format options",
|
|
1048
|
+
inputSchema: {
|
|
1049
|
+
type: "object" as const,
|
|
1050
|
+
properties: {
|
|
1051
|
+
filePath: { type: "string", description: "Path to the file" },
|
|
1052
|
+
offset: { type: "number", description: "Byte offset to start reading" },
|
|
1053
|
+
length: { type: "number", description: "Number of bytes to extract" },
|
|
1054
|
+
format: {
|
|
1055
|
+
type: "string",
|
|
1056
|
+
enum: ["hex", "binary", "decimal", "c-array"],
|
|
1057
|
+
description: "Output format (default: hex)",
|
|
1058
|
+
},
|
|
1059
|
+
},
|
|
1060
|
+
required: ["filePath", "offset", "length"],
|
|
1061
|
+
},
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
name: "xxd_find_pattern",
|
|
1065
|
+
description: "Search for a byte pattern in a file (hex or text)",
|
|
1066
|
+
inputSchema: {
|
|
1067
|
+
type: "object" as const,
|
|
1068
|
+
properties: {
|
|
1069
|
+
filePath: { type: "string", description: "Path to the file to search" },
|
|
1070
|
+
pattern: { type: "string", description: "Pattern to search for" },
|
|
1071
|
+
patternFormat: {
|
|
1072
|
+
type: "string",
|
|
1073
|
+
enum: ["hex", "text"],
|
|
1074
|
+
description: "Format of the pattern (default: text)",
|
|
1075
|
+
},
|
|
1076
|
+
maxLength: { type: "number", description: "Maximum bytes to search (for large files)" },
|
|
1077
|
+
},
|
|
1078
|
+
required: ["filePath", "pattern"],
|
|
1079
|
+
},
|
|
1080
|
+
},
|
|
1081
|
+
];
|
|
558
1082
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
).length;
|
|
563
|
-
const local = result.symbols.filter(
|
|
564
|
-
(s) => s.type === s.type.toLowerCase()
|
|
565
|
-
).length;
|
|
566
|
-
const undefined = result.symbols.filter((s) => s.type === "U").length;
|
|
567
|
-
const weak = result.symbols.filter(
|
|
568
|
-
(s) => s.type === "W" || s.type === "w" || s.type === "V" || s.type === "v"
|
|
569
|
-
).length;
|
|
1083
|
+
// ============================================================================
|
|
1084
|
+
// Server handlers
|
|
1085
|
+
// ============================================================================
|
|
570
1086
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
`Total symbols: ${result.totalCount}`,
|
|
575
|
-
`External: ${external}`,
|
|
576
|
-
`Local: ${local}`,
|
|
577
|
-
`Undefined: ${undefined}`,
|
|
578
|
-
`Weak: ${weak}`,
|
|
579
|
-
"",
|
|
580
|
-
"By symbol type:",
|
|
581
|
-
...Object.entries(byType)
|
|
582
|
-
.sort((a, b) => b[1].count - a[1].count)
|
|
583
|
-
.map(([type, data]) => {
|
|
584
|
-
const desc = SYMBOL_TYPE_DESCRIPTIONS[type] || "Unknown";
|
|
585
|
-
return ` ${type} (${desc}): ${data.count} symbols${
|
|
586
|
-
data.totalSize ? `, ${data.totalSize.toLocaleString()} bytes` : ""
|
|
587
|
-
}`;
|
|
588
|
-
}),
|
|
589
|
-
].join("\n");
|
|
1087
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1088
|
+
return { tools: TOOLS };
|
|
1089
|
+
});
|
|
590
1090
|
|
|
1091
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1092
|
+
const { name, arguments: args } = request.params;
|
|
1093
|
+
|
|
1094
|
+
try {
|
|
1095
|
+
switch (name) {
|
|
1096
|
+
case "nm_list_symbols":
|
|
1097
|
+
return await handleListSymbols(args as { filePath: string; demangle?: boolean });
|
|
1098
|
+
case "nm_external_symbols":
|
|
1099
|
+
return await handleExternalSymbols(args as { filePath: string; demangle?: boolean; showSize?: boolean });
|
|
1100
|
+
case "nm_undefined_symbols":
|
|
1101
|
+
return await handleUndefinedSymbols(args as { filePath: string; demangle?: boolean });
|
|
1102
|
+
case "nm_defined_symbols":
|
|
1103
|
+
return await handleDefinedSymbols(args as { filePath: string; demangle?: boolean; showSize?: boolean });
|
|
1104
|
+
case "nm_dynamic_symbols":
|
|
1105
|
+
return await handleDynamicSymbols(args as { filePath: string; demangle?: boolean; definedOnly?: boolean });
|
|
1106
|
+
case "nm_search_symbols":
|
|
1107
|
+
return await handleSearchSymbols(args as { filePath: string; pattern: string; demangle?: boolean; caseSensitive?: boolean });
|
|
1108
|
+
case "nm_compare_binaries":
|
|
1109
|
+
return await handleCompareBinaries(args as { file1: string; file2: string; demangle?: boolean });
|
|
1110
|
+
case "nm_symbol_info":
|
|
1111
|
+
return await handleSymbolInfo(args as { filePath: string; symbolName: string; demangle?: boolean });
|
|
1112
|
+
case "nm_summary":
|
|
1113
|
+
return await handleSummary(args as { filePath: string });
|
|
1114
|
+
// xxd handlers
|
|
1115
|
+
case "xxd_hexdump":
|
|
1116
|
+
return await handleXxdHexdump(args as {
|
|
1117
|
+
filePath: string;
|
|
1118
|
+
length?: number;
|
|
1119
|
+
seek?: number;
|
|
1120
|
+
cols?: number;
|
|
1121
|
+
groupSize?: number;
|
|
1122
|
+
uppercase?: boolean;
|
|
1123
|
+
});
|
|
1124
|
+
case "xxd_plain":
|
|
1125
|
+
return await handleXxdPlain(args as {
|
|
1126
|
+
filePath: string;
|
|
1127
|
+
length?: number;
|
|
1128
|
+
seek?: number;
|
|
1129
|
+
uppercase?: boolean;
|
|
1130
|
+
});
|
|
1131
|
+
case "xxd_c_include":
|
|
1132
|
+
return await handleXxdCInclude(args as {
|
|
1133
|
+
filePath: string;
|
|
1134
|
+
length?: number;
|
|
1135
|
+
seek?: number;
|
|
1136
|
+
variableName?: string;
|
|
1137
|
+
});
|
|
1138
|
+
case "xxd_binary":
|
|
1139
|
+
return await handleXxdBinary(args as {
|
|
1140
|
+
filePath: string;
|
|
1141
|
+
length?: number;
|
|
1142
|
+
seek?: number;
|
|
1143
|
+
cols?: number;
|
|
1144
|
+
});
|
|
1145
|
+
case "xxd_reverse":
|
|
1146
|
+
return await handleXxdReverse(args as {
|
|
1147
|
+
inputHex: string;
|
|
1148
|
+
outputPath: string;
|
|
1149
|
+
});
|
|
1150
|
+
case "xxd_extract":
|
|
1151
|
+
return await handleXxdExtract(args as {
|
|
1152
|
+
filePath: string;
|
|
1153
|
+
offset: number;
|
|
1154
|
+
length: number;
|
|
1155
|
+
format?: "hex" | "binary" | "decimal" | "c-array";
|
|
1156
|
+
});
|
|
1157
|
+
case "xxd_find_pattern":
|
|
1158
|
+
return await handleXxdFindPattern(args as {
|
|
1159
|
+
filePath: string;
|
|
1160
|
+
pattern: string;
|
|
1161
|
+
patternFormat?: "hex" | "text";
|
|
1162
|
+
maxLength?: number;
|
|
1163
|
+
});
|
|
1164
|
+
default:
|
|
1165
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
1166
|
+
}
|
|
1167
|
+
} catch (error) {
|
|
591
1168
|
return {
|
|
592
|
-
content: [
|
|
1169
|
+
content: [
|
|
1170
|
+
{
|
|
1171
|
+
type: "text",
|
|
1172
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
1173
|
+
},
|
|
1174
|
+
],
|
|
1175
|
+
isError: true,
|
|
593
1176
|
};
|
|
594
1177
|
}
|
|
595
|
-
);
|
|
1178
|
+
});
|
|
596
1179
|
|
|
597
1180
|
// ============================================================================
|
|
598
1181
|
// Start server
|