
zipnative MCP — ZIP creation, inspection, secure extraction & verification
io.github.Nizoka由 GitHub 账号 Nizoka 发布v1.1.0更新于 Nov 2, 2026
ZIP MCP server: create, inspect, verify, extract, canonicalize, merge, modify. 15 tools
概览
本地 MCP 服务器,让助手创建、检查、校验、解压、合并和修改 ZIP 压缩包,解压默认采用安全策略。
- 可以做什么
- 检查与列出压缩包:一次调用生成报告、确定性判定、CI 式检查与断言、分页条目清单
- 读取与校验条目:单条目内容以文本或 base64 返回、字节区间、原始压缩数据、深度 CRC 与头部校验
- 创建与修改压缩包:由内联数据或沙箱文件生成可复现 ZIP,并支持合并、修改、规范化、压缩整理
- 安全解压:拦截路径穿越、符号链接、重复路径与压缩炸弹,支持试运行计划与流式写入沙箱目录
- 扫描与路径净化:对截断或不可寻址的流做前向扫描,对名称列表做路径穿越检查
- 适用场景
- 适合助手在本机处理 ZIP 类文件(压缩包、JAR、Office 文档、EPUB、APK 等)的场景:在信任某个压缩包前先检查、生成可复现的构建产物,或在严格安全规则下解压内容。
- 运行要求
- 通过 npx(或全局 npm 安装)以 stdio 在本机运行,需要 Node.js 22 或更高版本。可选环境变量:ZIPNATIVE_MCP_OUTPUT_DIR(沙箱目录绝对路径,未设置时只能用 base64 输入输出)、ZIPNATIVE_MCP_CACHE_DIR、ZIPNATIVE_MCP_PORT(在回环地址上启用 HTTP)、ZIPNATIVE_MCP_HTTP_TOKEN,以及四个大小与线程上限。无需账号或 API 密钥。
安装
在 SourceWeft 中
- 打开 控制台中的 zipnative MCP — ZIP creation, inspection, secure extraction & verification,将其添加到工作区。
- 为需要使用其工具的对话启用该服务。
Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。
其他 MCP 客户端
参照 仓库 中的启动说明。
README
zipnative-mcp
MCP server for ZIP archives — deterministic creation, inspection and listing without extracting, random-access entry reads (in memory or by byte ranges for sandbox archives larger than memory), secure-by-default extraction, one-call verification, forward scanning of truncated streams, incremental modification, canonicalisation and merging without recompression, CRC-32 and raw-DEFLATE inflation — 15 tools and 7 prompts on the zipnative engine (zero-dependency, ISO/IEC 21320-1 conformance validated in CI), for Claude Desktop, Cursor, ChatGPT and any MCP client. No network, ever.
[npm version] [npm downloads] [Node version] [License: MIT] [CI] [ISO 21320-1 · veraZIP] [MCP] [zipnative] [TypeScript] [OpenSSF Scorecard] [CodeQL]
✨ Features
zipnative-mcp 1.1.0 (on zipnative 1.1.0) exposes 15 tools to any MCP host, listed here in tools/list order. The consumer contract — catalogue, decision tree, token economy, recipes and the complete error table — is docs/AGENT_CONTRACT.md.
What every tool guarantees:
- 🔐 Secure by default — every engine guard is on; a relaxation is an explicit, named input (
rejectTraversal,rejectSymlinks,onDuplicate,limits) that skips, never emits an unsafe path or materialises a link. Overlaps, central/local divergence and Zip64 spoofing have no opt-out at all. - 🌐 No network, ever — the server has no network code path: no telemetry, no GitHub, no URL from any argument, no remote byte-range source, no operator-configurable endpoint. The only filesystem boundary is
ZIPNATIVE_MCP_OUTPUT_DIR. - 🔁 Reproducible by default, deterministic on request —
create_zipemits canonical order, DOS-epoch timestamps and UTF-8 names unless you opt out, and every DOS date is written and read in UTC (dosTimeMode: 'utc'), so a pinned date gives the same bytes in Paris and on a UTC runner;compression.deterministic: truepins the pure-TypeScript encoder for the same SHA-256 on every runtime and is the only setting under whichsummary.deterministicistrue;canonicalize_zipbrings any foreign archive into the canonical form;parallelis byte-identical. - 🧭 Frozen
ZIP_*error codes, verbatim — the engine's 39-code vocabulary reaches you unchanged in_meta.error.code(with the entry name, the limit that fired, both CRCs, …), plus the 17 wrapper codes, each with a remedy. Branch on the code, never on the message. - 🪙 Token-frugal projections — the read tools accept
verbosity: 'summary'andfields: […]; a produced archive arrives as an embeddedresourceblock, never instructuredContent(the block carries the bytes inbloband repeats them in itsdata:URI, sooutputMode: 'file'— aresource_link— is the token-frugal choice for anything sizeable). - ⏹ Cancellable, with progress — the request's cancellation signal and, when the client sends a
progressToken, a progress sink reach every tool whose work scales with its input exceptverify_zip(zipnative'sverifyZipis synchronous and takes no signal: a verify runs to completion and returns its report). A cancelled call is never cached and a partial output file is removed; over MCP the SDK sends no response to a cancelled request (CANCELLEDis the in-process code). - 📏 ISO/IEC 21320-1 conformance gate — every archive the tools write is validated clause by clause by an engine-independent validator (veraZIP) on Linux and Windows in CI and again before publish.
All archive-producing tools (create_zip, modify_zip, canonicalize_zip, merge_zip; inflate_raw for raw bytes) support two output modes:
base64(default) — the archive is returned as an embeddedresourcecontent block: the bytes are inresource.blob, andresource.urirepeats them as adata:application/zip;base64,…URI (unchanged since 1.0.0, so a client that decodes the URI keeps working) — the archive therefore travels twice in the block;structuredContentcarries{ mode, sizeBytes, summary, diagnostics, diagnosticCounts }, never the bytes.file— the archive is streamed into a sandboxed directory configured viaZIPNATIVE_MCP_OUTPUT_DIR(≤ 4 GiB by default,ZIPNATIVE_MCP_MAX_FILE_OUTPUT_BYTESmoves the ceiling; never overwritten) and the result carries aresource_link(the cheapest response;structuredContent.filePathand the text line give the file's absolute host path, as in 1.0.0). File I/O is disabled unless this variable is set; absolute paths, traversal, non-container extensions and NUL bytes are all rejected, and the real path of every file read and of every parent written must stay inside the sandbox (a planted symlink or junction isSECURITY_VIOLATIONon both sides). The same sandbox serveszipPath/sourcePathinputs, so acreate_zip → modify_zip → verify_zip → extract_zipchain never re-sends the bytes.
Token-frugal reads. The read tools (inspect_zip, list_zip_entries, read_zip_entry, verify_zip, scan_zip_forward, sanitize_entry_paths, describe_engine) and extract_zip accept two optional inputs:
verbosity: 'summary'— a compact scalar-only verdict (drops the rows and payloads). E.g.verify_zip→{ ok, error, entryCount, verifiedCount, failedCount, skippedCount, diagnosticCount };inspect_zipkeepsdeterministic,canonicalLayout,checksPassed.fields: ['a', 'b.c']— projects the structured result to named dot-paths (≤ 16); composes afterverbosity. Unmatched paths are omitted and reported in_meta.unmatchedFields(with_meta.availableFields); a path holding a__proto__,constructororprototypesegment isVALIDATION_ERROR.
Smallest "is this archive intact?" probe: { "zipBase64": "…", "verbosity": "summary", "fields": ["ok", "error", "failedCount"] } on verify_zip.
Archives larger than memory. inspect_zip, list_zip_entries and read_zip_entry take access: 'auto' | 'memory' | 'range'. A sandbox zipPath above the 1 GiB in-memory cap is opened by byte ranges ('auto', the default, or always with 'range'): the engine's openZipRange over the sandbox file's own handle reads the end record and the central directory, then only the entries you touch — never a URL, never a remote source. See docs/guides/LARGE_ARCHIVES.md.
The server also ships seven MCP prompts — secure_extraction, reproducible_archive, incremental_update, forensic_scan, verify_before_trust, governance_contract, draft_issue_workflow — and exposes every file in the sandbox as a zipnative://output/{+path} resource.
Why zipnative?
zipnative-mcp inherits every guarantee of the underlying engine:
- Zero runtime dependencies in the engine — pure TypeScript, no native bindings, no
eval; one API across Node ≥ 22, browsers, Deno, Bun and Workers (this server adds only the MCP SDK and zod: three runtime dependencies in total). - Safe by default — path traversal, symlinks, duplicate names, decompression bombs, overlapping entries, parser-differential smuggling and Zip64 spoofing are refused, not guessed at; every parser loop runs under a named, CWE-tagged, caller-configurable bound (the nine
ZipLimitskeys). - Random access and streaming — read one entry from a multi-gigabyte archive without touching the rest, in memory or through a byte-range source; iterate unseekable streams with bounded memory.
- Deterministic — a written determinism contract: reproducible layout always, byte-stable per environment by default, the same SHA-256 on every runtime under
compression.deterministic: true, and a canonical form any archive can be brought into without recompression. - Incremental modification — replace, remove, add or rename entries without recompressing the untouched 99 % (append-only), compact for true deletion, transplant entries between archives.
- A frozen 39-code error vocabulary and
verifyZip()— one call, a machine-readable report that never throws for archive problems. - The first open clause-by-clause ISO/IEC 21320-1:2015 validator, blocking in CI.
- What it will NOT do: no encryption or decryption (no AES in the engine 1.x — AES is an engine 2.0 candidate; ZipCrypto is broken), no other archive formats, no multi-disk archives, no archive repair, no filesystem I/O in the engine, no network access, ever.
🚀 Installation
Requirements: Node.js ≥ 22. From source: git clone https://github.com/Nizoka/zipnative-mcp.git && cd zipnative-mcp && npm ci --ignore-scripts && npm run build && node dist/cli.js.
⚙️ Configuration
Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
Claude Code
.mcp.json at the root of the project (the repository ships one that drives the built server from dist/cli.js):
Cursor / Continue / Zed / Windsurf / Cline / Roo Code
Any MCP-compatible client that supports stdio servers will work. Use the same command + args + env triple. Example for Cursor (~/.cursor/mcp.json):
Windsurf / Cline / Roo Code use the same shape inside their respective MCP config files.
VS Code
.vscode/mcp.json:
🌐 Supported AI Ecosystem & Clients
zipnative-mcp is designed for MCP-native environments and works with clients that support MCP over stdio or Streamable HTTP: Claude Desktop, Claude Code, Cursor, Continue, Zed, Windsurf, Cline, Roo Code, VS Code, ChatGPT (Streamable HTTP), the MCP Inspector, and any client built on the MCP TypeScript / Python SDKs. Because the server never opens a socket of its own, it is also safe to run inside air-gapped or egress-restricted hosts.
It belongs to the "native" family of servers with pdfnative-mcp (PDF) and pkinative-mcp (PKI): the same thin-wrapper architecture, sandbox, cache, projection and governance conventions. The same engine is on the command line as zipnative-cli.
🔌 MCP protocol compliance
The server is built on the MCP TypeScript SDK v2 (@modelcontextprotocol/server ^2.3.1) and speaks MCP 2026-07-28:
- Stateless serving —
server/discoverreplaces the session handshake; every result carriesresultTypeand the_metaserverInfoenvelope. Over HTTP, 2026-07-28 clients sendMcp-Method/Mcp-Nameheaders and theMCP-Protocol-Versionheader with eachPOST /mcp(a 2026-07-28 request without it is refused by the SDK with 400 /-32020). - Cache hints —
tools/listandprompts/listarepublicwith a 24 httlMs,server/discoverispublicfor 1 h, andresources/list/resources/templates/list/resources/readareprivatewithttlMs: 0(sandbox files are per-host user data). - Protocol errors —
tools/callwith an unknown tool name is a JSON-RPC error (-32602,[UNKNOWN_TOOL] Unknown tool: …) rather than anisErrorresult, and an unknown or malformed resource URI is-32602(Invalid params,UNKNOWN_RESOURCE), as the specification requires;isError: trueis reserved for execution failures. - Cancellation and progress —
notifications/cancelled(or an HTTP client that goes away) stops the work between chunks on every tool whose work scales with its input — the engine for the archive tools, the tool's own chunk loop forcompute_crc32andinflate_raw— nothing is cached and a partial output file is removed.verify_zipis the exception: zipnative'sverifyZipis synchronous and takes no signal (an engine limitation), so a verify runs to completion and its result is returned;sanitize_entry_paths,describe_engineanddraft_governance_issuedo no work worth cancelling. As the MCP cancellation rules require, the SDK sends no response to a cancelled request;CANCELLED(data.remedy: 'retry') is what the tool returns in-process (callToolDirect); the server writes nothing to stderr for it. A request carrying_meta.progressTokenreceivesnotifications/progresswhoseprogressis strictly increasing across the whole call, throttled to one every 200 ms plus the one that completes a known entry count; without a token, no notification is sent. - Automatic legacy fallback — a client that opens with
initializeis served through the SDK's legacy path on both stdio and HTTP for every revision the SDK negotiates: 2024-10-07, 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25 (legacy) next to 2026-07-28 (modern). Nothing changes for existing hosts.tests/protocol-revisions.test.tsproves each legacy revision negotiates exactly that revision and serves the 15 tools;tests/stdio-modern.test.tsandtests/http-modern.test.tsprove the 2026-07-28 path on stdio and HTTP. - HTTP —
POST /mcponly;GET/DELETE /mcpanswer 405 (no SSE resumability; the server is stateless). Bound to127.0.0.1with aHost/Originguard (foreign values → 403; theOriginport must equal the server port);ZIPNATIVE_MCP_HTTP_TOKENadds an opt-in bearer-token gate (401+WWW-Authenticatewithout it); both checks run before the request body is read; bodies up to 256 MiB are accepted (413 above); a 30 s headers timeout, a 300 s request timeout, a 5 s keep-alive timeout and at most 100 request headers; every response carriesX-Content-Type-Options: nosniffandCache-Control: no-store. - stdio — the SDK's 10 MiB default frame cap is raised to 256 MiB so multi-MiB base64 archives fit in one
tools/call; as in every SDK release, a request sent beforeinitializeis dropped without a reply on the legacy path. - Output schemas — every
structuredContentvalidates against the tool'soutputSchema(a 2026-07-28 MUST), includingverbosity: 'summary'andfieldsprojections: the read tools declare projectable schemas (all properties optional,additionalProperties: falsekept). Input schemas carry no$schemakeyword and no$refby policy (some hosts forwardinputSchemato function-calling APIs that reject unknown keywords).serverInfocarrieswebsiteUrl; the resource template iszipnative://output/{+path}.
The tools/call payload (content, structuredContent, isError) is identical between the 2026-07-28 path and the legacy path; tests/http-modern.test.ts asserts it, and tests/schema-conformance.test.ts validates structuredContent with the SDK's JSON Schema 2020-12 validator.
Compatibility matrix — every negotiated revision, on both transports, with the test that proves it:
Autonomy proof. tests/autonomy.test.ts replays one scripted agent scenario (S0 handshake → S1 tools/list → S2 describe_engine → S3 create_zip file / base64 / parallel → S4 verify_zip → S5 inspect_zip checks → S6 list_zip_entries → S7 read_zip_entry / inflate_raw / compute_crc32 → S8 modify_zip append then compact → S9 extract_zip dry run then files → S10 resources/* → S11 scan_zip_forward → S12 hostile refusals → S13 ceilings → S14 protocol errors → S15 draft_governance_issue → S16 prompts/get → S17 clean shutdown) against the built dist/cli.js on four transports — stdio legacy, stdio modern, HTTP legacy, HTTP modern with ZIPNATIVE_MCP_HTTP_TOKEN — under a sandbox, a cache directory, ZIPNATIVE_MCP_MAX_ENTRIES=5000 and ZIPNATIVE_MCP_WORKERS=2, asserting no interaction, no unexpected isError, a code and a remedy on every refusal, a cache hit over the wire, a JSON-only stdout and a clean exit. tests/cancellation.test.ts drives cancellation and progress over MCP. To replay the scenario by hand from Claude Code, Claude Desktop or the MCP Inspector, see docs/guides/LOCAL_TESTING.md § "Drive it from Claude Code / Claude Desktop / MCP Inspector" (the repository ships a ready .mcp.json).
Environment variables
All eight are declared in server.json and validated at startup; an invalid value refuses to start — never a silent fallback (a ZIPNATIVE_MCP_PORT outside 1–65535 or not an integer, a relative ZIPNATIVE_MCP_OUTPUT_DIR or ZIPNATIVE_MCP_CACHE_DIR, a malformed ceiling, a weak token). The four operator ceilings are read once, at boot; change any variable by restarting the server. The token is never logged; the startup log on stderr states which HTTP mode is active and the ceilings in force.
The operator ceilings. The engine's nine named ZipLimits bounds can be raised per call through the limits fragment for trusted input only. Four of those keys are bounded by two of the ceilings above — ZIPNATIVE_MCP_MAX_UNCOMPRESSED_BYTES (maxEntryUncompressedSize, maxEntryCompressedSize, maxTotalUncompressedSize) and ZIPNATIVE_MCP_MAX_ENTRIES (maxEntries, and the scan_zip_forward maxEntries input); ZIPNATIVE_MCP_WORKERS bounds create_zip parallel.workers. A per-call value above its ceiling is refused with LIMIT_CEILING_EXCEEDED and data.env names the variable an operator would raise. The fourth ceiling, ZIPNATIVE_MCP_MAX_FILE_OUTPUT_BYTES, bounds no input: it caps every file written in file mode (OUTPUT_TOO_LARGE). There is no "unbounded" and no ceiling a tool argument can move. describe_engine reports the defaults, the ceilings (ceilings.env lists the four variables) and every cap in force.
🛠 Tool reference
Every archive-consuming tool takes exactly one of zipBase64 (the raw archive as base64 exactly once; a data:…;base64, prefix is tolerated; decoded ≤ 128 MiB) or zipPath (a relative path inside the sandbox; container extensions only — .zip .jar .war .ear .docx .xlsx .pptx .odt .ods .odp .epub .vsix .nupkg .whl .apk .ipa .xpi .crx .kmz; ≤ 1 GiB buffered, larger through access: 'range' on the three random-access tools). Every engine-touching tool takes limits (the nine named ZipLimits bounds — maxEntries, maxEntryUncompressedSize, maxEntryCompressedSize, maxTotalUncompressedSize, maxCompressionRatio, maxNameBytes, maxExtraFieldBytes, maxCommentBytes, maxCentralDirectoryBytes; values above the operator ceilings → LIMIT_CEILING_EXCEEDED) and strict (escalate the first diagnostic to ZIP_STRICT_DIAGNOSTIC). The read tools take verbosity ('full' | 'summary') and fields (≤ 16 dot paths). Every archive tool's result carries diagnostics[], de-duplicated by code and entry and capped at 200 rows (diagnosticsTruncated: true when the cap bites), plus diagnosticCounts (exact per code) on every one of them but verify_zip, whose 1.0.0 shape is kept. Error results: content[0].text = "<tool> failed [CODE]: message", isError: true, _meta.error = { code, message, data? }. A cancelled call ends in CANCELLED in-process (over MCP the cancelled request gets no response); verify_zip cannot be interrupted once its verification starts. Full input/output schemas are advertised by tools/list; the complete error table is in docs/AGENT_CONTRACT.md.
Dates are UTC. Every engine call runs with dosTimeMode: 'utc'. A date input with a Z or an offset is converted to UTC; an offset-less date-time is read as the UTC wall clock (the DOS fields 1.0.0 wrote for it on a UTC host); lastModified and earliestDate / latestDate read back as UTC instants. A pinned date therefore gives the same bytes on every host, whatever its time zone (tests/reproducible-build.test.ts runs the built server under two time zones). On a non-UTC host this is a change from 1.0.0 on both sides: a Z / offset date writes different DOS fields than 1.0.0 wrote there (compression.deterministic: true bytes included — they equal 1.0.0's on a UTC host), and every archive's dates read back shifted by the zone offset (the DOS epoch reads 1980-01-01T00:00:00.000Z, where 1.0.0 in Paris reported 1979-12-31T23:00:00.000Z); an archive another tool wrote in local time reads shifted by its producer's offset.
inspect_zip
Read-only archive report in ONE call, the zipnative inspect --json shape: size, entry / file / directory counts, Zip64, comment (+ commentHex), compressed vs uncompressed totals and ratio, per-method counts, encrypted / symlink / data-descriptor / Zip64 / cp437 / duplicate / unsafe-name counts, earliest / latest date, prependedData / multipleEocd, a determinism verdict (epochTimestamps, canonicalOrder, utf8Flags, noDataDescriptors, canonicalLayout, deterministic) and every diagnostic. The sub-checks come from the engine's analyzeDeterminism(); the default deterministic keeps the 1.0.0 formula (epoch timestamps + canonical order + UTF-8 flags — a streamed archive is reproducible but not canonical; a pinned non-epoch date is reproducible but reports deterministic: false here — this is the structural verdict, distinct from create_zip.summary.deterministic), so no existing CI gate flips. determinismReport: true adds the engine's stricter canonical-form verdict (also Zip64-only extras and the constant version-made-by) with every offending entry and the rule it breaks (≤ 200 rows, counts exact). It opens the archive eagerly by default: overlapping entries, entries reaching into the central directory or past EOF, and Zip64 spoofing are refused with their ZIP_* code rather than summarised. A method / CRC / size divergence between the central directory and a local header is not an extent problem: verify_zip (localHeaderMatch) is the one-call gate for it, and read_zip_entry / extract_zip raise ZIP_CD_LFH_MISMATCH / ZIP_SIZE_MISMATCH when the entry is read. check / assert turn it into a CI gate.
Outputs: the report above plus checks[] ({ check, ok, detail }) and checksPassed when check / assert was supplied; determinismReport on request; access: 'range' when the byte-range reader served the call; diagnostics[]. Summary: archiveBytes, entryCount, isZip64, compressedBytes, uncompressedBytes, encryptedCount, unsafeNameCount, duplicateNameCount, deterministic, canonicalLayout, diagnosticCount, checksPassed.
(fields alone keeps the checks[] rows; verbosity: 'summary' would drop them and leave only checksPassed.)
Errors: VALIDATION_ERROR, INPUT_TOO_LARGE, INPUT_NOT_FOUND, SECURITY_VIOLATION, INVALID_EXTENSION, IO_ERROR, LIMIT_CEILING_EXCEEDED, ZIP_STRICT_DIAGNOSTIC, and the structural ZIP_* refusals (ZIP_EOCD_NOT_FOUND, ZIP_EOCD_INCONSISTENT, ZIP_CD_INCONSISTENT, ZIP_RECORD_TRUNCATED, ZIP_SIGNATURE_MISMATCH, ZIP_ZIP64_LOCATOR_MISSING, ZIP_ZIP64_EOCD_MISPLACED, ZIP_ZIP64_CONTRADICTION, ZIP_ENTRY_OVERLAP, ZIP_CD_LFH_MISMATCH, ZIP_VALUE_UNREPRESENTABLE, ZIP_UNSUPPORTED_MULTI_DISK, ZIP_LIMIT_EXCEEDED).
list_zip_entries
Read-only central-directory inventory without decompressing anything: every entry (duplicates included) in directory order as a full row — index, name, nameEncoding, isDirectory, isSymlink, method / methodName, compressedSize, uncompressedSize, ratio, crc32, lastModified (UTC), isEncrypted, usesZip64, usesDataDescriptor, unixMode, sanitizedPath (null = the extraction gate would refuse it), comment, decoded flags, versionMadeBy / versionNeeded, hostSystem, attributes, localHeaderOffset, DOS date / time, named extraFields, rawNameHex, commentHex. Paged so a 100 000-entry archive stays token-bounded. nameEncoding is 'utf-8' or 'cp437' (the engine's 'custom' only comes from a nameDecoder, which this server does not expose).
Outputs: entryCount, matchedCount, offset, limit, returnedCount, hasMore, isZip64, entries[], access (byte-range reads only), diagnostics[]. Summary: the counts plus names (this page).
Errors: as inspect_zip (structural refusals appear on validate: 'eager' or when the central directory itself is broken); an invalid glob (an inverted class range such as [z-a]) is VALIDATION_ERROR.
read_zip_entry
Read ONE entry without extracting the archive, by name (exact, case-sensitive; the last duplicate wins) or index (central-directory position — disambiguates duplicates). mode: 'data' (default) returns the decompressed content, CRC-verified, as dataBase64 or as text with encoding: 'utf8'; a range reads a byte window through the chunked stream (bounded memory, no CRC for a window). mode: 'raw' returns the compressed payload verbatim (zero-copy — feed it to inflate_raw). mode: 'verify' returns verification: { ok, crcMatch, sizeMatch, localHeaderMatch, skipped? } only — the engine's verdict, with the real local-header comparison even for an entry it cannot decode and skipped ∈ 'encrypted' | 'stream-only-codec' | 'unsupported-method'. Encrypted entries are refused in modes data and raw (ZIP_UNSUPPORTED_ENCRYPTION — no decryption in this engine). Over a byte-range reader (access: 'range', or a zipPath above 1 GiB), only the bytes of the entry you read are fetched; limits.maxEntryCompressedSize bounds one fetched payload.
Outputs: entry (the full row), mode, sizeBytes, crcVerified, encoding, dataBase64 / text + textLength, range: { offset, length, returned }, verification, access (byte-range reads only), diagnostics[]. Inline cap 16 MiB per entry (ENTRY_TOO_LARGE, data.remedy: 'range'): in mode: 'data' read a range window or use extract_zip file mode; a raw payload above the cap is not retrievable inline at all — the same remedies yield the decompressed content. An out-of-range index is ZIP_ENTRY_NOT_FOUND naming the valid range (data.remedy: 'list_zip_entries').
Errors: ZIP_ENTRY_NOT_FOUND, ENTRY_TOO_LARGE, ENTRY_NOT_UTF8, ZIP_UNSUPPORTED_ENCRYPTION, ZIP_UNSUPPORTED_METHOD, ZIP_CRC_MISMATCH, ZIP_SIZE_MISMATCH, ZIP_DEFLATE_CORRUPT, ZIP_DEFLATE_TRUNCATED, ZIP_INFLATE_OUTPUT_OVERFLOW, ZIP_CD_LFH_MISMATCH, ZIP_ENTRY_OVERLAP, ZIP_RECORD_TRUNCATED (a byte-range source that delivers fewer bytes than asked), plus the structural refusals and wrapper codes above.
verify_zip
Deep verification in one call — zipnative's verifyZip report verbatim: eager structural validation, then every entry's CRC-32 / size / local-header agreement. ok is true when the structure is valid AND every verifiable entry passed. This tool never fails for a problem with the archive: a structural refusal lands in error: { code: 'ZIP_*', message }; an unverifiable entry carries its skipped reason. The counts follow the engine's archive rule: an encrypted or stream-only-codec entry whose local header agrees with the central directory is skipped and does not fail the archive; an entry whose method has no codec here (skipped: 'unsupported-method' — anything but 0 store / 8 deflate) and an encrypted entry whose local header contradicts the central directory are failed (ok: false). Only caller mistakes (bad limits, bad input) are isError. Verify before you trust: run it on any archive from an untrusted source before extract_zip.
Outputs: ok, error (or null), entryCount, verifiedCount, failedCount, skippedCount, entries[] ({ name, ok, crcMatch, sizeMatch, localHeaderMatch, skipped? }), entriesTruncated, diagnostics[] (de-duplicated by code and entry, ≤ 200 rows, diagnosticsTruncated: true only when the cap bites; no diagnosticCounts, to keep the 1.0.0 shape). An entry stored with a method this server has no codec for (anything but 0 store / 8 deflate — Zstandard, LZMA, …) comes back with skipped: 'unsupported-method' and ok: false and counts in failedCount: relay it as "not checked", not as "corrupt" (merge_zip refuses such an entry under verify: true). The verification cannot be interrupted once it starts: zipnative's verifyZip is synchronous and takes no signal, so a cancellation that arrives during it is ignored and the call runs to completion.
Errors (caller mistakes only): VALIDATION_ERROR, INPUT_TOO_LARGE, INPUT_NOT_FOUND, SECURITY_VIOLATION, INVALID_EXTENSION, IO_ERROR, LIMIT_CEILING_EXCEEDED, ZIP_LIMIT_INVALID.
extract_zip
Extract with every engine guard ON unless you name the relaxation: zip-slip / absolute / drive / UNC / NUL / ADS / Windows-device names → ZIP_PATH_TRAVERSAL (rejectTraversal: false SKIPS them, listed in skipped; an unsafe path is never emitted); symlinks → ZIP_SYMLINK_REJECTED (rejectSymlinks: false extracts the target text as data, never a link); duplicate sanitized paths → ZIP_EXTRACT_DUPLICATE_PATH (onDuplicate); declared sizes / ratios beyond limits → ZIP_LIMIT_EXCEEDED; overlapping entries and central/local divergence are always refused. The archive is opened eagerly before any plan in both modes, so the dry run runs the same open-time guards as the extraction: overlapping entries, entries reaching into the central directory or past EOF and Zip64 spoofing are refused with their ZIP_* code before a single plan row is returned (and a file-mode extraction fails before its first write, data.writtenCount: 0); a central/local divergence of method, CRC or sizes is detected when the entry is read — verify_zip (localHeaderMatch) is the one-call gate for it. In base64 mode the plan (filter, sanitize, symlink policy, duplicates, budgets) is then computed without touching any payload — includeData: false returns that plan as a dry run. In file mode every file is streamed under outputDir inside the sandbox: only the engine's sanitized path is joined, the parent's real path is checked, files are opened exclusively, and a symlink accepted through rejectSymlinks: false is written as a regular file holding the link target. The archive itself is read in memory (≤ 1 GiB for a zipPath).
Outputs: mode, dryRun, fileCount, totalBytes, entries[] ({ path, entryName, index, sizeBytes, crc32, method, dataBase64? | filePath? }), skipped[] ({ entryName, reason: 'unsafe-path' }), outputDir, directoriesCreated, diagnostics[]; file mode adds up to 50 resource_link content blocks. Summary: mode, dryRun, fileCount, totalBytes, skippedCount, paths.
Errors: ZIP_PATH_TRAVERSAL, ZIP_SYMLINK_REJECTED, ZIP_EXTRACT_DUPLICATE_PATH, ZIP_LIMIT_EXCEEDED, ZIP_ENTRY_OVERLAP, ZIP_CD_LFH_MISMATCH, ZIP_UNSUPPORTED_ENCRYPTION, ZIP_UNSUPPORTED_METHOD, ZIP_CRC_MISMATCH, ZIP_SIZE_MISMATCH, ZIP_DEFLATE_CORRUPT, ZIP_DEFLATE_TRUNCATED, ENTRY_TOO_LARGE, OUTPUT_TOO_LARGE, OUTPUT_EXISTS, IO_ERROR, MISSING_OUTPUT_PATH, SECURITY_VIOLATION, CANCELLED, plus the structural refusals and wrapper codes.
scan_zip_forward
Walk LOCAL headers in stream order with bounded memory (zipnative's central-directory-less reader iterateZipEntries). The only tool that works on a truncated archive or a stream cut mid-way — the stream must start at a local header: it cannot skip an SFX / prepended prefix and refuses one with ZIP_SIGNATURE_MISMATCH (route prefixed archives to inspect_zip, which reports prependedData / ZIP_PREPENDED_DATA and shifts offsets; modify_zip mode: 'compact' drops the prefix) — and the only one whose result is NOT authoritative: forward iteration trusts local headers ALONE, so a hostile archive can present different content here than inspect_zip / list_zip_entries report (the upload-scanner differential). Prefer those tools whenever the whole archive is available; use this one for forensics. Names are NOT sanitized — sanitizedPath shows what the extraction gate would use. A zipPath is streamed from disk through one open handle without the 1 GiB buffered cap. The wrapper never reads a PK signature: stoppedAt is derived from byte accounting (the engine leaves the rest of the stream unread when it meets a central directory). Corrupt deflate data is ZIP_DEFLATE_CORRUPT / ZIP_DEFLATE_TRUNCATED on every runtime tier (1.0.0 reported ZIP_DECOMPRESSION_FAILED here).
Outputs: trust: 'local-headers-only', entryCount, keptCount, entries[] (rows with kept and data: { verified, bytesProduced, dataBase64? }; isSymlink / usesZip64 / unixMode are null), stoppedAt ('central-directory' — the reader met a central directory and left the rest unread | 'eof' — the whole stream was consumed without one: truncated or CD-less | 'max-entries' — more local headers remained past the budget (an archive with exactly maxEntries entries ends at 'central-directory') | 'error'), truncated, inputBytes, bytesDelivered, maxEntries (the budget in force after clamping), error, diagnostics[].
Errors: ZIP_STREAM_TRUNCATED, ZIP_SIGNATURE_MISMATCH, ZIP_RECORD_TRUNCATED, ZIP_DESCRIPTOR_MISMATCH, ZIP_UNSUPPORTED_CD_LESS_DESCRIPTOR, ZIP_UNSUPPORTED_ENCRYPTION, ZIP_UNSUPPORTED_METHOD, ZIP_CRC_MISMATCH, ZIP_SIZE_MISMATCH, ZIP_DEFLATE_CORRUPT, ZIP_DEFLATE_TRUNCATED, ZIP_INFLATE_OUTPUT_OVERFLOW, ZIP_LIMIT_EXCEEDED, ENTRY_TOO_LARGE, OUTPUT_TOO_LARGE, LIMIT_CEILING_EXCEEDED (all engine codes land in error under tolerateTruncation: true).
sanitize_entry_paths
Apply zipnative's single traversal gate sanitizeEntryPath() to a list of names: the safe /-separated relative form each maps to (join it under your extraction root — never the raw name), or null with the rule that fired. Use it when you extract with another tool or plan an external filesystem sink. No archive needed.
Outputs: count, rejectedCount, results[] ({ name, sanitized, rejected, reason? } with reason ∈ empty, nul, absolute, drive, unc, traversal, ads, device-name, no-segments). Summary: count, rejectedCount, rejectedNames.
Errors: VALIDATION_ERROR.
create_zip
Write a ZIP from inline entries (dataBase64 | text | sourcePath in the sandbox, or directory: true). Reproducible by default: canonical order (raw UTF-8 name bytes), DOS-epoch timestamps (every date written in UTC), UTF-8 names, constant attributes — defaultDate: 'now' opts out (ZIP_TIMESTAMP_NOT_PINNED diagnostic). compression.deterministic: true pins the pure-TS encoder so the bytes are identical on every runtime (summary.deflateTier: 'pure-pinned'); the default node-zlib tier is byte-stable per environment only. Three determinism verdicts, read them where they apply: summary.deterministic is true only with compression.deterministic: true on every entry and no wall-clock date (cross-runtime identity — a default call reports false although its bytes are reproducible on the same runtime); summary.sha256 (includeSha256, base64 and file mode) is the proof; inspect_zip.determinism.deterministic is the structural verdict against the canonical defaults (epoch timestamps + canonical order + UTF-8 flags — a pinned non-epoch date reproduces but reports false there). order: 'insertion' keeps call order for EPUB / JAR (mimetype first, stored). stream: true on a sourcePath feeds the file through addStream from one open handle (data-descriptor layout, bounded memory); a streamed entry that crosses 4 GiB is ZIP_UNSUPPORTED_ZIP64_STREAMING unless zip64: true opts it into the Zip64 streaming layout (APPNOTE 4.3.9.2). parallel compresses with the worker pool — byte-identical output; with no workers the engine default max(1, min(cores − 1, 8)) applies, bounded by ZIPNATIVE_MCP_WORKERS. Zip64 is emitted exactly when a field overflows — reachable here: up to 100 000 entries per call (the engine default maxEntries and the ZIPNATIVE_MCP_MAX_ENTRIES ceiling remain the effective bound), so a 65 536-entry archive auto-promotes. A name ending in / denotes a directory and cannot carry a payload (VALIDATION_ERROR — drop the slash or pass directory: true). Every archive it writes is ISO/IEC 21320-1 conformant (CI-validated).
A zip64: true entry below 4 GiB still carries the Zip64 records, which some streaming readers (Java's ZipInputStream) cannot read — opt in only for entries that may cross 4 GiB, and raise ZIPNATIVE_MCP_MAX_FILE_OUTPUT_BYTES to write the result into the sandbox.
Outputs: the archive as an embedded resource (≤ 50 MiB) or a resource_link; structuredContent: mode, sizeBytes, filePath, summary (typed in the tool's outputSchema: entryCount, fileCount, directoryCount, streamedCount, uncompressedBytes, order, deterministic, deflateTier, parallel: { requested, workers }, sha256), diagnostics[].
Errors: VALIDATION_ERROR, INPUT_TOO_LARGE, INPUT_NOT_FOUND, OUTPUT_TOO_LARGE, OUTPUT_EXISTS, IO_ERROR, MISSING_OUTPUT_PATH, INVALID_PATH, INVALID_EXTENSION, SECURITY_VIOLATION, LIMIT_CEILING_EXCEEDED, CANCELLED, ZIP_INVALID_ENTRY_NAME, ZIP_INVALID_OPTION, ZIP_LIMIT_EXCEEDED, ZIP_UNSUPPORTED_ZIP64_STREAMING, ZIP_INPUT_TOO_LARGE, ZIP_STRICT_DIAGNOSTIC, ZIP_API_MISUSE.
modify_zip
Edit an existing archive without recompressing anything: operations add / replace / remove / rename / setComment applied in order. The input is opened eagerly in both modes — the overlap table and every entry's extent are checked before any operation runs, so an overlapping or truncated-header archive is refused even in mode: 'append'; before an append-mode save every non-encrypted entry is also checked against its local header and its declared sizes (a raw read, nothing decompressed), so a method / CRC / size divergence (ZIP_CD_LFH_MISMATCH) or a declared-size bomb (ZIP_LIMIT_EXCEEDED) is refused as mode: 'compact' and the other writers refuse it. Payloads are otherwise copied verbatim and never decoded: a corrupt payload or an unsafe name already in the archive (../../evil.txt) is carried into the output unchanged — extract_zip still refuses such a name. mode: 'append' (default) = save(): the original bytes stay verbatim and edits are appended — fast and byte-preserving, BUT removed or replaced payloads remain in the file (data remanence: recoverable by anyone; a ZIP_DEAD_BYTES_RATIO diagnostic fires past 50 % dead bytes, and 7-Zip is known to read the stale payload). mode: 'compact' = saveCompact(): a rewrite without recompression that sorts the entries by raw name bytes (an insertion-ordered layout — EPUB mimetype first, JAR manifest first — is not preserved: rebuild such a container with create_zip order: 'insertion'), removed data truly gone, SFX prefix dropped; add canonical: {…} to write the canonical deterministic form after the edits (the rules of canonicalize_zip). No edits and an unchanged comment return the same bytes (summary.noOp). Archives with duplicate entry names are refused.
Outputs: the archive as an embedded resource or a resource_link; summary (saveMode, operationsApplied, entryCountBefore, entryCountAfter, inputBytes, outputBytes, grewByBytes, noOp, canonical (present when the canonical form was written), sha256), diagnostics[].
Errors: ZIP_ENTRY_NOT_FOUND, ZIP_ENTRY_EXISTS, ZIP_DUPLICATE_ENTRY_NAME, ZIP_INVALID_ENTRY_NAME, ZIP_ENTRY_OVERLAP, ZIP_CD_LFH_MISMATCH, ZIP_RECORD_TRUNCATED, ZIP_LIMIT_EXCEEDED, ZIP_STRICT_DIAGNOSTIC, plus the structural refusals, the output codes and the wrapper codes (data.operationIndex / data.op name the failing operation).
canonicalize_zip
Make ANY archive reproducible. zipnative's canonicalizeZip() rewrites an archive you did not build — Gradle, Maven, zip, Windows Explorer, 7-Zip, bsdtar — into the canonical deterministic form the engine's own writer emits: entries sorted by raw name bytes, one timestamp (the DOS epoch, or the pinned date), the UTF-8 flag on every name, no extra field but Zip64, the constant version-made-by (0x032D), no data descriptor. No payload is recompressed or decoded: the compressed bytes are copied bit for bit, so a corrupt or encrypted entry travels unchanged — and names are copied verbatim too, so an unsafe name such as ../../evil.txt stays in the output (canonicalForm can still be true; extract_zip refuses the name, and merge_zip, which writes names through the checked writer, refuses it with ZIP_INVALID_ENTRY_NAME) — and the result is byte-stable across runtimes because no codec runs. Idempotent — canonicalising the output again yields the same bytes (summary.alreadyCanonical). The output is checked once more by the engine's analyzeDeterminism() with the same pinned date, and the verdict is summary.canonicalForm; verify it later with inspect_zip { check: ['canonical-form'], canonicalDate }. Archives with duplicate entry names are refused, as modify_zip refuses them. To change content and canonicalise in one call, use modify_zip mode: 'compact' with canonical.
Outputs: the archive as an embedded resource (≤ 50 MiB) or a resource_link; summary (entryCount, inputBytes, outputBytes, alreadyCanonical, canonicalForm, pinnedDate (UTC ISO-8601, or null for the DOS epoch), keepComments, keepExternalAttributes, sha256), diagnostics[].
Errors: VALIDATION_ERROR (a malformed or out-of-range date), INPUT_TOO_LARGE, INPUT_NOT_FOUND, SECURITY_VIOLATION, INVALID_EXTENSION, IO_ERROR, LIMIT_CEILING_EXCEEDED, OUTPUT_TOO_LARGE, OUTPUT_EXISTS, MISSING_OUTPUT_PATH, CANCELLED, ZIP_DUPLICATE_ENTRY_NAME, ZIP_LIMIT_EXCEEDED, ZIP_STRICT_DIAGNOSTIC, and the structural ZIP_* refusals.
merge_zip
Several archives into one without recompressing anything: zipnative's ZipWriter.addFromReader() transplants each entry's compressed bytes with its method, CRC, sizes, timestamp, attributes, comment and extra fields (Zip64 recomputed). With verify: true (the default) every entry is decompressed once and checked against its CRC and size before it is copied, so a corrupt payload is refused with its frozen code instead of being propagated; an entry this server cannot decompress (a method other than 0 store / 8 deflate) is refused with ZIP_UNSUPPORTED_METHOD rather than copied unchecked (verify: false copies it as it is; summary.verified reports whether verification was on). Each name passes the writer's name rules, so a source entry the writer refuses — the ./ directory entry bsdtar writes, a .. segment, a device name — fails the merge with ZIP_INVALID_ENTRY_NAME and data.sourceIndex: exclude it with that source's filter. Encrypted entries are refused (ZIP_UNSUPPORTED_ENCRYPTION — they cannot be read raw); filter them out to merge the rest. Name collisions: onConflict: 'error' (default) lets the engine refuse the second occurrence (ZIP_DUPLICATE_ENTRY_NAME); 'first' / 'last' keep the first / last occurrence across the sources in order and report the others. The checked transplant is the only form exposed — the engine's addRaw() (where the caller vouches for CRC and sizes) stays unexposed, so an agent cannot forge an inconsistent archive through this tool. The result is ordered like create_zip (order: 'canonical' by default); run canonicalize_zip on it for the full canonical form. Every source is opened in memory.
Outputs: the archive as an embedded resource or a resource_link; summary (sourceCount, entryCount, inputBytes, outputBytes, onConflict, verified, order, perSource[] ({ index, label, entryCount, taken, filteredOut, dropped }), dropped[] ({ name, source }, ≤ 200 rows), sha256), diagnostics[]. A refusal names the source in data.sourceIndex.
Errors: VALIDATION_ERROR, INPUT_TOO_LARGE, INPUT_NOT_FOUND, SECURITY_VIOLATION, INVALID_EXTENSION, IO_ERROR, LIMIT_CEILING_EXCEEDED, OUTPUT_TOO_LARGE, OUTPUT_EXISTS, MISSING_OUTPUT_PATH, CANCELLED, ZIP_DUPLICATE_ENTRY_NAME, ZIP_INVALID_ENTRY_NAME (a source name the writer refuses — filter it out), ZIP_CRC_MISMATCH, ZIP_SIZE_MISMATCH, ZIP_DEFLATE_CORRUPT, ZIP_DEFLATE_TRUNCATED, ZIP_UNSUPPORTED_METHOD (verify: true on a method with no codec here), ZIP_UNSUPPORTED_ENCRYPTION, ZIP_CD_LFH_MISMATCH, ZIP_ENTRY_OVERLAP, ZIP_LIMIT_EXCEEDED, ZIP_STRICT_DIAGNOSTIC, and the structural ZIP_* refusals.
compute_crc32
The ZIP checksum (IEEE 802.3 CRC-32, the engine's slice-by-8 implementation) of inline bytes / text or a sandbox file (streamed in 1 MiB chunks through one open handle, ≤ 1 GiB). seed continues a running CRC across consecutive chunks; expect compares against a known value. Use it to cross-check an entry's crc32 from list_zip_entries against a file on disk.
Outputs: crc32 (unsigned), hex (8 lowercase digits), byteLength, seed, matches (when expect was given).
Errors: VALIDATION_ERROR, INPUT_TOO_LARGE, INPUT_NOT_FOUND, SECURITY_VIOLATION, IO_ERROR, CANCELLED (checked between 1 MiB chunks).
inflate_raw
Decompress a raw DEFLATE (RFC 1951) stream — e.g. the payload from read_zip_entry mode: 'raw' — with a mandatory maxOutput bound through zipnative's resumable inflater (constant memory, exact bytesConsumed, trailing bytes reported as leftover; ZIP_INFLATE_OUTPUT_OVERFLOW past the bound). method: 'store' is a bounded pass-through; a numeric method id selects a registered codec (none beyond 0 / 8 in this server → ZIP_UNSUPPORTED_METHOD).
Outputs: the inflated bytes as an embedded resource (application/octet-stream) or a resource_link; summary (method, methodName, bytesIn, bytesOut, bytesConsumed, leftover, finished, maxOutput, trailingWarning).
Errors: ZIP_INFLATE_OUTPUT_OVERFLOW, ZIP_DEFLATE_CORRUPT, ZIP_DEFLATE_TRUNCATED, ZIP_UNSUPPORTED_METHOD, ZIP_UNSUPPORTED_CODEC_MODE, ZIP_API_MISUSE, CANCELLED (checked between input chunks), the input wrapper codes and the file-mode codes (OUTPUT_EXISTS, MISSING_OUTPUT_PATH, INVALID_EXTENSION). A maxOutput above the inline or file cap is lowered to that cap, so an oversized result is ZIP_INFLATE_OUTPUT_OVERFLOW (data.configured = the effective bound), never OUTPUT_TOO_LARGE.
describe_engine
Offline preflight (no archive needed): server / engine / tool-API versions and the protocol string, the tool count, the deflate tier in use and the pinned deterministic tier, runtime codecs (node:zlib, CompressionStream, DecompressionStream) and worker threads, the codec registry (codecs[] with sync, stream and incremental — whether the forward and byte-range readers decode the codec chunk by chunk), the engine's default limits (the nine keys), the four operator ceilings (maxUncompressedBytes, maxEntries, maxWorkers, maxFileOutputBytes, with env naming the variables), every server cap, whether the sandbox and the cache are enabled, the 39 frozen ZIP_* error codes and 12 diagnostic codes, and the engine exports this server deliberately does not expose, each with its reason (nameDecoder, addRaw, registerCodec, the worker URL, …). network is always 'none'. Never cached.
Outputs: server, engine, capabilities, codecs[], defaultLimits, ceilings, caps, sandbox, network, errorCodes[], diagnosticCodes[], unexposed[].
Errors: VALIDATION_ERROR.
draft_governance_issue
Draft a governance-compliant GitHub issue locally for a human to review and submit — for zipnative (engine behaviour: parsing, writing, limits, codecs) or zipnative-mcp (wrapper behaviour: schemas, sandbox, transport). The server never contacts GitHub — it has no network code path at all; it returns the draft Markdown plus a machine-readable compliance report. Present both to the user, then STOP: they review and submit under their own identity.
Outputs: title, issueType, targetRepo, outputMode, filePath, sizeBytes, draftMarkdown, warnings[], compliance (zeroDependencyConfirmed, reproductionCommand, reproductionResult, duplicateSearchPerformed, affectedPackages, identityReminderShown, humanGate, environment).
A draft that proposes a runtime dependency or sets duplicateSearchPerformed: false is rejected with GOVERNANCE_VIOLATION; a missing reproduction (a required input) is VALIDATION_ERROR. Other errors: VALIDATION_ERROR, MISSING_OUTPUT_PATH, INVALID_EXTENSION, OUTPUT_EXISTS, SECURITY_VIOLATION. See the governance_contract and draft_issue_workflow prompts and docs/guides/AI_GOVERNANCE.md for the full human-in-the-loop contract.
🔐 Security model
zipnative-mcp runs inside the host process and exposes a stdio MCP server (or a loopback-only HTTP endpoint). It treats every archive as untrusted input and performs no I/O outside the configured sandbox other than the opt-in response cache under ZIPNATIVE_MCP_CACHE_DIR.
- The sandbox (
ZIPNATIVE_MCP_OUTPUT_DIR) is the one filesystem boundary for reads (zipPath,sourcePath,resources/read, amerge_zipsource, the forward scanner's stream, a byte-range reader) and writes (outputPath,outputDir, extracted files, governance drafts). Paths must be relative; absolute / UNC paths,.., NUL bytes and non-container extensions are rejected; the real path of the file read and of the parent written must stay inside (a symlink or junction planted in the sandbox isSECURITY_VIOLATIONon both sides — the check is on the file itself for reads, so a file symlink cannot slip past a parent-only check); files are opened exclusively (OUTPUT_EXISTS— never overwritten); a failed or cancelled write removes the partial file. Unset ⇒ base64 only (the opt-in response cache underZIPNATIVE_MCP_CACHE_DIRis the only other file I/O). - Inputs are validated against strict JSON Schemas + Zod at the boundary of every tool — unknown or misspelt keys (top-level or nested) are
VALIDATION_ERROR; base64 is sanity-checked (data:prefix tolerated, base64-twice rejected) before any parser runs;fieldspaths refuse prototype segments and the projection walks own properties only. - The error boundary — every
ZipErrorkeeps its frozen code; anything else that escapes the engine or the runtime isENGINE_ERRORwithdata.errorClass: 'internal', its message stripped of absolute paths. No error text carries a host path. - Encryption is never performed: encrypted entries are detected,
skippedbyverify_zipwhen their local header agrees, refused by the read paths and bymerge_zipwithZIP_UNSUPPORTED_ENCRYPTION. There is no password input. - Every engine diagnostic goes through a sink — nothing is ever printed to stdout on the stdio transport.
- HTTP transport (
ZIPNATIVE_MCP_PORT) binds loopback only with a Host / Origin guard, request timeouts and a header-count bound; it has no authentication unlessZIPNATIVE_MCP_HTTP_TOKENis set (then401without a valid bearer token).
The full threat model, the supply chain and the reproducibility promise are in SECURITY.md; the defences with their CWE ids in docs/guides/SECURITY.md.
Network & egress
None, ever. The server has no network code path: no telemetry, no GitHub, no update check, no URL from any tool argument, no remote byte-range source (the engine's openZipRange runs over a sandbox file handle only), and — unlike its PDF sibling — no operator-configurable endpoint of any kind. describe_engine reports network: 'none' and tests/no-network.test.ts holds the source tree to it. Archive bytes only ever flow back in the JSON-RPC response or into the sandbox. The engine itself never opens a socket, never touches the filesystem and never evals.
Every row is a constant in src/caps.ts and is advertised by describe_engine.caps (stdioMaxFrameBytes, maxBase64InputBytes, maxPathInputBytes, maxBase64OutputBytes, maxFileOutputBytes, maxEntryInlineBytes, maxExtractInlineTotalBytes, maxListLimit, defaultListLimit, maxCreateEntries, maxModifyOperations, maxMergeSources, maxMergeInputBytes, maxSanitizeNames, maxDiagnostics, maxDeterminismOffenders, maxFilterNames, maxFilterGlobs, maxGlobLength, maxExtraFields, maxIssueTextChars, maxFieldsPaths, defaultScanMaxEntries, maxResourceLinks, maxRangeLength, maxListedResources, maxResourceWalkDepth, maxResourceReadBytes, cacheTtlSeconds, cacheMaxBytes); the file-output cap in force is describe_engine.ceilings.maxFileOutputBytes.
See SECURITY.md for the responsible disclosure process.
🧪 Conformance (veraZIP)
ZIP has no veraPDF — JHOVE never shipped a ZIP module and no ISO/IEC 21320-1 validator existed — so the zipnative ecosystem ships its own gate, veraZIP: an ISO/IEC 21320-1:2015 (Document Container File) validator vendored from the engine at its 1.1.0 commit (scripts/verazip-core.mjs) that raw-parses the bytes with its own EOCD / central-directory / local-header reader and never imports zipnative — a validator that shared the engine's parser would attest the engine with the engine.
npm run validate:zip builds the server, drives the real tools/call handler (create_zip, modify_zip, canonicalize_zip and merge_zip across every writer path — buffered, stream: true, a Zip64 streamed entry, parallel, compression.deterministic, order: 'insertion', extra fields, comments, Unix modes, append, compact and compact + canonical, canonicalisation of an append-mode archive (idempotence asserted) and with a pinned date, a merge) plus raw-crafted files to write a 43-archive corpus, then validates every file clause by clause (23 check ids). The 39 conformant archives include four hostile-but-conformant ones (zip-slip, a Windows device name, duplicate paths, a symlink entry): the ISO profile constrains the container, not the meaning of names, so they PASS the validator and extract_zip must refuse them — the gate checks both. 4 negative canaries, raw-crafted, must be rejected with a declared clause id. The expected verdict is 39 PASS and 4 XFAIL. Level 0 (the ISO clauses) needs no external tool and always runs; level 1 re-tests every conformant archive with the foreign integrity tools present on the machine (unzip -t, 7z t, python -m zipfile -t, tar -tf, jar tf) and skips the absent ones visibly; VERAZIP_REQUIRED=1 (set in CI) fails closed. The corpus definition is recorded in docs/data/verazip.json and the manifest of each run in test-output/zip/manifest.json.
A PASS proves that every archive this server writes is a well-formed ISO/IEC 21320-1 document container: one self-consistent end-of-central-directory record, a central directory that agrees with every local header, only store / deflate methods, no encryption, no multi-disk fields, valid Zip64 records exactly where a field overflows, a data descriptor whose width matches the local header's Zip64 extra. It is not a certification — validation evidence against a specific validator revision — and conformant does not mean safe: the security guards exist on top of conformance.
The gate is blocking: the ci workflow runs it inside scripts/gate.ts --ci --require-all on Linux (Node 22 and 24), Windows and macOS, verazip.yml repeats it on Linux and Windows on every push and pull request to main (and on manual dispatch), and it runs again before every publish. Every archive a test produces through a tool must also pass assertValidZip (ISO conformance included). See docs/guides/CONFORMANCE.md.
🧪 Local development
npm run gate (scripts/gate.ts) is the single definition of green — 1643 tests across 79 test files, the coverage thresholds of vitest.config.ts (94 / 90 / 95 / 94), and the built server driven over stdio, where stdout must carry JSON-RPC frames only. It prints one line per step and writes the logs to test-output/.gate/<step>.log; --only <step> runs one step, --from <step> resumes, --json gives machine output (call the script directly to pass flags: PowerShell swallows a bare -- after npm run). The gate is hermetic: TZ=UTC, every inherited ZIPNATIVE_MCP_* variable scrubbed, no network. The individual steps are ordinary npm scripts:
Smoke-test the server over stdio:
Drive the built server from Claude Code with the repository's .mcp.json (run npm run build first; Claude Code asks for approval on first use and injects ${CLAUDE_PROJECT_DIR}), from Claude Desktop with absolute paths, or from the MCP Inspector — the scenario to replay and the expected results are in docs/guides/LOCAL_TESTING.md § "Drive it from Claude Code / Claude Desktop / MCP Inspector".
Run it over HTTP with a bearer token and a sandbox:
Contributors: start at
AGENTS.md(the repository rules shared by every coding agent and contributor) and see CONTRIBUTING.md for the full workflow — the gate, examples-as-tests, the sample baseline,assertValidZipon every archive a test writes, the veraZIP corpus, and the MCP Inspector.
📣 Release process
zipnative-mcp follows the same release formalism as zipnative, pdfnative-mcp and pkinative-mcp:
- One release note file per tag in
release-notes/vX.Y.Z.md(no emojis);CHANGELOG.mdmirrors each release bullet list; the GitHub Release body is copied from the note npx tsx scripts/release-prepare.ts --version X.Y.Zapplies the mechanical part of a version bump (it never commits, tags or publishes); the version moves in lock-step acrosspackage.json,src/version.ts,server.jsonandCITATION.cff(tests/metadata.test.ts) and indocs/assets/ecosystem.json(npm run verify:docs);TOOL_API_VERSIONbumps only when a schema or error code changes- A release branch must pass
npx tsx scripts/gate.ts --publish --require-all— every step, the veraZIP corpus included, with no skip - npm publication is handled by GitHub Actions Trusted Publishing (OIDC), without
NPM_TOKEN:publish.ymlrunsverify(the publish gate, onenpm pack, the CycloneDX SBOM, the digests) →publish(the protectednpm-publishenvironment, the handed-on tarball published with--provenance) →attest(the registry bytes compared, a build-provenance attestation for the tarball and the SBOM, both attached to the release) →registry(server.jsonto the MCP registry through amcp-publisherpinned by SHA-256) - Every job starts with
step-security/harden-runner(skipped on macOS, which the action does not support), checks out withpersist-credentials: false, installs withnpm ci --ignore-scriptsand runs under least-privilegepermissions; actions are pinned by commit SHA - Human-in-the-loop: coding agents prepare and verify; the maintainer pushes, opens the pull request, tags and publishes (
.github/AGENT_RULES.md)
See release-notes/TEMPLATE.md for the canonical structure and publication checklist, and CONTRIBUTING.md for the release procedure.
📚 Project structure
🗺 Roadmap
v1.1.0 is the first minor release, on zipnative 1.1.0: two new tools (canonicalize_zip, merge_zip), byte-range reads, the Zip64 streaming opt-in, cancellation and progress, UTC timestamps, and a live catalogue checked as a superset of the frozen 1.0.0 one. The full plan — released milestones, what is blocked upstream and the long-term direction — lives in ROADMAP.md.
Not exposed, by design:
- Encryption and decryption — none in the engine 1.x (ZipCrypto is broken; AES is an engine 2.0 candidate); only then will a password input exist here.
nameEncoding/nameDecoder— legacy non-UTF-8 names are decoded as cp437, as the specification says; an agent-supplied decoder is not exposed.addRaw, custom codecs, a remote byte-range source —merge_zipis the checked transplant,registerCodecis executable code, and the server has no network path.
Have a feature idea? Open an issue or PR — or let an agent draft one with draft_governance_issue and submit it yourself.
⭐ Star the project
If zipnative-mcp is useful to you, please ⭐ this repository — and consider also starring the underlying engine Nizoka/zipnative. Stars help others discover the project and motivate continued development.
🤝 Contributing
Contributions are very welcome. Please read CONTRIBUTING.md, check the open issues, and follow the code of conduct.
📄 License
MIT © 2026 Nizoka. Third-party material: THIRD-PARTY-NOTICES.md.
zipnative-mcp is built on top of zipnative and the Model Context Protocol TypeScript SDK.
来源:README.md,提交 5af5abb
工具
0版本历史
2- v1.1.0最新Oct 19, 2026
- v1.0.0Sep 16, 2026
评分
已满足适用检查项的 50%
该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。
- 近 30 天内可连通25 分
请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。
- 已声明认证方式10 分
请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。
- 已列出工具15 分
请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。
- 描述必需15 分
- 图标10 分
- 文档15 分
- 许可证10 分