Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 
 
 

Repository files navigation

mcp-binary-tools

An MCP (Model Context Protocol) server for inspecting and editing binary files: hex read/write, byte-pattern search, string extraction, file-type detection (magic bytes + entropy), hex dumps, hashing, binary diffing, XOR transforms, entropy scanning, file carving, and PE/ELF section listing.

Zero dependencies. Unlike the other mcp-* servers in this repo, this one does not use the mcp Python SDK (pip install mcp) or any other third-party package — not even for the MCP protocol itself. Everything, including the JSON-RPC/stdio transport, is implemented with the Python standard library only (json, struct, re, math, hashlib, zlib, pathlib, sys). Nothing to install beyond Python itself.

Quick start

No pip install needed. Just point your MCP client at:

python K:\mcp-tools\mcp-binary-tools\mcp-binary-tools.py

(adjust the path if you've moved this folder elsewhere). It's a plain stdio MCP server — any MCP-compatible client can spawn it directly.

Claude Desktop — add to claude_desktop_config.json:

{
  "mcpServers": {
    "binary-tools": {
      "command": "python",
      "args": ["K:\\mcp-tools\\mcp-binary-tools\\mcp-binary-tools.py"]
    }
  }
}

Goose — add to ~/.config/goose/config.yaml (key names have shifted between Goose versions — check goose configure / your version's docs if this doesn't match):

extensions:
  binary-tools:
    type: stdio
    cmd: python
    args: ["K:\\mcp-tools\\mcp-binary-tools\\mcp-binary-tools.py"]
    enabled: true

Any other MCP client: configure it to run python K:\mcp-tools\mcp-binary-tools\mcp-binary-tools.py as a stdio-based MCP server — that's the whole interface, no ports, no config files, no extra setup involved.

Requirements

  • Python 3.10+ (uses from __future__ import annotations and X | None style type hints)
  • That's it. No pip install, no bundled executables, no bin/ folder.

Available tools

binary_read(file, offset=0, length=256)

Read raw bytes at a given offset. Returns hex (compact), hex_spaced (byte-separated), and ascii (printable chars, . for the rest), plus file_size and an eof flag. Capped at 65536 bytes per call — page through larger regions with offset.

binary_write(file, offset, hex_data, extend=False)

Patch bytes into an existing file in place, hex-editor style:

with open("firmware.bin", "r+b") as f:
    f.seek(offset)
    f.write(bytes.fromhex(hex_data))

hex_data accepts spaces/underscores/0x prefixes and is cleaned up automatically ("DE AD BE EF", "DEADBEEF", "0xDE 0xAD..." all work). Writing past the current end of file is refused unless extend=true, which zero-pads the gap. This tool only patches files that already exist — it will not create a new one. Capped at 10 MB per call. There is no undo or automatic backup — this modifies the file on disk directly.

binary_search(file, hex_pattern, offset=0, length=None, max_results=1000)

Search for a byte pattern given as hex. ?? acts as a single-byte wildcard: "DE AD ?? EF" matches DE AD + any byte + EF. Returns match offsets (decimal + hex) and the matched bytes. If length is omitted, the scan covers the rest of the file capped at 32 MB (scan_truncated: true signals when that cap was hit — page through with offset/length for larger files). Matches are non-overlapping.

strings_search(file, min_length=4, encoding="ascii", offset=0, length=None, max_results=2000)

Extract printable strings, like the classic strings command-line tool. encoding is "ascii", "utf16le" (common in Windows binaries), or "both". Same offset/length paging and 32 MB scan cap as binary_search.

magic_analyze(file)

Detects file type from magic bytes/header structure and reports Shannon entropy over a sample of the file (hinting at compressed/encrypted/packed content when it's high, e.g. > 7.5).

Nearly every signature comes from magic_numbers.yaml — an editable, human-readable database of 816 signatures, grouped by category:

Category Contents
archives ZIP family (+ OOXML/EPUB/JAR sub-variants), RAR, 7-Zip, GZIP (+ generic fallback), TAR (generic + POSIX/GNU ustar), BZIP2 (+ legacy fallback), XZ, Zstandard, LZ4, CAB, ISO 9660, zlib streams (all 8 level/dictionary combinations), LZH, RNC, lzip, cpio, RPM, ARJ, Zoo, FreeArc, and dozens more compressors/archivers
images JPEG/PNG/GIF/BMP/WebP/TIFF/ICO/PSD plus Canon RAW, BigTIFF, JPEG 2000/XL, SPIFF, DPX, and dozens of legacy/scientific/retro formats (FITS, Sun Rasterfile, XCF, XPM, ...)
video_audio MP4/MOV/MP3/WAV/AVI/MKV/FLAC/OGG/WMV-WMA/MIDI plus Shockwave/Director (disambiguated), AMR, SILK, FLV, DSS, EnCase EWF, and many RIFF/IFF-family formats
documents PDF, Office (DOCX/XLSX/PPTX/DOC/XLS/PPT + several detected sub-variants like encrypted/Java-archive ZIPs), RTF, ODF, EPUB, XML (+ several specific XML-dialect signatures), HTML, PostScript/EPS
executables EXE/DLL (+ several specific MZ-based tool signatures), ELF, Mach-O, Java Class, DEX, WebAssembly, Shell Script, and more
fonts TrueType, OpenType, WOFF/WOFF2
disk_images Disk/CD/cartridge images, VHD/VHDX/WIM, filesystem journals, EnCase/AFF forensic images
virtual_machines VMDK, VDI, qcow
databases SQLite-adjacent and legacy spreadsheet/database formats (Lotus 1-2-3, MS Access/Money, Approach, columnar formats like Parquet/Avro/ORC, and many more)
network pcap / pcap-ng packet captures
text_encoding Unicode/text byte-order-marks (UTF-8/16/32/7, SCSU, EBCDIC) and XML-prolog encoding signatures
email mbox-family formats
security Certificates, private/public keys (PEM, PuTTY, OpenSSH), keystores, CrowdStrike channel files, and more
game_data ROMs and engine/resource files (Doom WAD, Unreal, Roblox, NES, ...)
retro_computing Commodore/Amiga/Atari/CP-M/Palm/AppleWorks-era formats
scientific FITS (full signature), HDF, DICOM, NIfTI, GRIB, NetCDF
3d_models glTF, Blender, voxel/mesh formats
misc PGP keyrings/encrypted data plus several hundred assorted formats (mostly from the largest source below) that don't fit elsewhere
extras mcp-binary-tools' original built-in signatures, plus every entry from the sources below that is itself too generic to safely place in a topic category (bare RIFF, a bare JPEG SOI marker, a PDF signature missing its trailing -, ...) — checked last, after everything above, specifically so these deliberately-generic fallbacks never shadow a more specific entry that happens to live in a different category

Sources: every entry except mcp-binary-tools' own original signatures is transcribed from one of the four sources listed at the bottom of this README — see that section for which source covers what, and how real overlaps/conflicts between them were resolved. The short version: exact or near-redundant duplicates were skipped; genuinely ambiguous shared signatures were kept as multiple entries with a note explaining the tie (e.g. Java class files vs. Mach-O Universal binaries, or the ~90 formats across the last source that happen to share a signature with something else); specific-vs-generic pairs were kept as both, ordered so the specific one is checked first (Canon CR2 before generic TIFF, POSIX/GNU ustar before generic TAR, a confirmed-Exif JPEG check before the looser one, UTF-32 BOMs before UTF-16 BOMs, dozens more from the largest source); and a few entries were upgraded in place to a fuller, more precise signature found in a later source (WMV/WMA's full 16-byte GUID, FITS's full 30-byte header record).

Add new entries or whole new categories to magic_numbers.yaml without touching the Python code — see the schema comment at the top of the file (it also documents the note field). A match reports "source": "magic_numbers.yaml" plus the matched entry's category, description, extensions, and note (when present).

The file is parsed by a small hand-written loader in the script (load_simple_yaml) — not a general YAML parser, just enough for this file's flat "category → list of name/extensions/offset/hex/ ascii/description/note mappings" shape, kept dependency-free on purpose. If magic_numbers.yaml is missing or fails to parse, a warning is logged to stderr (not stdout) and magic_analyze falls back to a minimal built-in check ("source": "built-in") that only recognizes generic RIFF/ftyp containers and otherwise reports "unknown" — detection quality depends on this file being present.

Independent of which of the two above finds a match, PE and ELF files additionally get a details block (machine type, PE32/PE32+, DLL vs EXE for PE; class, endianness, type, machine for ELF) from structural header parsing that can't be expressed as a simple byte pattern.

hex_dump(file, offset=0, length=256)

Classic hex-editor dump: 16 bytes per row, hex on the left, ASCII on the right.

00000000  4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00  |MZ..............|
00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|

Capped at 65536 bytes per call.

file_hash(file, algorithms=["md5","sha1","sha256"])

Compute one or more hashes/checksums of a whole file, streamed in 1 MiB chunks so it never loads a large file entirely into memory. Supported algorithms: md5, sha1, sha256, sha384, sha512, sha3_256, sha3_512 (all via hashlib), and crc32 (via zlib.crc32).

binary_diff(file_a, file_b, offset=0, length=None, max_results=200)

Byte-for-byte comparison of two files over the same offset/length range. Returns each contiguous differing region (offset, length, and up to a 32-byte hex preview from each file), not an aligned/LCS-style text diff — appropriate for binary files where insertions/deletions are rare and you mainly want to know which bytes changed. If the files are different sizes, only the range both actually have is compared, and a size_mismatch_note says so.

xor_transform(file, key_hex, offset=0, length=None, write=False)

XOR a byte range against a repeating hex key (key_hex can be one byte or several, e.g. a 4-byte key "DE AD BE EF" repeats across the range). Defaults to a preview only — returns the transformed bytes as hex/ASCII without touching the file, capped at 65536 bytes, useful for testing a candidate key against a suspected simple-XOR-obfuscated region before committing to anything. Pass write=true to write the transformed bytes back into the file in place instead (capped at 10 MB per call, same as binary_write).

entropy_scan(file, offset=0, length=None, window_size=4096, max_results=500)

Shannon entropy in a sliding window across a file, instead of the single whole-sample value magic_analyze reports — useful for spotting where in a larger file a packed/encrypted/compressed region starts and ends (e.g. an embedded payload inside an otherwise plain-text or structured file). Windows are non-overlapping and window_size bytes each; if that would produce more than max_results windows, the window size is automatically coarsened (and the actual size used is reported back) so the response stays a manageable length. Each window over entropy 7.5 is also counted in high_entropy_window_count as a quick signal.

file_carve(file, offset=0, length=None, categories=None, min_signature_length=4, max_results=500)

Scans for magic_numbers.yaml signatures at any offset in the file, not just the start — e.g. finding a PNG or ZIP hiding inside a firmware dump or memory image. Only signatures whose real-world position is offset 0 are used (a TAR-at-257 or ISO-at-32769 style signature only means anything relative to a whole file's own start, not an arbitrary carve position). min_signature_length filters out very short/generic signatures that would otherwise produce a lot of noise against padding/zero-filled regions; categories narrows (and speeds up) the scan to specific magic_numbers.yaml categories. Scanning the full default 32 MB range against all ~650 offset-0 signatures typically takes well under a second per MB on ordinary hardware, but large ranges are still slower than the other single-pass scanning tools since every signature is checked independently — narrow with offset/length or categories for faster interactive use on big files.

pe_sections(file)

Lists a PE (Windows .exe/.dll/.sys) file's section headers: name, virtual address/size, raw (on-disk) offset/size, and characteristics decoded into flags (executable, readable, writable, contains_code, ...). Extends the structural PE parsing magic_analyze already does (machine type, PE32 vs PE32+, DLL vs EXE) with the section table.

elf_sections(file)

Lists an ELF (Linux/Unix executable/object/shared-library) file's section headers: name (resolved via the file's own .shstrtab), type (PROGBITS, SYMTAB, DYNAMIC, ...), flags (alloc, exec, write, ...), address, file offset, and size. Handles both 32-bit and 64-bit, either endianness. Reports section_count: 0 with a note (rather than an error) for a stripped binary that has no section header table at all.

Safety notes

  • binary_write is destructive and has no undo, and so is xor_transform when called with write=true (its default, write=false, is read-only). Both patch the target file directly on disk with no confirmation prompt, dry-run mode, or backup — that's the caller's responsibility. Copy the file first if you want to be able to revert.
  • Every other tool (binary_read, binary_search, strings_search, magic_analyze, hex_dump, file_hash, binary_diff, entropy_scan, file_carve, pe_sections, elf_sections, and xor_transform in its default preview mode) is read-only and never modifies anything on disk.
  • Paths are resolved with Path.expanduser().resolve() — there is no sandboxing to a specific directory. This server can read/write anywhere the running Python process has filesystem permissions, by design (it's a general-purpose binary analysis toolbox, not a scoped file-upload service).
  • Large-file protections are per-call caps, not full streaming (except file_hash, which genuinely streams the whole file in 1 MiB chunks and has no size cap): binary_read / hex_dump / xor_transform preview mode cap at 64 KB per call, binary_write / xor_transform write mode at 10 MB per call, and binary_search / strings_search / entropy_scan / file_carve / binary_diff scan at most 32 MB per call by default. Use offset/length to page through bigger files. A pattern, string, or diff region that straddles the boundary between two separately-paged calls can be missed — if that matters, pass an explicit length large enough to cover the region of interest in one call.

How the MCP protocol is implemented (no SDK)

MCP's stdio transport is newline-delimited JSON-RPC 2.0: one JSON object per line on stdin (requests/notifications from the client) and one per line on stdout (responses from the server). mcp-binary-tools.py implements exactly that surface by hand:

  • initialize → returns protocolVersion, capabilities: {tools: {}}, serverInfo, and a short instructions string.
  • notifications/initialized → acknowledged silently (it's a notification, no response is sent).
  • tools/list → returns each tool's name, description, and JSON Schema inputSchema.
  • tools/call → dispatches to the matching Python function by name; exceptions are caught and returned as isError: true tool results (not JSON-RPC protocol errors), so the calling model sees a readable error message instead of a broken connection.
  • ping → returns an empty result.

stdout is reserved exclusively for JSON-RPC messages — nothing else in this script prints to stdout.

Layout

mcp-binary-tools.py   - the entire server: tool implementations, tool
                         registry, the magic_numbers.yaml loader, and the
                         native MCP/JSON-RPC stdio loop
magic_numbers.yaml     - editable file-signature database used by
                         magic_analyze (see that section above)

magic_numbers.yaml must sit next to mcp-binary-tools.py (the script locates it via Path(__file__).resolve().parent, independent of the current working directory) — if it's missing or fails to parse, magic_analyze keeps working but loses almost all named-format detection (see the magic_analyze section above for exactly what the minimal built-in fallback still covers).

Sources

magic_numbers.yaml's signatures were transcribed from:

  • Ilias1988/Magic-Bytes-List — the original archives, images, video_audio, documents, and executables entries.
  • leommoore's File Magic Numbers gist — 10 more images entries (FITS, Graphics Kernel System, IRIS rgb, ITC, NIFF, PM, Sun Rasterfile, Xfig, XPM, XCF Gimp), the documents PostScript entry, and the original misc category (the 4 PGP entries). Rows from this source that exactly duplicated an entry already transcribed from the first source (plain JPEG/PNG/TIFF/GZIP/ZIP/MZ/ELF/TAR, in each case with the same bytes) were skipped rather than re-added; rows with no fixed signature at all (Targa, X11 Bitmap, TAR pre-POSIX) were skipped too. Two rows described the same format as an existing entry but with a shorter, looser signature (Bzip's 2-byte magic vs. the existing 3-byte BZIP2 signature; gzip's 2-byte magic vs. the existing 3-byte GZIP signature) — both were kept as fallback entries positioned right after the more specific one, so the specific label always wins first (see the note field on Bzip (legacy) and GZIP (generic) in the YAML for the detail).
  • Wikipedia: List of file signatures — everything else: the rest of images, video_audio, documents, executables, and archives, plus every new category from fonts onward. This table is far larger than the first two sources (~340 rows), so the same duplicate/redundant-row skipping rules applied, plus a few extra cases worth calling out:
    • Genuinely ambiguous shared signatures (kept as separate entries, each with a note): Mach-O Universal Binary vs. Java Class (both CA FE BA BE), and Macromedia Director vs. Adobe Shockwave (initially looked identical at 8 bytes, but the full 12-byte signatures in the source table turned out to disambiguate them cleanly - see below).
    • Specific-before-generic pairs (both kept, ordered so the specific one wins): Canon RAW/CR2 before generic TIFF, Canon CR3 before the generic MP4 ftyp entry, a confirmed-Exif JPEG check before the looser APP1-marker one, Encapsulated PostScript v3.0/v3.1 before generic PostScript, POSIX/GNU ustar before generic TAR, AMR and SILK audio before Shell Script (both start with the Unix shebang bytes #!), UTF-32LE/BE byte-order-marks before UTF-16LE/BE ones (a UTF-16LE BOM is a byte-for-byte prefix of a UTF-32LE BOM), and a Lotus 1-2-3 v1 spreadsheet signature before CUR in extras (they share their first 4 bytes; extras is checked last specifically because of cases like this one).
    • In-place upgrades to a fuller, more precise signature found in this source: WMV/WMA now uses the full 16-byte ASF Header Object GUID instead of an 8-byte prefix, and FITS now uses the full 30-byte first-header-record text instead of just the 6-byte SIMPLE keyword.
    • Parsing quirks in the source table itself, resolved by hand: a few multi-line signatures (FITS's full record, AppleWorks 5/6, the VirtualBox VDI comment header, XML's UTF-32 prolog variants) are wrapped across several display lines in the wiki table in a way that looks identical to genuinely-separate alternative signatures (like GIF87a vs. GIF89a) - each was checked against the raw source and reassembled into one correct signature by hand. The Director/Shockwave pair above is the same story: the table's first row for each format only showed 8 bytes, but the other rows in the same group had the full 12 bytes needed to actually tell them apart.
    • Rows with no fixed signature (Targa, X11 Bitmap, TAR pre-POSIX, COM, .pyc, MXF's variable "run-in", a footer-relative Apple Disk Image signature) or too weak to be useful (a lone 0x00 byte, an all-zero Palm PDB signature) were skipped, same as for the second source.
  • Gary Kessler's File Signatures Table — by far the largest source (~540 header signatures covering ~630 named formats, since many entries share one signature), adding to every category above plus roughly 400 entries in misc. Its HTML table pairs each signature with one or more "extension + description" rows, so a single magic number legitimately maps to several named formats far more often than in the other three sources - this is where most of the note-carrying "shares this exact signature with N other formats" entries come from. Skipped as usual: exact duplicates, no-fixed-signature rows, trailer-only (end-of-file) signatures, and single-byte-style signatures too weak to be useful (0x00, 0x47, 0xFF, ...) - plus a few rows Kessler himself flags as speculative ("possibly, maybe, might be a fragment of an Ethernet frame...").
    • Specific-before-generic pairs: ~40 more cases in the same spirit as the Wikipedia source (e.g. a Synology-specific 4-byte GZIP variant before generic GZIP, EF BB BF 3C 3F before the plain UTF-8 BOM, FF D8 FF E0 ... 4A 46 49 46 00 before the existing JPEG/JFIF entry). Where several of Kessler's own entries needed inserting at the same point (e.g. three increasingly-specific XML-prolog signatures all ahead of generic XML), they're ordered longest-signature-first among themselves too, for the same reason.
    • Deliberately-generic entries moved to extras: a handful of Kessler's signatures are the bare prefix of a whole family of more specific signatures spread across multiple categories - bare RIFF (shared by Windows animated cursors, CorelDRAW, VirtualDub, and a dozen other RIFF-based formats already spread across images/video_audio/ documents/misc), a bare JPEG FF D8 start-of-image marker, and a PDF signature missing the real signature's trailing -. A single category's "append after the category's own entries" placement can't guarantee these land after every more-specific sibling in other categories, so they're placed in extras instead, which is always checked dead last.
  • aSecuritySite: Magic Numbers — a short (~46-row) reference table. Almost every row was an exact or redundant-subset duplicate of a signature already transcribed from one of the first four sources (e.g. its 2-byte TIF/4-byte PNG/2-byte JPEG entries are all shorter, less specific prefixes of signatures already here; its Word/Excel/PowerPoint/Visio/MSI/Outlook-message rows are the same OLE Compound File signature already covered by DOC's note), so those were skipped. Two rows were genuinely new:
    • MP4 (mp42 brand) - a specific ftyp brand not yet in video_audio, inserted before the generic MP4 entry alongside the existing isom/M4A/MOV brand-specific ones.
    • SDF File - a real conflict: its listed signature (78 9C) is byte-for-byte identical to the existing "zlib (default compression, no dict)" entry. Kept as a second entry with a note on both sides explaining the tie, in the same spirit as the DOC/XLS/PPT and Java-Class/Mach-O ambiguities - SDF File can never actually be the reported result (the zlib entry is checked first and matches identically), so it exists purely for documentation. A couple of rows looked like they might be new but weren't reliable enough to add: a .pst signature one byte longer than the well-established 4-byte Outlook PST magic, and a .mdb signature missing the 4 leading bytes every other source (and the MS-JET spec) agrees real Access files start with - both read as transcription simplifications in the source rather than genuinely different signatures, so the existing, more reliable entries were left as-is instead of adding unverified variants.

The original handful of extras entries (byte-swapped Mach-O, an extra MP3 frame variant, CUR, Unix compress, SQLite 3, Unix ar) predate all five sources above and aren't transcribed from any of them; the rest of extras is the deliberately-generic entries from the Kessler source described above.

About

MCP server for low-level binary file analysis, inspection, searching, and editing with minimal external dependencies.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages