@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.
Files changed (4) hide show
  1. package/dist/index.js +4433 -9723
  2. package/package.json +8 -4
  3. package/src/index.ts +976 -393
  4. 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 symbol analysis MCP server
3
+ * @ebowwa/nm-mcp - Binary analysis MCP server
4
4
  *
5
- * Provides tools for analyzing symbols in ELF/Mach-O binaries using nm
5
+ * Provides tools for analyzing binaries using nm (symbols) and xxd (hex dumps)
6
6
  */
7
7
 
8
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
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 McpServer({
18
- name: "@ebowwa/nm-mcp",
19
- version: "1.0.0",
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
- // Tools
541
+ // Tool handlers
166
542
  // ============================================================================
167
543
 
168
- // nm_list_symbols - Basic symbol listing
169
- server.tool(
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
- const summary = [
184
- `File: ${result.filePath}`,
185
- `Total symbols: ${result.totalCount}`,
186
- "",
187
- "Symbols:",
188
- ...result.symbols.slice(0, 100).map(
189
- (s) =>
190
- ` ${s.value} ${s.type} ${s.name}${
191
- SYMBOL_TYPE_DESCRIPTIONS[s.type]
192
- ? ` (${SYMBOL_TYPE_DESCRIPTIONS[s.type]})`
193
- : ""
194
- }`
195
- ),
196
- result.symbols.length > 100
197
- ? ` ... and ${result.symbols.length - 100} more`
198
- : "",
199
- ].join("\n");
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
- return {
202
- content: [{ type: "text", text: summary }],
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
- // nm_external_symbols - List only external (global) symbols
208
- server.tool(
209
- "nm_external_symbols",
210
- "List external (global) symbols in a binary file - symbols visible to other modules",
211
- {
212
- filePath: z.string().describe("Path to the binary file"),
213
- demangle: z
214
- .boolean()
215
- .optional()
216
- .default(true)
217
- .describe("Demangle C++ symbol names"),
218
- showSize: z
219
- .boolean()
220
- .optional()
221
- .default(false)
222
- .describe("Show symbol sizes"),
223
- },
224
- async ({ filePath, demangle, showSize }) => {
225
- const result = await runNm(filePath, {
226
- externalOnly: true,
227
- demangle,
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
- // Group by type
232
- const byType: Record<string, NmSymbol[]> = {};
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
- const summary = [
240
- `File: ${result.filePath}`,
241
- `External symbols: ${result.totalCount}`,
242
- "",
243
- "By type:",
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
- return {
262
- content: [{ type: "text", text: summary }],
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
- // nm_undefined_symbols - List undefined symbols (imports)
268
- server.tool(
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
- const summary = [
282
- `File: ${result.filePath}`,
283
- `Undefined symbols (imports): ${result.totalCount}`,
284
- "",
285
- "Dependencies:",
286
- ...result.symbols.map((s) => ` - ${s.name}`),
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
- return {
290
- content: [{ type: "text", text: summary }],
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
- // nm_defined_symbols - List defined symbols (exports)
296
- server.tool(
297
- "nm_defined_symbols",
298
- "List defined symbols in a binary - symbols this binary provides to others",
299
- {
300
- filePath: z.string().describe("Path to the binary file"),
301
- demangle: z.boolean().optional().default(true),
302
- showSize: z.boolean().optional().default(true),
303
- },
304
- async ({ filePath, demangle, showSize }) => {
305
- const result = await runNm(filePath, {
306
- definedOnly: true,
307
- demangle,
308
- showSize,
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
- // Separate code and data symbols
312
- const codeSymbols = result.symbols.filter(
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
- const summary = [
330
- `File: ${result.filePath}`,
331
- `Defined symbols: ${result.totalCount}`,
332
- "",
333
- `Code symbols (${codeSymbols.length}):`,
334
- ...codeSymbols.slice(0, 30).map(
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
- return {
356
- content: [{ type: "text", text: summary }],
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
- // nm_dynamic_symbols - List dynamic symbols (shared libraries)
362
- server.tool(
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
- const summary = [
378
- `File: ${result.filePath}`,
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
- return {
391
- content: [{ type: "text", text: summary }],
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
- // nm_search_symbols - Search for specific symbols
397
- server.tool(
398
- "nm_search_symbols",
399
- "Search for symbols matching a pattern in a binary",
400
- {
401
- filePath: z.string().describe("Path to the binary file"),
402
- pattern: z
403
- .string()
404
- .describe("Regex pattern to search for in symbol names"),
405
- demangle: z.boolean().optional().default(true),
406
- caseSensitive: z.boolean().optional().default(false),
407
- },
408
- async ({ filePath, pattern, demangle, caseSensitive }) => {
409
- const result = await runNm(filePath, { demangle });
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
- const regex = new RegExp(
412
- pattern,
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
- const summary = [
418
- `File: ${result.filePath}`,
419
- `Pattern: /${pattern}/${caseSensitive ? "" : "i"}`,
420
- `Matches: ${matches.length} of ${result.totalCount} symbols`,
421
- "",
422
- "Matching symbols:",
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
- return {
434
- content: [{ type: "text", text: summary }],
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
- // nm_compare_binaries - Compare symbols between two binaries
440
- server.tool(
441
- "nm_compare_binaries",
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
- const symbols1 = new Set(result1.symbols.map((s) => s.name));
455
- const symbols2 = new Set(result2.symbols.map((s) => s.name));
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
- const onlyIn1 = result1.symbols.filter((s) => !symbols2.has(s.name));
458
- const onlyIn2 = result2.symbols.filter((s) => !symbols1.has(s.name));
459
- const inBoth = result1.symbols.filter((s) => symbols2.has(s.name));
763
+ return { content: [{ type: "text", text: summary }] };
764
+ }
460
765
 
461
- const summary = [
462
- `Comparison: ${file1} vs ${file2}`,
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: [{ type: "text", text: summary }],
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
- // nm_symbol_info - Get detailed info about a specific symbol
487
- server.tool(
488
- "nm_symbol_info",
489
- "Get detailed information about a specific symbol by name",
490
- {
491
- filePath: z.string().describe("Path to the binary file"),
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
- s.name === symbolName ||
501
- s.name.includes(symbolName)
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
- if (matches.length === 0) {
505
- return {
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
- const summary = [
516
- `Symbol: ${symbolName}`,
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
- return {
533
- content: [{ type: "text", text: summary }],
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
- // nm_summary - Quick binary overview
539
- server.tool(
540
- "nm_summary",
541
- "Get a quick summary of a binary's symbol table - counts by type",
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
- filePath: z.string().describe("Path to the binary file"),
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
- async ({ filePath }) => {
546
- const result = await runNm(filePath, { showSize: true });
547
-
548
- // Count by type
549
- const byType: Record<string, { count: number; totalSize: number }> = {};
550
- for (const sym of result.symbols) {
551
- const type = sym.type;
552
- if (!byType[type]) byType[type] = { count: 0, totalSize: 0 };
553
- byType[type].count++;
554
- if (sym.size) {
555
- byType[type].totalSize += parseInt(sym.size, 10);
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
- // Classify
560
- const external = result.symbols.filter(
561
- (s) => s.type === s.type.toUpperCase() && s.type !== "?"
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
- const summary = [
572
- `Binary Summary: ${filePath}`,
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: [{ type: "text", text: summary }],
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