@ebowwa/mcp-nm 2.1.0 → 2.2.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 +2634 -2851
- package/package.json +1 -1
- package/src/handlers/index.ts +72 -0
- package/src/handlers/macos.ts +611 -0
- package/src/handlers/patch.ts +535 -0
- package/src/index.ts +137 -3901
- package/src/tools/index.ts +682 -0
- package/src/utils/convert.ts +4 -4
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ebowwa/mcp-nm - Binary patching handlers
|
|
3
|
+
*
|
|
4
|
+
* MCP tool handlers for binary patching operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execAsync, isMacOS } from "../utils/exec";
|
|
8
|
+
import { hexToBytes } from "../utils/xxd";
|
|
9
|
+
import { runXxd } from "../utils/xxd";
|
|
10
|
+
import { convertNumber } from "../utils/convert";
|
|
11
|
+
import type { McpResponse } from "../types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Patch bytes at offset
|
|
15
|
+
*/
|
|
16
|
+
export async function handlePatchBytes(args: {
|
|
17
|
+
filePath: string;
|
|
18
|
+
offset: number;
|
|
19
|
+
hexData: string;
|
|
20
|
+
createBackup?: boolean;
|
|
21
|
+
}): Promise<McpResponse> {
|
|
22
|
+
const createBackup = args.createBackup !== false;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const results: string[] = [];
|
|
26
|
+
results.push(`Patch Bytes: ${args.filePath}`);
|
|
27
|
+
results.push(` Offset: 0x${args.offset.toString(16)} (${args.offset})`);
|
|
28
|
+
results.push(` Data: ${args.hexData}`);
|
|
29
|
+
results.push("");
|
|
30
|
+
|
|
31
|
+
// Create backup if requested
|
|
32
|
+
if (createBackup) {
|
|
33
|
+
const backupPath = `${args.filePath}.bak`;
|
|
34
|
+
await execAsync(`cp "${args.filePath}" "${backupPath}"`);
|
|
35
|
+
results.push(`Backup created: ${backupPath}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Read original bytes for verification
|
|
39
|
+
const { stdout: originalHex } = await execAsync(
|
|
40
|
+
`xxd -s ${args.offset} -l ${args.hexData.replace(/\s/g, "").length / 2} -p "${args.filePath}" | tr -d '\\n'`
|
|
41
|
+
);
|
|
42
|
+
results.push(`Original bytes: ${originalHex.trim()}`);
|
|
43
|
+
|
|
44
|
+
// Apply the patch using printf and dd
|
|
45
|
+
const hexBytes = args.hexData.replace(/\s+/g, "");
|
|
46
|
+
const byteCount = hexBytes.length / 2;
|
|
47
|
+
const patchCmd = `printf '${hexBytes}' | dd of="${args.filePath}" bs=1 seek=${args.offset} count=${byteCount} conv=notrunc 2>/dev/null`;
|
|
48
|
+
await execAsync(patchCmd);
|
|
49
|
+
results.push(`Patched ${byteCount} bytes`);
|
|
50
|
+
|
|
51
|
+
// Verify the patch
|
|
52
|
+
const { stdout: newHex } = await execAsync(
|
|
53
|
+
`xxd -s ${args.offset} -l ${byteCount} -p "${args.filePath}" | tr -d '\\n'`
|
|
54
|
+
);
|
|
55
|
+
results.push(`New bytes: ${newHex.trim()}`);
|
|
56
|
+
|
|
57
|
+
if (newHex.trim().toLowerCase() === hexBytes.toLowerCase()) {
|
|
58
|
+
results.push("Verification: SUCCESS");
|
|
59
|
+
} else {
|
|
60
|
+
results.push("Verification: WARNING - Bytes don't match expected value");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
67
|
+
isError: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Insert NOP sled at offset
|
|
74
|
+
*/
|
|
75
|
+
export async function handleNopSled(args: {
|
|
76
|
+
filePath: string;
|
|
77
|
+
offset: number;
|
|
78
|
+
count: number;
|
|
79
|
+
createBackup?: boolean;
|
|
80
|
+
}): Promise<McpResponse> {
|
|
81
|
+
const createBackup = args.createBackup !== false;
|
|
82
|
+
|
|
83
|
+
// NOP = 0x90 on x86/x64
|
|
84
|
+
const nopHex = "90".repeat(args.count);
|
|
85
|
+
|
|
86
|
+
return handlePatchBytes({
|
|
87
|
+
filePath: args.filePath,
|
|
88
|
+
offset: args.offset,
|
|
89
|
+
hexData: nopHex,
|
|
90
|
+
createBackup,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Hex editor mode - read or modify bytes
|
|
96
|
+
*/
|
|
97
|
+
export async function handleHexEditor(args: {
|
|
98
|
+
filePath: string;
|
|
99
|
+
offset: number;
|
|
100
|
+
length: number;
|
|
101
|
+
newHex?: string;
|
|
102
|
+
}): Promise<McpResponse> {
|
|
103
|
+
try {
|
|
104
|
+
// If newHex provided, modify; otherwise just read
|
|
105
|
+
if (args.newHex) {
|
|
106
|
+
return handlePatchBytes({
|
|
107
|
+
filePath: args.filePath,
|
|
108
|
+
offset: args.offset,
|
|
109
|
+
hexData: args.newHex,
|
|
110
|
+
createBackup: true,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Read mode
|
|
115
|
+
const result = await runXxd(args.filePath, {
|
|
116
|
+
seek: args.offset,
|
|
117
|
+
length: args.length,
|
|
118
|
+
cols: 16,
|
|
119
|
+
groupSize: 1,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Also get plain hex for easy copying
|
|
123
|
+
const { stdout: plainHex } = await execAsync(
|
|
124
|
+
`xxd -s ${args.offset} -l ${args.length} -p "${args.filePath}" | tr -d '\\n'`
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const summary = [
|
|
128
|
+
`Hex Editor: ${args.filePath}`,
|
|
129
|
+
`Offset: 0x${args.offset.toString(16)} (${args.offset})`,
|
|
130
|
+
`Length: ${args.length} bytes`,
|
|
131
|
+
"",
|
|
132
|
+
"Hex dump:",
|
|
133
|
+
result.output,
|
|
134
|
+
"",
|
|
135
|
+
"Plain hex (for copying):",
|
|
136
|
+
plainHex.trim(),
|
|
137
|
+
].join("\n");
|
|
138
|
+
|
|
139
|
+
return { content: [{ type: "text", text: summary }] };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return {
|
|
142
|
+
content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
143
|
+
isError: true,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Number conversion utility
|
|
150
|
+
*/
|
|
151
|
+
export async function handleConvertNumber(args: {
|
|
152
|
+
value: string;
|
|
153
|
+
fromFormat?: "hex" | "decimal" | "binary" | "octal" | "auto";
|
|
154
|
+
toFormat?: "all" | "hex" | "decimal" | "binary" | "octal" | "ascii";
|
|
155
|
+
byteSize?: 8 | 16 | 32 | 64;
|
|
156
|
+
signed?: boolean;
|
|
157
|
+
}): Promise<McpResponse> {
|
|
158
|
+
const result = convertNumber(args.value, {
|
|
159
|
+
fromFormat: args.fromFormat ?? "auto",
|
|
160
|
+
toFormat: args.toFormat ?? "all",
|
|
161
|
+
byteSize: args.byteSize,
|
|
162
|
+
signed: args.signed,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const summary: string[] = [
|
|
166
|
+
`Number Conversion: ${args.value}`,
|
|
167
|
+
`Input format: ${args.fromFormat ?? "auto-detect"}`,
|
|
168
|
+
"",
|
|
169
|
+
"Results:",
|
|
170
|
+
` Decimal: ${result.decimal}`,
|
|
171
|
+
` Hex: ${result.hex}`,
|
|
172
|
+
` Binary: ${result.binary}`,
|
|
173
|
+
` Octal: ${result.octal}`,
|
|
174
|
+
` ASCII: ${result.ascii}`,
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
if (result.signed !== undefined) {
|
|
178
|
+
summary.push(` Signed: ${result.signed}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (result.byteBreakdown && result.byteBreakdown.length > 0) {
|
|
182
|
+
summary.push("");
|
|
183
|
+
summary.push("Byte breakdown:");
|
|
184
|
+
for (const byte of result.byteBreakdown) {
|
|
185
|
+
summary.push(` Byte ${byte.index}: 0x${byte.hex} (${byte.decimal})`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { content: [{ type: "text", text: summary.join("\n") }] };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Patch manifest for persistent patches
|
|
194
|
+
*/
|
|
195
|
+
interface PatchEntry {
|
|
196
|
+
id: string;
|
|
197
|
+
offset: number;
|
|
198
|
+
originalBytes: string;
|
|
199
|
+
patchedBytes: string;
|
|
200
|
+
description: string;
|
|
201
|
+
applied: boolean;
|
|
202
|
+
timestamp: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const patchManifest: Map<string, PatchEntry[]> = new Map();
|
|
206
|
+
|
|
207
|
+
function getPatches(filePath: string): PatchEntry[] {
|
|
208
|
+
if (!patchManifest.has(filePath)) {
|
|
209
|
+
patchManifest.set(filePath, []);
|
|
210
|
+
}
|
|
211
|
+
return patchManifest.get(filePath)!;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Register a patch for persistence
|
|
216
|
+
*/
|
|
217
|
+
export async function handlePatchRegister(args: {
|
|
218
|
+
filePath: string;
|
|
219
|
+
patchId: string;
|
|
220
|
+
offset: number;
|
|
221
|
+
patchedBytes: string;
|
|
222
|
+
description: string;
|
|
223
|
+
captureOriginal?: boolean;
|
|
224
|
+
}): Promise<McpResponse> {
|
|
225
|
+
const patches = getPatches(args.filePath);
|
|
226
|
+
|
|
227
|
+
// Check for duplicate ID
|
|
228
|
+
if (patches.some((p) => p.id === args.patchId)) {
|
|
229
|
+
return {
|
|
230
|
+
content: [{ type: "text", text: `Error: Patch ID "${args.patchId}" already exists for this file.` }],
|
|
231
|
+
isError: true,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let originalBytes = "";
|
|
236
|
+
if (args.captureOriginal !== false) {
|
|
237
|
+
try {
|
|
238
|
+
const { stdout } = await execAsync(
|
|
239
|
+
`xxd -s ${args.offset} -l ${args.patchedBytes.replace(/\s/g, "").length / 2} -p "${args.filePath}" | tr -d '\\n'`
|
|
240
|
+
);
|
|
241
|
+
originalBytes = stdout.trim();
|
|
242
|
+
} catch {
|
|
243
|
+
originalBytes = "unknown";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const patch: PatchEntry = {
|
|
248
|
+
id: args.patchId,
|
|
249
|
+
offset: args.offset,
|
|
250
|
+
originalBytes,
|
|
251
|
+
patchedBytes: args.patchedBytes.replace(/\s/g, ""),
|
|
252
|
+
description: args.description,
|
|
253
|
+
applied: false,
|
|
254
|
+
timestamp: new Date().toISOString(),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
patches.push(patch);
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
content: [{
|
|
261
|
+
type: "text",
|
|
262
|
+
text: [
|
|
263
|
+
`Patch Registered: ${args.patchId}`,
|
|
264
|
+
` File: ${args.filePath}`,
|
|
265
|
+
` Offset: 0x${args.offset.toString(16)}`,
|
|
266
|
+
` Original: ${originalBytes || "(not captured)"}`,
|
|
267
|
+
` Patched: ${patch.patchedBytes}`,
|
|
268
|
+
` Description: ${args.description}`,
|
|
269
|
+
"",
|
|
270
|
+
"Patch has been registered but not yet applied.",
|
|
271
|
+
"Use bin_patch_apply to apply all registered patches.",
|
|
272
|
+
].join("\n"),
|
|
273
|
+
}],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* List registered patches
|
|
279
|
+
*/
|
|
280
|
+
export async function handlePatchList(args: { filePath?: string }): Promise<McpResponse> {
|
|
281
|
+
if (args.filePath) {
|
|
282
|
+
const patches = getPatches(args.filePath);
|
|
283
|
+
if (patches.length === 0) {
|
|
284
|
+
return { content: [{ type: "text", text: `No patches registered for ${args.filePath}` }] };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
content: [{
|
|
289
|
+
type: "text",
|
|
290
|
+
text: [
|
|
291
|
+
`Patches for ${args.filePath}:`,
|
|
292
|
+
"",
|
|
293
|
+
...patches.map(
|
|
294
|
+
(p) =>
|
|
295
|
+
` [${p.applied ? "X" : " "}] ${p.id}: offset 0x${p.offset.toString(16)} - ${p.description}`
|
|
296
|
+
),
|
|
297
|
+
].join("\n"),
|
|
298
|
+
}],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// List all patches
|
|
303
|
+
const allPatches = Array.from(patchManifest.entries()).flatMap(([file, patches]) =>
|
|
304
|
+
patches.map((p) => ({ file, ...p }))
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (allPatches.length === 0) {
|
|
308
|
+
return { content: [{ type: "text", text: "No patches registered." }] };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
content: [{
|
|
313
|
+
type: "text",
|
|
314
|
+
text: [
|
|
315
|
+
"All Registered Patches:",
|
|
316
|
+
"",
|
|
317
|
+
...allPatches.map(
|
|
318
|
+
(p) =>
|
|
319
|
+
` [${p.applied ? "X" : " "}] ${p.file}#${p.id}: offset 0x${p.offset.toString(16)} - ${p.description}`
|
|
320
|
+
),
|
|
321
|
+
].join("\n"),
|
|
322
|
+
}],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Apply all registered patches
|
|
328
|
+
*/
|
|
329
|
+
export async function handlePatchApply(args: {
|
|
330
|
+
filePath: string;
|
|
331
|
+
resign?: boolean;
|
|
332
|
+
removeQuarantine?: boolean;
|
|
333
|
+
}): Promise<McpResponse> {
|
|
334
|
+
const patches = getPatches(args.filePath);
|
|
335
|
+
|
|
336
|
+
if (patches.length === 0) {
|
|
337
|
+
return {
|
|
338
|
+
content: [{ type: "text", text: `No patches registered for ${args.filePath}` }],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const results: string[] = [];
|
|
343
|
+
results.push(`Applying Patches: ${args.filePath}`);
|
|
344
|
+
results.push("");
|
|
345
|
+
|
|
346
|
+
let appliedCount = 0;
|
|
347
|
+
|
|
348
|
+
for (const patch of patches) {
|
|
349
|
+
try {
|
|
350
|
+
const byteCount = patch.patchedBytes.length / 2;
|
|
351
|
+
const patchCmd = `printf '${patch.patchedBytes}' | dd of="${args.filePath}" bs=1 seek=${patch.offset} count=${byteCount} conv=notrunc 2>/dev/null`;
|
|
352
|
+
await execAsync(patchCmd);
|
|
353
|
+
patch.applied = true;
|
|
354
|
+
appliedCount++;
|
|
355
|
+
results.push(`[OK] ${patch.id} at 0x${patch.offset.toString(16)}`);
|
|
356
|
+
} catch (e) {
|
|
357
|
+
results.push(`[FAIL] ${patch.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
results.push("");
|
|
362
|
+
results.push(`Applied ${appliedCount}/${patches.length} patches.`);
|
|
363
|
+
|
|
364
|
+
// macOS-specific post-processing
|
|
365
|
+
if (isMacOS() && appliedCount > 0) {
|
|
366
|
+
if (args.resign !== false) {
|
|
367
|
+
results.push("");
|
|
368
|
+
results.push("Re-signing binary...");
|
|
369
|
+
try {
|
|
370
|
+
await execAsync(`codesign --remove-signature "${args.filePath}" 2>&1 || true`);
|
|
371
|
+
await execAsync(`codesign --force --sign - "${args.filePath}"`);
|
|
372
|
+
results.push(" Ad-hoc signature applied.");
|
|
373
|
+
} catch (e) {
|
|
374
|
+
results.push(` Warning: ${e instanceof Error ? e.message : String(e)}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (args.removeQuarantine !== false) {
|
|
379
|
+
results.push("");
|
|
380
|
+
results.push("Removing quarantine...");
|
|
381
|
+
try {
|
|
382
|
+
await execAsync(`xattr -d com.apple.quarantine "${args.filePath}" 2>&1 || true`);
|
|
383
|
+
results.push(" Quarantine removed.");
|
|
384
|
+
} catch {
|
|
385
|
+
results.push(" No quarantine attribute present.");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Restore original bytes for patches
|
|
395
|
+
*/
|
|
396
|
+
export async function handlePatchRestore(args: {
|
|
397
|
+
filePath: string;
|
|
398
|
+
patchId?: string;
|
|
399
|
+
resign?: boolean;
|
|
400
|
+
}): Promise<McpResponse> {
|
|
401
|
+
const patches = getPatches(args.filePath);
|
|
402
|
+
|
|
403
|
+
if (patches.length === 0) {
|
|
404
|
+
return {
|
|
405
|
+
content: [{ type: "text", text: `No patches registered for ${args.filePath}` }],
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const results: string[] = [];
|
|
410
|
+
results.push(`Restoring Patches: ${args.filePath}`);
|
|
411
|
+
results.push("");
|
|
412
|
+
|
|
413
|
+
const toRestore = args.patchId
|
|
414
|
+
? patches.filter((p) => p.id === args.patchId)
|
|
415
|
+
: patches.filter((p) => p.applied);
|
|
416
|
+
|
|
417
|
+
if (toRestore.length === 0) {
|
|
418
|
+
return {
|
|
419
|
+
content: [{ type: "text", text: args.patchId ? `Patch "${args.patchId}" not found.` : "No applied patches to restore." }],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
let restoredCount = 0;
|
|
424
|
+
|
|
425
|
+
for (const patch of toRestore) {
|
|
426
|
+
if (patch.originalBytes === "unknown" || !patch.originalBytes) {
|
|
427
|
+
results.push(`[SKIP] ${patch.id}: No original bytes captured`);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const byteCount = patch.originalBytes.length / 2;
|
|
433
|
+
const restoreCmd = `printf '${patch.originalBytes}' | dd of="${args.filePath}" bs=1 seek=${patch.offset} count=${byteCount} conv=notrunc 2>/dev/null`;
|
|
434
|
+
await execAsync(restoreCmd);
|
|
435
|
+
patch.applied = false;
|
|
436
|
+
restoredCount++;
|
|
437
|
+
results.push(`[OK] ${patch.id} restored at 0x${patch.offset.toString(16)}`);
|
|
438
|
+
} catch (e) {
|
|
439
|
+
results.push(`[FAIL] ${patch.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
results.push("");
|
|
444
|
+
results.push(`Restored ${restoredCount}/${toRestore.length} patches.`);
|
|
445
|
+
|
|
446
|
+
// Re-sign on macOS
|
|
447
|
+
if (isMacOS() && restoredCount > 0 && args.resign !== false) {
|
|
448
|
+
results.push("");
|
|
449
|
+
results.push("Re-signing binary...");
|
|
450
|
+
try {
|
|
451
|
+
await execAsync(`codesign --remove-signature "${args.filePath}" 2>&1 || true`);
|
|
452
|
+
await execAsync(`codesign --force --sign - "${args.filePath}"`);
|
|
453
|
+
results.push(" Ad-hoc signature applied.");
|
|
454
|
+
} catch (e) {
|
|
455
|
+
results.push(` Warning: ${e instanceof Error ? e.message : String(e)}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Verify registered patches
|
|
464
|
+
*/
|
|
465
|
+
export async function handlePatchVerify(args: { filePath: string }): Promise<McpResponse> {
|
|
466
|
+
const patches = getPatches(args.filePath);
|
|
467
|
+
|
|
468
|
+
if (patches.length === 0) {
|
|
469
|
+
return {
|
|
470
|
+
content: [{ type: "text", text: `No patches registered for ${args.filePath}` }],
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const results: string[] = [];
|
|
475
|
+
results.push(`Verifying Patches: ${args.filePath}`);
|
|
476
|
+
results.push("");
|
|
477
|
+
|
|
478
|
+
for (const patch of patches) {
|
|
479
|
+
try {
|
|
480
|
+
const byteCount = patch.patchedBytes.length / 2;
|
|
481
|
+
const { stdout: currentBytes } = await execAsync(
|
|
482
|
+
`xxd -s ${patch.offset} -l ${byteCount} -p "${args.filePath}" | tr -d '\\n'`
|
|
483
|
+
);
|
|
484
|
+
const current = currentBytes.trim().toLowerCase();
|
|
485
|
+
const expected = patch.patchedBytes.toLowerCase();
|
|
486
|
+
|
|
487
|
+
if (current === expected) {
|
|
488
|
+
results.push(`[APPLIED] ${patch.id}: Bytes match`);
|
|
489
|
+
} else if (patch.originalBytes && current === patch.originalBytes.toLowerCase()) {
|
|
490
|
+
results.push(`[NOT APPLIED] ${patch.id}: Original bytes detected`);
|
|
491
|
+
} else {
|
|
492
|
+
results.push(`[UNKNOWN] ${patch.id}: Unexpected bytes (${current})`);
|
|
493
|
+
results.push(` Expected: ${expected}`);
|
|
494
|
+
}
|
|
495
|
+
} catch (e) {
|
|
496
|
+
results.push(`[ERROR] ${patch.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return { content: [{ type: "text", text: results.join("\n") }] };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Remove a patch from registry
|
|
505
|
+
*/
|
|
506
|
+
export async function handlePatchRemove(args: {
|
|
507
|
+
filePath: string;
|
|
508
|
+
patchId: string;
|
|
509
|
+
}): Promise<McpResponse> {
|
|
510
|
+
const patches = getPatches(args.filePath);
|
|
511
|
+
const index = patches.findIndex((p) => p.id === args.patchId);
|
|
512
|
+
|
|
513
|
+
if (index === -1) {
|
|
514
|
+
return {
|
|
515
|
+
content: [{ type: "text", text: `Patch "${args.patchId}" not found.` }],
|
|
516
|
+
isError: true,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const removed = patches.splice(index, 1)[0];
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
content: [{
|
|
524
|
+
type: "text",
|
|
525
|
+
text: [
|
|
526
|
+
`Patch Removed: ${args.patchId}`,
|
|
527
|
+
` File: ${args.filePath}`,
|
|
528
|
+
` Offset: 0x${removed.offset.toString(16)}`,
|
|
529
|
+
` Description: ${removed.description}`,
|
|
530
|
+
"",
|
|
531
|
+
"Note: This only removes from registry. Binary is unchanged.",
|
|
532
|
+
].join("\n"),
|
|
533
|
+
}],
|
|
534
|
+
};
|
|
535
|
+
}
|