@ebowwa/mcp-nm 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@
14
14
  * - Binary patching: patch bytes, NOP sled, hex editor
15
15
  * - Number conversion: hex, decimal, binary, octal, ASCII
16
16
  * - macOS binary patching: code signing, quarantine, safe patch workflow
17
+ * - Patch management: persistent patch registry, auto-apply on binary updates
17
18
  */
18
19
 
19
20
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -24,6 +25,9 @@ import {
24
25
  } from "@modelcontextprotocol/sdk/types.js";
25
26
  import { exec } from "child_process";
26
27
  import { promisify } from "util";
28
+ import * as fs from "fs";
29
+ import * as path from "path";
30
+ import * as os from "os";
27
31
 
28
32
  const execAsync = promisify(exec);
29
33
 
@@ -31,7 +35,7 @@ const execAsync = promisify(exec);
31
35
  const server = new Server(
32
36
  {
33
37
  name: "@ebowwa/mcp-nm",
34
- version: "2.0.3",
38
+ version: "2.1.0",
35
39
  },
36
40
  {
37
41
  capabilities: {
@@ -2544,6 +2548,588 @@ async function handleBinVerify(args: { filePath: string }) {
2544
2548
  }
2545
2549
  }
2546
2550
 
2551
+ // ============================================================================
2552
+ // Patch Management System
2553
+ // ============================================================================
2554
+
2555
+ interface PatchEntry {
2556
+ id: string;
2557
+ offset: number;
2558
+ originalBytes: string;
2559
+ patchedBytes: string;
2560
+ description: string;
2561
+ createdAt: string;
2562
+ }
2563
+
2564
+ interface BinaryPatchRecord {
2565
+ binaryPath: string;
2566
+ binaryName: string;
2567
+ lastPatchedChecksum: string;
2568
+ lastAppliedAt: string;
2569
+ patches: PatchEntry[];
2570
+ }
2571
+
2572
+ interface PatchManifest {
2573
+ version: string;
2574
+ createdAt: string;
2575
+ updatedAt: string;
2576
+ binaries: Record<string, BinaryPatchRecord>;
2577
+ }
2578
+
2579
+ const PATCH_DIR = path.join(os.homedir(), ".claude", "patches");
2580
+ const MANIFEST_PATH = path.join(PATCH_DIR, "manifest.json");
2581
+ const BACKUPS_DIR = path.join(PATCH_DIR, "backups");
2582
+ const MANIFEST_VERSION = "1.0.0";
2583
+
2584
+ // Ensure patch directory exists
2585
+ function ensurePatchDir(): void {
2586
+ if (!fs.existsSync(PATCH_DIR)) {
2587
+ fs.mkdirSync(PATCH_DIR, { recursive: true });
2588
+ }
2589
+ if (!fs.existsSync(BACKUPS_DIR)) {
2590
+ fs.mkdirSync(BACKUPS_DIR, { recursive: true });
2591
+ }
2592
+ }
2593
+
2594
+ // Load manifest
2595
+ function loadManifest(): PatchManifest {
2596
+ ensurePatchDir();
2597
+ if (fs.existsSync(MANIFEST_PATH)) {
2598
+ try {
2599
+ const data = fs.readFileSync(MANIFEST_PATH, "utf-8");
2600
+ return JSON.parse(data);
2601
+ } catch {
2602
+ // Return new manifest if corrupted
2603
+ }
2604
+ }
2605
+ return {
2606
+ version: MANIFEST_VERSION,
2607
+ createdAt: new Date().toISOString(),
2608
+ updatedAt: new Date().toISOString(),
2609
+ binaries: {}
2610
+ };
2611
+ }
2612
+
2613
+ // Save manifest
2614
+ function saveManifest(manifest: PatchManifest): void {
2615
+ ensurePatchDir();
2616
+ manifest.updatedAt = new Date().toISOString();
2617
+ fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
2618
+ }
2619
+
2620
+ // Get file checksum
2621
+ async function getFileChecksum(filePath: string): Promise<string> {
2622
+ try {
2623
+ const { stdout } = await execAsync(`shasum -a 256 "${filePath}" | cut -d' ' -f1`);
2624
+ return stdout.trim();
2625
+ } catch {
2626
+ return "unknown";
2627
+ }
2628
+ }
2629
+
2630
+ // Read bytes at offset
2631
+ function readBytesAtOffset(filePath: string, offset: number, length: number): string {
2632
+ const fd = fs.openSync(filePath, "r");
2633
+ const buffer = Buffer.alloc(length);
2634
+ fs.readSync(fd, buffer, 0, length, offset);
2635
+ fs.closeSync(fd);
2636
+ return buffer.toString("hex").match(/.{2}/g)?.join(" ").toUpperCase() || "";
2637
+ }
2638
+
2639
+ // Write bytes at offset
2640
+ function writeBytesAtOffset(filePath: string, offset: number, hexData: string): void {
2641
+ const bytes = Buffer.from(hexData.replace(/\s+/g, ""), "hex");
2642
+ const fd = fs.openSync(filePath, "r+");
2643
+ fs.writeSync(fd, bytes, 0, bytes.length, offset);
2644
+ fs.closeSync(fd);
2645
+ }
2646
+
2647
+ // --- Register a patch ---
2648
+ async function handlePatchRegister(args: {
2649
+ filePath: string;
2650
+ patchId: string;
2651
+ offset: number;
2652
+ patchedBytes: string;
2653
+ description: string;
2654
+ captureOriginal?: boolean;
2655
+ }) {
2656
+ try {
2657
+ const results: string[] = [];
2658
+ results.push(`Register Patch: ${args.patchId}`);
2659
+ results.push(`Binary: ${args.filePath}`);
2660
+ results.push(`Offset: 0x${args.offset.toString(16)} (${args.offset})`);
2661
+ results.push("");
2662
+
2663
+ // Validate file exists
2664
+ if (!fs.existsSync(args.filePath)) {
2665
+ return {
2666
+ content: [{ type: "text", text: `Error: File not found: ${args.filePath}` }],
2667
+ isError: true
2668
+ };
2669
+ }
2670
+
2671
+ // Normalize hex data
2672
+ const patchedBytes = args.patchedBytes.replace(/\s+/g, "").toUpperCase();
2673
+ const byteLength = patchedBytes.length / 2;
2674
+
2675
+ // Capture original bytes if requested
2676
+ let originalBytes = "";
2677
+ if (args.captureOriginal !== false) {
2678
+ try {
2679
+ originalBytes = readBytesAtOffset(args.filePath, args.offset, byteLength);
2680
+ results.push(`Original bytes: ${originalBytes}`);
2681
+ } catch (e) {
2682
+ return {
2683
+ content: [{ type: "text", text: `Error: Could not read original bytes - ${e instanceof Error ? e.message : String(e)}` }],
2684
+ isError: true
2685
+ };
2686
+ }
2687
+ }
2688
+
2689
+ // Save backup of original bytes
2690
+ const backupFileName = `${path.basename(args.filePath)}.offset_${args.offset}.bin`;
2691
+ const backupPath = path.join(BACKUPS_DIR, backupFileName);
2692
+ if (originalBytes) {
2693
+ fs.writeFileSync(backupPath, Buffer.from(originalBytes.replace(/\s+/g, ""), "hex"));
2694
+ results.push(`Backup saved: ${backupPath}`);
2695
+ }
2696
+
2697
+ // Load and update manifest
2698
+ const manifest = loadManifest();
2699
+ const checksum = await getFileChecksum(args.filePath);
2700
+
2701
+ if (!manifest.binaries[args.filePath]) {
2702
+ manifest.binaries[args.filePath] = {
2703
+ binaryPath: args.filePath,
2704
+ binaryName: path.basename(args.filePath),
2705
+ lastPatchedChecksum: checksum,
2706
+ lastAppliedAt: new Date().toISOString(),
2707
+ patches: []
2708
+ };
2709
+ }
2710
+
2711
+ // Check if patch ID already exists
2712
+ const existingIndex = manifest.binaries[args.filePath].patches.findIndex(p => p.id === args.patchId);
2713
+ const newPatch: PatchEntry = {
2714
+ id: args.patchId,
2715
+ offset: args.offset,
2716
+ originalBytes: originalBytes || "UNKNOWN",
2717
+ patchedBytes: patchedBytes.match(/.{2}/g)?.join(" ") || patchedBytes,
2718
+ description: args.description,
2719
+ createdAt: new Date().toISOString()
2720
+ };
2721
+
2722
+ if (existingIndex >= 0) {
2723
+ manifest.binaries[args.filePath].patches[existingIndex] = newPatch;
2724
+ results.push(`Updated existing patch: ${args.patchId}`);
2725
+ } else {
2726
+ manifest.binaries[args.filePath].patches.push(newPatch);
2727
+ results.push(`Registered new patch: ${args.patchId}`);
2728
+ }
2729
+
2730
+ saveManifest(manifest);
2731
+
2732
+ results.push("");
2733
+ results.push("Patch registered successfully!");
2734
+ results.push(`Manifest: ${MANIFEST_PATH}`);
2735
+ results.push("");
2736
+ results.push("To apply this patch, use: bin_patch_apply");
2737
+
2738
+ return { content: [{ type: "text", text: results.join("\n") }] };
2739
+ } catch (error) {
2740
+ return {
2741
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
2742
+ isError: true
2743
+ };
2744
+ }
2745
+ }
2746
+
2747
+ // --- Apply all registered patches ---
2748
+ async function handlePatchApply(args: {
2749
+ filePath: string;
2750
+ resign?: boolean;
2751
+ removeQuarantine?: boolean;
2752
+ }) {
2753
+ const isMac = isMacOS();
2754
+ const resign = args.resign !== false && isMac;
2755
+ const removeQuarantine = args.removeQuarantine !== false && isMac;
2756
+
2757
+ try {
2758
+ const results: string[] = [];
2759
+ results.push(`Apply Patches: ${args.filePath}`);
2760
+ results.push("");
2761
+
2762
+ // Load manifest
2763
+ const manifest = loadManifest();
2764
+ const record = manifest.binaries[args.filePath];
2765
+
2766
+ if (!record || record.patches.length === 0) {
2767
+ results.push("No registered patches found for this binary.");
2768
+ results.push("");
2769
+ results.push("To register a patch, use: bin_patch_register");
2770
+ return { content: [{ type: "text", text: results.join("\n") }] };
2771
+ }
2772
+
2773
+ // Check current checksum
2774
+ const currentChecksum = await getFileChecksum(args.filePath);
2775
+ results.push(`Current checksum: ${currentChecksum.substring(0, 16)}...`);
2776
+ results.push(`Last patched checksum: ${record.lastPatchedChecksum.substring(0, 16)}...`);
2777
+
2778
+ if (currentChecksum === record.lastPatchedChecksum) {
2779
+ results.push("Status: Binary appears to already have patches applied.");
2780
+ results.push("Use force=true to re-apply anyway.");
2781
+ return { content: [{ type: "text", text: results.join("\n") }] };
2782
+ }
2783
+
2784
+ results.push("Binary has been updated - applying patches...");
2785
+ results.push("");
2786
+
2787
+ // Create backup of entire binary
2788
+ const backupPath = `${args.filePath}.prepatch`;
2789
+ fs.copyFileSync(args.filePath, backupPath);
2790
+ results.push(`Backup: ${backupPath}`);
2791
+
2792
+ // Apply each patch
2793
+ let appliedCount = 0;
2794
+ for (const patch of record.patches) {
2795
+ try {
2796
+ results.push(`Applying: ${patch.id}`);
2797
+ results.push(` Offset: 0x${patch.offset.toString(16)}`);
2798
+ results.push(` Bytes: ${patch.patchedBytes}`);
2799
+
2800
+ writeBytesAtOffset(args.filePath, patch.offset, patch.patchedBytes);
2801
+ appliedCount++;
2802
+ results.push(" Status: Applied");
2803
+ } catch (e) {
2804
+ results.push(` Status: FAILED - ${e instanceof Error ? e.message : String(e)}`);
2805
+ }
2806
+ }
2807
+
2808
+ // macOS: resign and remove quarantine
2809
+ if (isMac && appliedCount > 0) {
2810
+ results.push("");
2811
+
2812
+ if (resign) {
2813
+ results.push("Re-signing binary...");
2814
+ try {
2815
+ await execAsync(`codesign --remove-signature "${args.filePath}" 2>&1`);
2816
+ await execAsync(`codesign --force --sign - "${args.filePath}"`);
2817
+ results.push(" Status: Re-signed with ad-hoc signature");
2818
+ } catch (e) {
2819
+ results.push(` Warning: ${e instanceof Error ? e.message : String(e)}`);
2820
+ }
2821
+ }
2822
+
2823
+ if (removeQuarantine) {
2824
+ results.push("Removing quarantine...");
2825
+ try {
2826
+ await execAsync(`xattr -d com.apple.quarantine "${args.filePath}" 2>&1`);
2827
+ await execAsync(`xattr -d com.apple.provenance "${args.filePath}" 2>&1`);
2828
+ results.push(" Status: Quarantine attributes removed");
2829
+ } catch {
2830
+ results.push(" Status: No quarantine to remove");
2831
+ }
2832
+ }
2833
+ }
2834
+
2835
+ // Update manifest with new checksum
2836
+ const newChecksum = await getFileChecksum(args.filePath);
2837
+ record.lastPatchedChecksum = newChecksum;
2838
+ record.lastAppliedAt = new Date().toISOString();
2839
+ saveManifest(manifest);
2840
+
2841
+ results.push("");
2842
+ results.push(`=== SUMMARY ===`);
2843
+ results.push(`Patches applied: ${appliedCount}/${record.patches.length}`);
2844
+ results.push(`New checksum: ${newChecksum.substring(0, 16)}...`);
2845
+ results.push("Binary is ready to use.");
2846
+
2847
+ return { content: [{ type: "text", text: results.join("\n") }] };
2848
+ } catch (error) {
2849
+ return {
2850
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
2851
+ isError: true
2852
+ };
2853
+ }
2854
+ }
2855
+
2856
+ // --- Verify patches are applied ---
2857
+ async function handlePatchVerify(args: { filePath: string }) {
2858
+ try {
2859
+ const results: string[] = [];
2860
+ results.push(`Verify Patches: ${args.filePath}`);
2861
+ results.push("");
2862
+
2863
+ // Load manifest
2864
+ const manifest = loadManifest();
2865
+ const record = manifest.binaries[args.filePath];
2866
+
2867
+ if (!record || record.patches.length === 0) {
2868
+ results.push("No registered patches found for this binary.");
2869
+ return { content: [{ type: "text", text: results.join("\n") }] };
2870
+ }
2871
+
2872
+ // Get current checksum
2873
+ const currentChecksum = await getFileChecksum(args.filePath);
2874
+ results.push(`Current checksum: ${currentChecksum.substring(0, 16)}...`);
2875
+ results.push(`Expected checksum: ${record.lastPatchedChecksum.substring(0, 16)}...`);
2876
+ results.push("");
2877
+
2878
+ // Check if binary was updated
2879
+ if (currentChecksum !== record.lastPatchedChecksum) {
2880
+ results.push("⚠️ Binary has been UPDATED since last patch.");
2881
+ results.push(" Run bin_patch_apply to re-apply patches.");
2882
+ results.push("");
2883
+ } else {
2884
+ results.push("✓ Binary checksum matches patched version.");
2885
+ results.push("");
2886
+ }
2887
+
2888
+ // Verify each patch by reading current bytes
2889
+ results.push("Patch Status:");
2890
+ let allApplied = true;
2891
+ for (const patch of record.patches) {
2892
+ try {
2893
+ const byteLength = patch.patchedBytes.replace(/\s+/g, "").length / 2;
2894
+ const currentBytes = readBytesAtOffset(args.filePath, patch.offset, byteLength);
2895
+ const normalizedCurrent = currentBytes.replace(/\s+/g, "").toUpperCase();
2896
+ const normalizedPatched = patch.patchedBytes.replace(/\s+/g, "").toUpperCase();
2897
+
2898
+ if (normalizedCurrent === normalizedPatched) {
2899
+ results.push(` ✓ ${patch.id}: APPLIED`);
2900
+ } else if (normalizedCurrent === patch.originalBytes.replace(/\s+/g, "").toUpperCase()) {
2901
+ results.push(` ✗ ${patch.id}: NOT APPLIED (original bytes present)`);
2902
+ allApplied = false;
2903
+ } else {
2904
+ results.push(` ? ${patch.id}: UNKNOWN (bytes don't match expected)`);
2905
+ results.push(` Current: ${currentBytes}`);
2906
+ results.push(` Expected: ${patch.patchedBytes}`);
2907
+ allApplied = false;
2908
+ }
2909
+ } catch (e) {
2910
+ results.push(` ✗ ${patch.id}: ERROR - ${e instanceof Error ? e.message : String(e)}`);
2911
+ allApplied = false;
2912
+ }
2913
+ }
2914
+
2915
+ results.push("");
2916
+ if (allApplied && currentChecksum === record.lastPatchedChecksum) {
2917
+ results.push("Status: All patches verified and applied.");
2918
+ } else if (!allApplied) {
2919
+ results.push("Status: Some patches need to be applied.");
2920
+ results.push("Action: Run bin_patch_apply");
2921
+ } else {
2922
+ results.push("Status: Binary may have been updated. Re-apply patches.");
2923
+ results.push("Action: Run bin_patch_apply");
2924
+ }
2925
+
2926
+ return { content: [{ type: "text", text: results.join("\n") }] };
2927
+ } catch (error) {
2928
+ return {
2929
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
2930
+ isError: true
2931
+ };
2932
+ }
2933
+ }
2934
+
2935
+ // --- Restore original bytes ---
2936
+ async function handlePatchRestore(args: {
2937
+ filePath: string;
2938
+ patchId?: string;
2939
+ resign?: boolean;
2940
+ }) {
2941
+ const isMac = isMacOS();
2942
+ const resign = args.resign !== false && isMac;
2943
+
2944
+ try {
2945
+ const results: string[] = [];
2946
+ results.push(`Restore Patches: ${args.filePath}`);
2947
+ results.push("");
2948
+
2949
+ // Load manifest
2950
+ const manifest = loadManifest();
2951
+ const record = manifest.binaries[args.filePath];
2952
+
2953
+ if (!record || record.patches.length === 0) {
2954
+ results.push("No registered patches found for this binary.");
2955
+ return { content: [{ type: "text", text: results.join("\n") }] };
2956
+ }
2957
+
2958
+ // Filter patches if specific ID requested
2959
+ const patchesToRestore = args.patchId
2960
+ ? record.patches.filter(p => p.id === args.patchId)
2961
+ : record.patches;
2962
+
2963
+ if (patchesToRestore.length === 0) {
2964
+ results.push(`No patch found with ID: ${args.patchId}`);
2965
+ return { content: [{ type: "text", text: results.join("\n") }] };
2966
+ }
2967
+
2968
+ // Create backup
2969
+ const backupPath = `${args.filePath}.prerestore`;
2970
+ fs.copyFileSync(args.filePath, backupPath);
2971
+ results.push(`Backup: ${backupPath}`);
2972
+ results.push("");
2973
+
2974
+ // Restore each patch
2975
+ let restoredCount = 0;
2976
+ for (const patch of patchesToRestore) {
2977
+ if (patch.originalBytes === "UNKNOWN") {
2978
+ results.push(`${patch.id}: SKIPPED (no original bytes captured)`);
2979
+ continue;
2980
+ }
2981
+
2982
+ try {
2983
+ results.push(`Restoring: ${patch.id}`);
2984
+ writeBytesAtOffset(args.filePath, patch.offset, patch.originalBytes);
2985
+ results.push(` Offset: 0x${patch.offset.toString(16)}`);
2986
+ results.push(` Bytes: ${patch.originalBytes}`);
2987
+ results.push(" Status: Restored");
2988
+ restoredCount++;
2989
+ } catch (e) {
2990
+ results.push(` Status: FAILED - ${e instanceof Error ? e.message : String(e)}`);
2991
+ }
2992
+ }
2993
+
2994
+ // macOS: resign
2995
+ if (isMac && restoredCount > 0 && resign) {
2996
+ results.push("");
2997
+ results.push("Re-signing binary...");
2998
+ try {
2999
+ await execAsync(`codesign --remove-signature "${args.filePath}" 2>&1`);
3000
+ await execAsync(`codesign --force --sign - "${args.filePath}"`);
3001
+ results.push(" Status: Re-signed with ad-hoc signature");
3002
+ } catch (e) {
3003
+ results.push(` Warning: ${e instanceof Error ? e.message : String(e)}`);
3004
+ }
3005
+ }
3006
+
3007
+ // Update manifest
3008
+ const newChecksum = await getFileChecksum(args.filePath);
3009
+ record.lastPatchedChecksum = newChecksum;
3010
+ saveManifest(manifest);
3011
+
3012
+ results.push("");
3013
+ results.push(`Restored: ${restoredCount}/${patchesToRestore.length} patches`);
3014
+
3015
+ return { content: [{ type: "text", text: results.join("\n") }] };
3016
+ } catch (error) {
3017
+ return {
3018
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
3019
+ isError: true
3020
+ };
3021
+ }
3022
+ }
3023
+
3024
+ // --- List registered patches ---
3025
+ async function handlePatchList(args: { filePath?: string }) {
3026
+ try {
3027
+ const results: string[] = [];
3028
+ results.push("Patch Registry");
3029
+ results.push(`Manifest: ${MANIFEST_PATH}`);
3030
+ results.push("");
3031
+
3032
+ const manifest = loadManifest();
3033
+
3034
+ if (Object.keys(manifest.binaries).length === 0) {
3035
+ results.push("No patches registered.");
3036
+ results.push("");
3037
+ results.push("To register a patch, use: bin_patch_register");
3038
+ return { content: [{ type: "text", text: results.join("\n") }] };
3039
+ }
3040
+
3041
+ // Filter by file if specified
3042
+ const binariesToShow = args.filePath
3043
+ ? { [args.filePath]: manifest.binaries[args.filePath] }
3044
+ : manifest.binaries;
3045
+
3046
+ for (const [binaryPath, record] of Object.entries(binariesToShow)) {
3047
+ if (!record) continue;
3048
+
3049
+ results.push(`═══════════════════════════════════════════════════════════`);
3050
+ results.push(`Binary: ${binaryPath}`);
3051
+ results.push(`Name: ${record.binaryName}`);
3052
+ results.push(`Patches: ${record.patches.length}`);
3053
+ results.push(`Last Applied: ${record.lastAppliedAt}`);
3054
+ results.push(`Checksum: ${record.lastPatchedChecksum.substring(0, 16)}...`);
3055
+ results.push("");
3056
+
3057
+ if (record.patches.length > 0) {
3058
+ results.push("Registered Patches:");
3059
+ for (const patch of record.patches) {
3060
+ results.push(` ┌─ ${patch.id} ─────────────────────────────────`);
3061
+ results.push(` │ Offset: 0x${patch.offset.toString(16)} (${patch.offset})`);
3062
+ results.push(` │ Original: ${patch.originalBytes}`);
3063
+ results.push(` │ Patched: ${patch.patchedBytes}`);
3064
+ results.push(` │ Description: ${patch.description}`);
3065
+ results.push(` │ Created: ${patch.createdAt}`);
3066
+ results.push(` └────────────────────────────────────────────`);
3067
+ }
3068
+ }
3069
+ results.push("");
3070
+ }
3071
+
3072
+ return { content: [{ type: "text", text: results.join("\n") }] };
3073
+ } catch (error) {
3074
+ return {
3075
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
3076
+ isError: true
3077
+ };
3078
+ }
3079
+ }
3080
+
3081
+ // --- Remove a patch from registry ---
3082
+ async function handlePatchRemove(args: {
3083
+ filePath: string;
3084
+ patchId: string;
3085
+ }) {
3086
+ try {
3087
+ const results: string[] = [];
3088
+ results.push(`Remove Patch: ${args.patchId}`);
3089
+ results.push("");
3090
+
3091
+ const manifest = loadManifest();
3092
+ const record = manifest.binaries[args.filePath];
3093
+
3094
+ if (!record) {
3095
+ results.push("No patches registered for this binary.");
3096
+ return { content: [{ type: "text", text: results.join("\n") }] };
3097
+ }
3098
+
3099
+ const index = record.patches.findIndex(p => p.id === args.patchId);
3100
+ if (index < 0) {
3101
+ results.push(`Patch not found: ${args.patchId}`);
3102
+ return { content: [{ type: "text", text: results.join("\n") }] };
3103
+ }
3104
+
3105
+ const removed = record.patches.splice(index, 1)[0];
3106
+ results.push("Removed patch:");
3107
+ results.push(` ID: ${removed.id}`);
3108
+ results.push(` Offset: 0x${removed.offset.toString(16)}`);
3109
+ results.push(` Description: ${removed.description}`);
3110
+
3111
+ // Remove binary entry if no patches left
3112
+ if (record.patches.length === 0) {
3113
+ delete manifest.binaries[args.filePath];
3114
+ results.push("");
3115
+ results.push("No more patches for this binary - removed from registry.");
3116
+ }
3117
+
3118
+ saveManifest(manifest);
3119
+ results.push("");
3120
+ results.push("Patch removed from registry.");
3121
+ results.push("Note: This does NOT undo the patch in the binary.");
3122
+ results.push("Use bin_patch_restore to restore original bytes.");
3123
+
3124
+ return { content: [{ type: "text", text: results.join("\n") }] };
3125
+ } catch (error) {
3126
+ return {
3127
+ content: [{ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
3128
+ isError: true
3129
+ };
3130
+ }
3131
+ }
3132
+
2547
3133
  // ============================================================================
2548
3134
  // Tool definitions
2549
3135
  // ============================================================================
@@ -3102,6 +3688,83 @@ const TOOLS = [
3102
3688
  required: ["filePath"],
3103
3689
  },
3104
3690
  },
3691
+ // Patch Management Tools
3692
+ {
3693
+ name: "bin_patch_register",
3694
+ description: "Register a binary patch for persistence across updates. Captures original bytes and stores in manifest.",
3695
+ inputSchema: {
3696
+ type: "object" as const,
3697
+ properties: {
3698
+ filePath: { type: "string", description: "Path to the binary file" },
3699
+ patchId: { type: "string", description: "Unique identifier for this patch (e.g., 'skip-permissions')" },
3700
+ offset: { type: "number", description: "Byte offset to patch" },
3701
+ patchedBytes: { type: "string", description: "Hex bytes to write (e.g., '90 90 90')" },
3702
+ description: { type: "string", description: "Description of what this patch does" },
3703
+ captureOriginal: { type: "boolean", description: "Capture original bytes (default: true)" },
3704
+ },
3705
+ required: ["filePath", "patchId", "offset", "patchedBytes", "description"],
3706
+ },
3707
+ },
3708
+ {
3709
+ name: "bin_patch_apply",
3710
+ description: "Apply all registered patches to a binary. Handles macOS re-signing and quarantine. Detects binary updates.",
3711
+ inputSchema: {
3712
+ type: "object" as const,
3713
+ properties: {
3714
+ filePath: { type: "string", description: "Path to the binary file" },
3715
+ resign: { type: "boolean", description: "Re-sign binary after patching (macOS, default: true)" },
3716
+ removeQuarantine: { type: "boolean", description: "Remove quarantine after patching (macOS, default: true)" },
3717
+ },
3718
+ required: ["filePath"],
3719
+ },
3720
+ },
3721
+ {
3722
+ name: "bin_patch_verify",
3723
+ description: "Verify if registered patches are currently applied to a binary. Detects if binary was updated.",
3724
+ inputSchema: {
3725
+ type: "object" as const,
3726
+ properties: {
3727
+ filePath: { type: "string", description: "Path to the binary file" },
3728
+ },
3729
+ required: ["filePath"],
3730
+ },
3731
+ },
3732
+ {
3733
+ name: "bin_patch_restore",
3734
+ description: "Restore original bytes for registered patches. Undoes patches in the binary.",
3735
+ inputSchema: {
3736
+ type: "object" as const,
3737
+ properties: {
3738
+ filePath: { type: "string", description: "Path to the binary file" },
3739
+ patchId: { type: "string", description: "Specific patch to restore (optional - restores all if omitted)" },
3740
+ resign: { type: "boolean", description: "Re-sign binary after restoring (macOS, default: true)" },
3741
+ },
3742
+ required: ["filePath"],
3743
+ },
3744
+ },
3745
+ {
3746
+ name: "bin_patch_list",
3747
+ description: "List all registered patches in the manifest, optionally filtered by binary.",
3748
+ inputSchema: {
3749
+ type: "object" as const,
3750
+ properties: {
3751
+ filePath: { type: "string", description: "Filter by binary path (optional)" },
3752
+ },
3753
+ required: [],
3754
+ },
3755
+ },
3756
+ {
3757
+ name: "bin_patch_remove",
3758
+ description: "Remove a patch from the registry. Does NOT undo the patch in the binary.",
3759
+ inputSchema: {
3760
+ type: "object" as const,
3761
+ properties: {
3762
+ filePath: { type: "string", description: "Path to the binary file" },
3763
+ patchId: { type: "string", description: "ID of the patch to remove from registry" },
3764
+ },
3765
+ required: ["filePath", "patchId"],
3766
+ },
3767
+ },
3105
3768
  ];
3106
3769
 
3107
3770
  // ============================================================================
@@ -3257,6 +3920,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3257
3920
  });
3258
3921
  case "bin_verify":
3259
3922
  return await handleBinVerify(args as { filePath: string });
3923
+ // Patch Management handlers
3924
+ case "bin_patch_register":
3925
+ return await handlePatchRegister(args as {
3926
+ filePath: string;
3927
+ patchId: string;
3928
+ offset: number;
3929
+ patchedBytes: string;
3930
+ description: string;
3931
+ captureOriginal?: boolean;
3932
+ });
3933
+ case "bin_patch_apply":
3934
+ return await handlePatchApply(args as {
3935
+ filePath: string;
3936
+ resign?: boolean;
3937
+ removeQuarantine?: boolean;
3938
+ });
3939
+ case "bin_patch_verify":
3940
+ return await handlePatchVerify(args as { filePath: string });
3941
+ case "bin_patch_restore":
3942
+ return await handlePatchRestore(args as {
3943
+ filePath: string;
3944
+ patchId?: string;
3945
+ resign?: boolean;
3946
+ });
3947
+ case "bin_patch_list":
3948
+ return await handlePatchList(args as { filePath?: string });
3949
+ case "bin_patch_remove":
3950
+ return await handlePatchRemove(args as {
3951
+ filePath: string;
3952
+ patchId: string;
3953
+ });
3260
3954
  default:
3261
3955
  throw new Error(`Unknown tool: ${name}`);
3262
3956
  }