@ebowwa/mcp-nm 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +233 -0
- package/dist/index.js +19770 -0
- package/package.json +48 -0
- package/src/index.ts +610 -0
- package/tsconfig.json +19 -0
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ebowwa/mcp-nm",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Binary symbol analysis MCP server using nm tool - analyze exports, imports, and symbols in ELF/Mach-O binaries",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"nm-mcp": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "bun build src/index.ts --outdir dist --target node && chmod +x dist/index.js",
|
|
13
|
+
"dev": "bun run src/index.ts",
|
|
14
|
+
"watch": "bun build src/index.ts --outdir dist --target node --watch",
|
|
15
|
+
"prepublishOnly": "bun run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"nm",
|
|
20
|
+
"binary",
|
|
21
|
+
"symbols",
|
|
22
|
+
"elf",
|
|
23
|
+
"mach-o",
|
|
24
|
+
"analysis",
|
|
25
|
+
"reverse-engineering",
|
|
26
|
+
"abi"
|
|
27
|
+
],
|
|
28
|
+
"author": "ebowwa",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
32
|
+
"zod": "^3.22.4"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.0.0",
|
|
36
|
+
"typescript": "^5.0.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"bun": ">=1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"ownership": {
|
|
42
|
+
"domain": "mcp",
|
|
43
|
+
"responsibilities": [
|
|
44
|
+
"network-manager-mcp",
|
|
45
|
+
"network-configuration"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @ebowwa/nm-mcp - Binary symbol analysis MCP server
|
|
4
|
+
*
|
|
5
|
+
* Provides tools for analyzing symbols in ELF/Mach-O binaries using nm
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
import { exec } from "child_process";
|
|
11
|
+
import { promisify } from "util";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
|
|
16
|
+
// Server instance
|
|
17
|
+
const server = new McpServer({
|
|
18
|
+
name: "@ebowwa/nm-mcp",
|
|
19
|
+
version: "1.0.0",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Types
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
interface NmSymbol {
|
|
27
|
+
value: string;
|
|
28
|
+
type: string;
|
|
29
|
+
name: string;
|
|
30
|
+
size?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface NmResult {
|
|
34
|
+
filePath: string;
|
|
35
|
+
symbols: NmSymbol[];
|
|
36
|
+
totalCount: number;
|
|
37
|
+
filteredCount: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// nm execution
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
async function runNm(
|
|
45
|
+
filePath: string,
|
|
46
|
+
options: {
|
|
47
|
+
externalOnly?: boolean;
|
|
48
|
+
undefinedOnly?: boolean;
|
|
49
|
+
definedOnly?: boolean;
|
|
50
|
+
dynamicSymbols?: boolean;
|
|
51
|
+
showSize?: boolean;
|
|
52
|
+
demangle?: boolean;
|
|
53
|
+
noSort?: boolean;
|
|
54
|
+
reverseSort?: boolean;
|
|
55
|
+
radix?: "hex" | "decimal" | "octal";
|
|
56
|
+
} = {}
|
|
57
|
+
): Promise<NmResult> {
|
|
58
|
+
const args: string[] = [];
|
|
59
|
+
|
|
60
|
+
// Symbol filtering
|
|
61
|
+
if (options.externalOnly) args.push("-g");
|
|
62
|
+
if (options.undefinedOnly) args.push("-u");
|
|
63
|
+
if (options.definedOnly) args.push("--defined-only");
|
|
64
|
+
if (options.dynamicSymbols) args.push("-D");
|
|
65
|
+
|
|
66
|
+
// Output formatting
|
|
67
|
+
if (options.showSize) args.push("-S");
|
|
68
|
+
if (options.demangle) args.push("--demangle");
|
|
69
|
+
if (options.noSort) args.push("-p");
|
|
70
|
+
if (options.reverseSort) args.push("-r");
|
|
71
|
+
|
|
72
|
+
// Radix for values
|
|
73
|
+
if (options.radix === "hex") args.push("-t");
|
|
74
|
+
if (options.radix === "decimal") args.push("-t");
|
|
75
|
+
if (options.radix === "octal") args.push("-to");
|
|
76
|
+
|
|
77
|
+
// Build command
|
|
78
|
+
const cmd = `nm ${args.join(" ")} "${filePath}"`;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
82
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (stderr && !stdout) {
|
|
86
|
+
throw new Error(stderr);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Parse nm output
|
|
90
|
+
const symbols: NmSymbol[] = stdout
|
|
91
|
+
.trim()
|
|
92
|
+
.split("\n")
|
|
93
|
+
.filter((line) => line.trim())
|
|
94
|
+
.map((line) => {
|
|
95
|
+
// nm output format: "value type name" or " type name" (for undefined)
|
|
96
|
+
const match = line.match(
|
|
97
|
+
/^\s*([0-9a-fA-F]+)?\s+([a-zA-Z?])\s+(.+?)(?:\s+(\d+))?$/
|
|
98
|
+
);
|
|
99
|
+
if (match) {
|
|
100
|
+
return {
|
|
101
|
+
value: match[1] || "0",
|
|
102
|
+
type: match[2],
|
|
103
|
+
name: match[3],
|
|
104
|
+
size: match[4],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Alternative format (just symbol name for some cases)
|
|
108
|
+
const parts = line.trim().split(/\s+/);
|
|
109
|
+
if (parts.length >= 2) {
|
|
110
|
+
return {
|
|
111
|
+
value: parts[0] === "U" ? "0" : parts[0] || "0",
|
|
112
|
+
type: parts.length >= 2 ? parts[parts.length - 2] : "?",
|
|
113
|
+
name: parts[parts.length - 1] || "",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { value: "0", type: "?", name: line.trim() };
|
|
117
|
+
})
|
|
118
|
+
.filter((s) => s.name);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
filePath,
|
|
122
|
+
symbols,
|
|
123
|
+
totalCount: symbols.length,
|
|
124
|
+
filteredCount: symbols.length,
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`nm command failed: ${error instanceof Error ? error.message : error}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================================
|
|
134
|
+
// Symbol type descriptions
|
|
135
|
+
// ============================================================================
|
|
136
|
+
|
|
137
|
+
const SYMBOL_TYPE_DESCRIPTIONS: Record<string, string> = {
|
|
138
|
+
A: "Global absolute symbol",
|
|
139
|
+
B: "Global BSS (uninitialized data)",
|
|
140
|
+
b: "Local BSS (uninitialized data)",
|
|
141
|
+
C: "Common symbol (uninitialized)",
|
|
142
|
+
D: "Global initialized data",
|
|
143
|
+
d: "Local initialized data",
|
|
144
|
+
G: "Global small initialized data",
|
|
145
|
+
g: "Local small initialized data",
|
|
146
|
+
i: "Indirect function reference",
|
|
147
|
+
N: "Debugging symbol",
|
|
148
|
+
p: "Stack unwind section",
|
|
149
|
+
R: "Global read-only data",
|
|
150
|
+
r: "Local read-only data",
|
|
151
|
+
S: "Global small BSS",
|
|
152
|
+
s: "Local small BSS",
|
|
153
|
+
T: "Global text (code)",
|
|
154
|
+
t: "Local text (code)",
|
|
155
|
+
U: "Undefined symbol",
|
|
156
|
+
u: "Unique global symbol",
|
|
157
|
+
V: "Weak object",
|
|
158
|
+
v: "Weak object",
|
|
159
|
+
W: "Weak symbol",
|
|
160
|
+
w: "Weak symbol",
|
|
161
|
+
"?": "Unknown symbol type",
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// Tools
|
|
166
|
+
// ============================================================================
|
|
167
|
+
|
|
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 });
|
|
182
|
+
|
|
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");
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: "text", text: summary }],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
|
|
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
|
+
});
|
|
230
|
+
|
|
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
|
+
}
|
|
238
|
+
|
|
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");
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
content: [{ type: "text", text: summary }],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
|
|
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
|
+
});
|
|
280
|
+
|
|
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");
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
content: [{ type: "text", text: summary }],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
|
|
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
|
+
});
|
|
310
|
+
|
|
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
|
+
);
|
|
328
|
+
|
|
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");
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
content: [{ type: "text", text: summary }],
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
|
|
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
|
+
});
|
|
376
|
+
|
|
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");
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
content: [{ type: "text", text: summary }],
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
|
|
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 });
|
|
410
|
+
|
|
411
|
+
const regex = new RegExp(
|
|
412
|
+
pattern,
|
|
413
|
+
caseSensitive ? "g" : "gi"
|
|
414
|
+
);
|
|
415
|
+
const matches = result.symbols.filter((s) => regex.test(s.name));
|
|
416
|
+
|
|
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");
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
content: [{ type: "text", text: summary }],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
);
|
|
438
|
+
|
|
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
|
+
]);
|
|
453
|
+
|
|
454
|
+
const symbols1 = new Set(result1.symbols.map((s) => s.name));
|
|
455
|
+
const symbols2 = new Set(result2.symbols.map((s) => s.name));
|
|
456
|
+
|
|
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));
|
|
460
|
+
|
|
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");
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
content: [{ type: "text", text: summary }],
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
|
|
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(
|
|
499
|
+
(s) =>
|
|
500
|
+
s.name === symbolName ||
|
|
501
|
+
s.name.includes(symbolName)
|
|
502
|
+
);
|
|
503
|
+
|
|
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
|
+
}
|
|
514
|
+
|
|
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");
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
content: [{ type: "text", text: summary }],
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
);
|
|
537
|
+
|
|
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",
|
|
542
|
+
{
|
|
543
|
+
filePath: z.string().describe("Path to the binary file"),
|
|
544
|
+
},
|
|
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
|
+
}
|
|
558
|
+
|
|
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;
|
|
570
|
+
|
|
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");
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
content: [{ type: "text", text: summary }],
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
// ============================================================================
|
|
598
|
+
// Start server
|
|
599
|
+
// ============================================================================
|
|
600
|
+
|
|
601
|
+
async function main() {
|
|
602
|
+
const transport = new StdioServerTransport();
|
|
603
|
+
await server.connect(transport);
|
|
604
|
+
console.error("nm-mcp server running on stdio");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
main().catch((error) => {
|
|
608
|
+
console.error("Fatal error:", error);
|
|
609
|
+
process.exit(1);
|
|
610
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"allowSyntheticDefaultImports": true,
|
|
14
|
+
"outDir": "./dist",
|
|
15
|
+
"rootDir": "./src"
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|