# zipnative-mcp — AI client integration

> **`zipnative-mcp` 1.0.0 is the MCP server that gives AI assistants the
> zipnative engine as thirteen typed tools** — inspect, list, read,
> verify, extract, scan, sanitise, create, modify, CRC, inflate, describe,
> draft — plus seven prompts and sandboxed file resources, over stdio or
> loopback Streamable HTTP, with no network access, ever. Pins
> `zipnative ^1.0.0`.

[`zipnative-mcp`](https://github.com/Nizoka/zipnative-mcp) exposes the
engine to any client that speaks the
[Model Context Protocol](https://modelcontextprotocol.io) — Claude
Desktop, Claude Code, Cursor, VS Code, Continue, Zed, Windsurf, Cline,
Roo Code, ChatGPT over Streamable HTTP, the MCP Inspector, or anything
built on the MCP SDKs. With it installed you can say to your assistant:

> *"Verify this build artefact, confirm it is reproducible and contains
> no encrypted or unsafe entries, then extract the manifest."*

…and the assistant will call `verify_zip`, `inspect_zip` with its CI
checks, and `read_zip_entry`, reasoning on structured results instead
of prose. The server adds only the MCP SDK and zod to the engine —
three runtime dependencies in total; the engine itself keeps zero.

## Installation

```bash
# Run directly with npx — the recommended form for MCP clients
npx -y zipnative-mcp

# Or install globally
npm install -g zipnative-mcp
zipnative-mcp
```

**Requirements:** Node.js ≥ 22. The published package carries npm
provenance and an MCP registry manifest (`server.json`, name
`io.github.Nizoka/zipnative-mcp`).

## Configuration by client

Every configuration below sets `ZIPNATIVE_MCP_OUTPUT_DIR`. It is
optional — without it the tools work on base64 only — but it is what
enables `zipPath` / `sourcePath` inputs, `outputMode: 'file'` and the
`zipnative://output/` resources.

### Claude Desktop

Edit `~/Library/Application Support/Claude/claude_desktop_config.json`
(macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows), then
restart Claude Desktop:

```json
{
  "mcpServers": {
    "zipnative": {
      "command": "npx",
      "args": ["-y", "zipnative-mcp"],
      "env": { "ZIPNATIVE_MCP_OUTPUT_DIR": "/Users/you/Documents/mcp-archives" }
    }
  }
}
```

### Claude Code

```bash
claude mcp add zipnative --env ZIPNATIVE_MCP_OUTPUT_DIR=/path/to/mcp-archives -- npx -y zipnative-mcp
```

Or commit a project-scoped `.mcp.json` with the same `mcpServers`
shape as Claude Desktop — the zipnative-mcp repository ships one.

### Cursor · Continue · Zed · Windsurf · Cline · Roo Code

Any stdio-capable client takes the same `command` + `args` + `env`
triple. Cursor (`~/.cursor/mcp.json` or the project `.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "zipnative": {
      "command": "npx",
      "args": ["-y", "zipnative-mcp"],
      "env": { "ZIPNATIVE_MCP_OUTPUT_DIR": "/Users/you/Documents/mcp-archives" }
    }
  }
}
```

### VS Code

`.vscode/mcp.json`:

```json
{
  "servers": {
    "zipnative": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "zipnative-mcp"],
      "env": { "ZIPNATIVE_MCP_OUTPUT_DIR": "${workspaceFolder}/.mcp-archives" }
    }
  }
}
```

### Streamable HTTP (ChatGPT and other HTTP hosts)

```bash
ZIPNATIVE_MCP_PORT=3000 ZIPNATIVE_MCP_HTTP_TOKEN="$(openssl rand -hex 24)" npx -y zipnative-mcp
# → POST http://127.0.0.1:3000/mcp   (Authorization: Bearer <token>)
```

The endpoint binds **loopback only** and accepts `POST /mcp` alone: any
other path answers 404, `GET` / `DELETE` answer 405, a foreign `Host` /
`Origin` answers 403 (DNS-rebinding guard — and because the SDK's own
check is port-agnostic, an `Origin` whose port differs from the server's
port is refused too, so a local web page on another port is still
cross-site), bodies above 256 MiB answer 413. **Without
`ZIPNATIVE_MCP_HTTP_TOKEN` the endpoint has no authentication** — other
local processes can reach it; with the token, a missing or wrong bearer
answers 401 + `WWW-Authenticate`.

## Environment variables

| Variable | Default | Meaning |
|---|---|---|
| `ZIPNATIVE_MCP_OUTPUT_DIR` | *(unset)* | The **one** sandbox: `zipPath` / `sourcePath` inputs are read from it, `outputMode: 'file'` / `outputDir` outputs are written under it (exclusive create, never overwritten), and `resources/*` list and read it. The *real* path of every file read and every parent written must stay inside — a planted symlink or junction is `SECURITY_VIOLATION`. Unset ⇒ base64 only |
| `ZIPNATIVE_MCP_CACHE_DIR` | *(unset)* | Opt-in SHA-256-keyed result cache (1 h TTL, 256 MiB LRU, **plaintext at rest**). Keys are namespaced by tool API version, package version and engine version, so an upgrade never serves bytes written by the previous engine. Never caches path inputs, file output, `defaultDate: 'now'`, `parallel`, `describe_engine` or `draft_governance_issue`; a hit carries `_meta.cached: true`. A directory that cannot be created disables the cache with one `[zipnative-mcp] cache disabled: …` line on stderr rather than failing |
| `ZIPNATIVE_MCP_PORT` | *(unset → stdio)* | A port 1–65535 serves Streamable HTTP on `http://127.0.0.1:<port>/mcp` instead of stdio |
| `ZIPNATIVE_MCP_HTTP_TOKEN` | *(unset)* | *(secret)* Bearer token for the HTTP transport — ≥ 16 characters, no whitespace; a weaker value refuses to start. Compared constant-time, never logged |
| `ZIPNATIVE_MCP_MAX_UNCOMPRESSED_BYTES` | `8589934592` (8 GiB) | Operator ceiling for `limits.maxEntryUncompressedSize` and `limits.maxTotalUncompressedSize` (integer ≥ 1024, read once at startup). A per-call value above it is `LIMIT_CEILING_EXCEEDED` |
| `ZIPNATIVE_MCP_MAX_ENTRIES` | `100000` | Operator ceiling for `limits.maxEntries` and `scan_zip_forward.maxEntries` (integer ≥ 1) |
| `ZIPNATIVE_MCP_WORKERS` | `8` | Operator ceiling for `create_zip.parallel.workers`; `0` disables worker threads (integer ≥ 0) |

An invalid ceiling refuses to start with one `[zipnative-mcp] fatal:`
line and exit 1.

The same seven variables are declared in the package's MCP registry
manifest, `server.json` (`runtimeHint: "npx"` with `-y zipnative-mcp`,
transport `stdio`): each carries `isRequired: false`, a `format`
(`filepath` for the two directories, `number` for the port and the
three ceilings) and `isSecret: true` on `ZIPNATIVE_MCP_HTTP_TOKEN`
alone, so a registry-driven host can prompt for them correctly.

## Protocol and transport

- **MCP 2026-07-28** on the MCP TypeScript SDK v2: stateless serving
  (`server/discover`), the `_meta` envelope, and cache hints
  (`tools/list` and `prompts/list` public for 24 h, `server/discover`
  public for 1 h, the resource methods private with `ttlMs: 0`).
- **Schemas are declared, not re-validated on the way out.** Every tool
  advertises an `inputSchema` and an `outputSchema`; inputs are
  enforced at the boundary by Zod twins of those schemas, and
  `structuredContent` is built to follow the output schema — but the
  server deliberately stays on the SDK's low-level `Server` surface and
  does **not** run a runtime validator over its own results (the
  `McpServer` helper that would do so cannot express the `verbosity` /
  `fields` projections, hence the read tools' output schemas mark every
  property optional). A validating host is free to check.
- **`instructions`.** The `initialize` / `server/discover` response
  carries a server-instructions block — a decision tree (which tool for
  which question), the security posture and the common pitfalls — that
  a host may inject into the assistant's context; the seven prompts
  repeat the same material on demand.
- **`tools/list` `_meta`.** Every tool entry carries
  `_meta.apiVersion` (the tool-API version, `1.0.0`, independent from
  the package version and bumped only when a schema changes) and
  `_meta.examples` — one or two self-contained, executable inputs per
  tool (`<zip-base64>` stands for the archive).
- **Automatic legacy fallback** for clients that open with `initialize`:
  2024-10-07, 2024-11-05, 2025-03-26, 2025-06-18 and 2025-11-25 are
  negotiated on both transports; the `tools/call` payload is identical
  on every path.
- **stdio** raises the SDK's frame cap to 256 MiB so multi-MiB base64
  archives fit in one call; nothing is ever printed to stdout except
  JSON-RPC.
- **Protocol errors** are JSON-RPC errors (`-32602` for an unknown
  tool, resource or prompt), as the specification requires; `isError:
  true` is reserved for execution failures.
- SIGINT / SIGTERM close the server gracefully, exit 0.

## Tool reference

Thirteen tools, in the order `tools/list` returns them. Every
archive-consuming tool takes exactly one of `zipBase64` (the raw archive
as base64, a `data:…;base64,` prefix 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).

The shared inputs are not on every tool — the table below says which:

- `limits` (the eight `ZipLimits` bounds — `maxEntries`,
  `maxEntryUncompressedSize`, `maxTotalUncompressedSize`,
  `maxCompressionRatio`, `maxNameBytes`, `maxExtraFieldBytes`,
  `maxCommentBytes`, `maxCentralDirectoryBytes`; above the operator
  ceilings → `LIMIT_CEILING_EXCEEDED`) on the eight archive-consuming
  and archive-producing tools: `inspect_zip`, `list_zip_entries`,
  `read_zip_entry`, `verify_zip`, `extract_zip`, `scan_zip_forward`,
  `create_zip`, `modify_zip`.
- `strict` (escalate the first diagnostic to `ZIP_STRICT_DIAGNOSTIC`)
  on seven of those — **not** on `verify_zip`, whose whole point is to
  report rather than throw.
- `verbosity` (`'full'` | `'summary'`) and `fields` (≤ 16 dot paths) on
  the read tools: `inspect_zip`, `list_zip_entries`, `read_zip_entry`,
  `verify_zip`, `extract_zip`, `scan_zip_forward`,
  `sanitize_entry_paths`, `describe_engine`.
- `diagnostics[]` **and** `diagnosticCounts` (de-duplicated by code and
  entry, 200 kept, `diagnosticsTruncated: true` beyond) on the seven
  `strict` tools; `verify_zip` carries `diagnostics[]` alone (the
  engine report's own list); `inflate_raw` carries an always-empty
  `diagnostics: []`; `compute_crc32`, `sanitize_entry_paths`,
  `describe_engine` and `draft_governance_issue` carry neither.

Tool annotations: the read tools are `readOnlyHint: true`; every tool
is `idempotentHint: true`, `destructiveHint: false`, `openWorldHint:
false`. Each tool also has a human `title`, listed here as `tools/list`
returns it.

| Tool | Title | Purpose | `limits` | `strict` | projection |
|---|---|---|---|---|---|
| `inspect_zip` | Inspect archive (facts, determinism verdict, CI checks) | One-call forensic report with a determinism verdict and CI assertions (`check`, `assert`) | ✓ | ✓ | ✓ |
| `list_zip_entries` | List entries (paged inventory) | Paged central-directory inventory with filters and the sanitized path each entry would extract to | ✓ | ✓ | ✓ |
| `read_zip_entry` | Read one entry (random access) | One entry by name or index — decompressed, raw, or verified; byte ranges | ✓ | ✓ | ✓ |
| `verify_zip` | Verify archive integrity (one call, never throws for archive problems) | The engine's `verifyZip` report verbatim — never fails for an archive problem | ✓ | — | ✓ |
| `extract_zip` | Extract (secure by default) | Secure extraction, inline or into the sandbox, with a dry-run plan | ✓ | ✓ | ✓ |
| `scan_zip_forward` | Scan forward (truncated / unseekable streams — NOT authoritative) | Forward scan of a truncated or streamed archive — explicitly not authoritative | ✓ | ✓ | ✓ |
| `sanitize_entry_paths` | Sanitize entry paths (the traversal gate) | The engine's traversal gate applied to a list of names | — | — | ✓ |
| `create_zip` | Create archive (reproducible; deterministic:true for cross-runtime identical bytes) | A reproducible archive from inline or sandbox entries, optionally parallel or streamed | ✓ | ✓ | — |
| `modify_zip` | Modify archive (incremental, no recompression) | Add, replace, remove, rename, comment — append-only or compact, no recompression | ✓ | ✓ | — |
| `compute_crc32` | Compute CRC-32 | CRC-32 of inline bytes, text or a sandbox file, seedable | — | — | — |
| `inflate_raw` | Inflate raw DEFLATE (bounded) | Raw DEFLATE decompression under a mandatory output bound | — | — | — |
| `describe_engine` | Describe engine & server capabilities | Offline preflight: versions, tiers, codecs, limits, ceilings, caps, sandbox and cache state, the code registries | — | — | ✓ |
| `draft_governance_issue` | Draft a governance-compliant GitHub issue (HITL) | A governance-compliant GitHub issue draft, produced locally for a human to submit | — | — | — |

### `inspect_zip`

Read-only report in one call — the `zipnative inspect --json` shape:
size, entry / file / directory counts, Zip64, comment, compressed vs
uncompressed totals and ratio, per-method counts, encrypted / symlink /
data-descriptor / Zip64 / cp437 / duplicate / unsafe-name counts, date
range, `prependedData` / `multipleEocd`, a `determinism` verdict
(`epochTimestamps`, `canonicalOrder`, `utf8Flags`, `noDataDescriptors`,
`canonicalLayout`, `deterministic`) and every diagnostic. Opens
**eagerly** by default: overlapping entries, entries reaching past EOF
and Zip64 spoofing are refused with their `ZIP_*` code rather than
summarised.

| Input | Type | Default | Notes |
|---|---|---|---|
| `zipBase64` / `zipPath` | string | — | exactly one |
| `validate` | `'eager'` \| `'lazy'` | `'eager'` | `lazy` defers local-header cross-checks to first read |
| `check` | string[] ≤ 15 | — | `deterministic`, `epoch-timestamps`, `canonical-order`, `utf8-names`, `no-data-descriptor`, `canonical-layout`, `no-zip64`, `zip64`, `no-encryption`, `no-symlinks`, `safe-names`, `no-duplicates`, `no-diagnostics`, `store-only`, `deflate-only` |
| `assert` | object | — | `maxEntries`, `minEntries`, `maxUncompressedBytes`, `maxRatio` (≥ 1), `has` (≤ 100 names), `method` (`'store'` \| `'deflate'`) |
| `strict`, `limits`, `verbosity`, `fields` | | | shared |

Outputs the report plus `checks[]` (`{ check, ok, detail }`) and
`checksPassed` when `check` / `assert` was supplied.

```jsonc
{ "zipBase64": "<base64 ZIP>", "check": ["deterministic", "no-encryption", "safe-names"], "assert": { "maxEntries": 5000, "has": ["manifest.json"] }, "verbosity": "summary", "fields": ["checksPassed", "checks"] }
```

### `list_zip_entries`

Every entry (duplicates included) in directory order as a full row —
name and encoding, sizes, ratio, CRC-32, method, timestamp, decoded
flags, Unix mode, symlink, Zip64, data descriptor, host system,
offsets, named extra fields, `sanitizedPath` (`null` = the extraction
gate would refuse it), comment — without decompressing anything.

| Input | Type | Default | Notes |
|---|---|---|---|
| `zipBase64` / `zipPath` | string | — | exactly one |
| `offset` | integer ≥ 0 | `0` | skip this many matching entries |
| `limit` | integer 1–2000 | `200` | `hasMore` tells you to page |
| `filter` | object | — | `names` (≤ 1000 exact), `prefix`, `glob` (≤ 32), `includeDirectories` |
| `includeExtraData` | boolean | `false` | extra-field payloads as base64 |
| `validate` | `'lazy'` \| `'eager'` | `'lazy'` | |
| `strict`, `limits`, `verbosity`, `fields` | | | shared |

One asymmetry to know: with **no** `filter` at all, explicit directory
entries are listed; as soon as any `filter` object is given, its
`includeDirectories` default of `false` applies (the default exists for
extraction) — pass `includeDirectories: true` to keep them.

```jsonc
{ "zipBase64": "<base64 ZIP>", "filter": { "prefix": "src/", "glob": ["**/*.json"] }, "limit": 50, "fields": ["entries.name", "entries.uncompressedSize", "hasMore"] }
```

### `read_zip_entry`

One entry by `name` (exact; the last duplicate wins) or `index`
(disambiguates duplicates). `mode: 'data'` returns the decompressed
content, CRC-verified, as base64 or UTF-8 text; a `range` reads a byte
window through the chunked stream (no CRC for a window). `mode: 'raw'`
returns the compressed payload verbatim — feed it to `inflate_raw`.
`mode: 'verify'` returns `{ ok, crcMatch, sizeMatch, localHeaderMatch }`
only. Encrypted entries are refused with `ZIP_UNSUPPORTED_ENCRYPTION`.

| Input | Type | Default | Notes |
|---|---|---|---|
| `zipBase64` / `zipPath` | string | — | exactly one |
| `name` / `index` | string / integer ≥ 0 | — | exactly one |
| `mode` | `'data'` \| `'raw'` \| `'verify'` | `'data'` | |
| `encoding` | `'base64'` \| `'utf8'` | `'base64'` | mode `data`; invalid UTF-8 → `ENTRY_NOT_UTF8` |
| `verifyCrc` | boolean | `true` | mode `data` without `range` |
| `range` | `{ offset ≥ 0, length 1–16777216 }` | — | mode `data` only |
| `strict`, `limits`, `verbosity`, `fields` | | | shared |

Inline cap 16 MiB per entry (`ENTRY_TOO_LARGE`, remedy `range` or
`extract_zip` file mode).

```jsonc
{ "zipBase64": "<base64 ZIP>", "name": "META-INF/MANIFEST.MF", "encoding": "utf8" }
```

### `verify_zip`

The engine's `verifyZip` report verbatim: eager structural validation,
then every entry's CRC-32 / size / local-header agreement. **Never
fails for a problem with the archive** — a structural refusal lands in
`error: { code: 'ZIP_*', message }`, an encrypted or stream-only-codec
entry is `skipped`, an entry with no codec here is reported `failed`.
Only caller mistakes are `isError`. Verify before you trust.

| Input | Type | Default | Notes |
|---|---|---|---|
| `zipBase64` / `zipPath` | string | — | exactly one |
| `entries` | `'all'` \| `'failed'` \| `'skipped'` \| `'none'` | `'all'` | which per-entry rows to return |
| `maxEntries` | integer 1–100000 | `1000` | cap on returned rows (`entriesTruncated`); counts cover every entry |
| `limits`, `verbosity`, `fields` | | | shared |

Outputs `ok`, `error`, `entryCount`, `verifiedCount`, `failedCount`,
`skippedCount`, `entries[]`, `entriesTruncated`, `diagnostics[]`.

```jsonc
{ "zipBase64": "<base64 ZIP>", "entries": "failed", "verbosity": "summary" }
```

### `extract_zip`

Extract with every engine guard on unless you name the relaxation:
unsafe names → `ZIP_PATH_TRAVERSAL` (`rejectTraversal: false` *skips*
them — 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`), sizes 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.

| Input | Type | Default | Notes |
|---|---|---|---|
| `zipBase64` / `zipPath` | string | — | exactly one |
| `outputMode` | `'base64'` \| `'file'` | `'base64'` | inline ≤ 16 MiB per file, ≤ 32 MiB total; file mode ≤ 4 GiB |
| `outputDir` | string | — | file mode: relative directory inside the sandbox (created, never overwritten) |
| `includeData` | boolean | `true` | base64 mode: `false` = dry run (paths and sizes, nothing decompressed) |
| `filter` | object | — | as `list_zip_entries` |
| `rejectTraversal` | boolean | `true` | `false` skips unsafe names into `skipped[]` |
| `rejectSymlinks` | boolean | `true` | `false` writes the target text as data |
| `onDuplicate` | `'error'` \| `'first'` \| `'last'` | `'error'` | |
| `emptyDirectories` | boolean | `false` | file mode: create explicit empty directory entries |
| `strict`, `limits`, `verbosity`, `fields` | | | shared |

Outputs `mode`, `dryRun`, `fileCount`, `totalBytes`, `entries[]`
(`{ path, entryName, index, sizeBytes, crc32, method, dataBase64? |
filePath? }`), `skipped[]`, `outputDir`, `directoriesCreated`,
`diagnostics[]`; file mode adds up to 50 `resource_link` blocks.

```jsonc
{ "zipBase64": "<base64 ZIP>", "outputMode": "file", "outputDir": "unpacked/report", "filter": { "glob": ["docs/", "*.json"] } }
```

### `scan_zip_forward`

Walk local headers in stream order with bounded memory — the engine's
central-directory-less reader `iterateZipEntries`. The only tool that
works on a truncated archive or a stream cut mid-way, 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` reports. The stream must start at a local header —
an SFX or prepended prefix is refused with `ZIP_SIGNATURE_MISMATCH`
(route such archives to `inspect_zip`; `modify_zip mode: 'compact'`
drops the prefix). Use it for forensics; prefer the authoritative tools
whenever the whole archive is available.

| Input | Type | Default | Notes |
|---|---|---|---|
| `zipBase64` / `zipPath` | string | — | exactly one; a `zipPath` streams from disk without the 1 GiB buffered cap |
| `data` | `'none'` \| `'verify'` \| `'include'` | `'none'` | skip payloads / check CRCs / return content (inline caps) |
| `filter` | object | — | as `list_zip_entries` |
| `maxEntries` | integer ≥ 1 | `10000` | the default is clamped to `ZIPNATIVE_MCP_MAX_ENTRIES`; an **explicit** value above the ceiling is refused with `LIMIT_CEILING_EXCEEDED` |
| `tolerateTruncation` | boolean | `false` | `true`: a truncated stream ends the scan with `stoppedAt: 'error'` instead of failing |
| `strict`, `limits`, `verbosity`, `fields` | | | shared |

Outputs `trust: 'local-headers-only'`, `entryCount`, `keptCount`,
`entries[]`, `stoppedAt` (`'central-directory'` | `'eof'` |
`'max-entries'` | `'error'`), `truncated`, `inputBytes`,
`bytesDelivered`, `maxEntries`, `error`, `diagnostics[]`.

```jsonc
{ "zipBase64": "<base64 of a truncated download>", "tolerateTruncation": true, "verbosity": "summary" }
```

### `sanitize_entry_paths`

The engine's single traversal gate `sanitizeEntryPath()` over a list of
names: the safe `/`-separated relative form each maps to, or `null` with
the rule that fired (`empty`, `nul`, `absolute`, `drive`, `unc`,
`traversal`, `ads`, `device-name`, `no-segments`). No archive needed —
use it before an external filesystem sink.

| Input | Type | Default | Notes |
|---|---|---|---|
| `names` | string[] 1–10000 | — | each ≤ 4096 characters, as stored in an archive |
| `verbosity`, `fields` | | | projection |

```jsonc
{ "names": ["docs/readme.md", "../../etc/passwd", "C:\\Windows\\win.ini", "aux.txt", "a/./b//c.txt"] }
```

### `create_zip`

Write a ZIP from inline entries (`dataBase64` | `text` | `sourcePath` in
the sandbox, or `directory: true`). Reproducible on one runtime by
default — canonical order, DOS-epoch timestamps, UTF-8 names —
and identical on every runtime under `compression.deterministic: true`
(`summary.deflateTier: 'pure-pinned'`). `order: 'insertion'` keeps call
order for EPUB / JAR; `stream: true` on a `sourcePath` feeds the file
through `addStream` (bounded memory, > 4 GiB refused); `parallel`
compresses with the worker pool, byte-identical. Zip64 is emitted
exactly when a field overflows. Every archive it writes is ISO/IEC
21320-1 conformant.

| Input | Type | Default | Notes |
|---|---|---|---|
| `entries` | array 0–100000 | — | required; per entry `name`, exactly one payload, and optional `compression`, `date`, `comment`, `unixMode` xor `externalAttributes`, `extraFields` (≤ 16), `stream` |
| `order` | `'canonical'` \| `'insertion'` | `'canonical'` | |
| `defaultDate` | string | DOS epoch | ISO-8601 instant or `'now'` (`ZIP_TIMESTAMP_NOT_PINNED` diagnostic) |
| `compression` | object | deflate 6 | `method` (`'store'` \| `'deflate'`), `level` 0–9, `deterministic` |
| `comment` | string ≤ 65535 | — | |
| `chunkSize` | integer 1024–16777216 | 64 KiB | file-mode streaming chunk; never changes the bytes |
| `parallel` | object | — | `workers` 0–64 (≤ `ZIPNATIVE_MCP_WORKERS`; omitted ⇒ the engine default `max(1, min(cores − 1, 8))`, bounded by the ceiling), `minWorkerJobSize` (default 32 KiB — smaller entries compress inline), `jobTimeout` 1000–600000 ms (default 60000, then inline fallback) |
| `includeSha256` | boolean | `false` | `summary.sha256` — the determinism proof |
| `outputMode` / `outputPath` | | `'base64'` | file mode: a relative path ending in a container extension |
| `strict`, `limits` | | | shared |

Outputs the archive as an embedded `resource` (≤ 50 MiB inline) or a
`resource_link`, plus `summary` (`entryCount`, `fileCount`,
`directoryCount`, `streamedCount`, `uncompressedBytes`, `order`,
`deterministic`, `deflateTier`, `parallel`, `sha256`) and
`diagnostics[]`.

```jsonc
{
  "entries": [
    { "name": "mimetype", "text": "application/epub+zip", "compression": { "method": "store" } },
    { "name": "META-INF/container.xml", "text": "<container/>" },
    { "name": "OEBPS/", "directory": true }
  ],
  "order": "insertion",
  "compression": { "deterministic": true },
  "includeSha256": true
}
```

### `modify_zip`

Edit an existing archive without recompressing anything: `operations`
applied in order. `mode: 'append'` = `save()` — the original bytes stay
verbatim and edits are appended, **but** removed or replaced payloads
remain in the file (data remanence; a `ZIP_DEAD_BYTES_RATIO` diagnostic
fires past 50 % dead bytes; 7-Zip is known to read the stale payload).
`mode: 'compact'` = `saveCompact()` — canonical rewrite, still no
recompression, removed data truly gone, SFX prefix dropped. Archives
with duplicate names are refused.

| Input | Type | Default | Notes |
|---|---|---|---|
| `zipBase64` / `zipPath` | string | — | exactly one |
| `operations` | array 1–1000 | — | `{ op: 'add' \| 'replace', name, payload… }` (an `add` may instead carry `directory: true` for an explicit directory entry — `replace` cannot), `{ op: 'remove', name }`, `{ op: 'rename', from, to }`, `{ op: 'setComment', comment }` |
| `mode` | `'append'` \| `'compact'` | `'append'` | |
| `compression` / `defaultDate` | | engine defaults | for new payloads |
| `includeSha256` | boolean | `false` | |
| `outputMode` / `outputPath` | | `'base64'` | |
| `strict`, `limits` | | | shared |

Outputs the archive plus `summary` (`saveMode`, `operationsApplied`,
`entryCountBefore`, `entryCountAfter`, `inputBytes`, `outputBytes`,
`grewByBytes`, `noOp`, `sha256`).

```jsonc
{ "zipBase64": "<base64 ZIP>", "operations": [{ "op": "remove", "name": ".env" }, { "op": "rename", "from": "config.json", "to": "config/app.json" }], "mode": "compact" }
```

### `compute_crc32`

The ZIP checksum (IEEE CRC-32) of inline bytes, text or a sandbox file
(streamed, ≤ 1 GiB). `seed` continues a running CRC; `expect` compares
against a known value — cross-check an entry's `crc32` from
`list_zip_entries` against a file on disk.

| Input | Type | Default | Notes |
|---|---|---|---|
| `dataBase64` / `text` / `sourcePath` | string | — | exactly one |
| `seed` | integer 0–4294967295 | `0` | |
| `expect` | string | — | 1–8 hex digits, optional `0x` |

Outputs `crc32`, `hex`, `byteLength`, `seed`, `matches`.

```jsonc
{ "text": "hello world", "expect": "0d4a1185" }
```

### `inflate_raw`

Decompress a raw DEFLATE stream — for instance the payload from
`read_zip_entry mode: 'raw'` — through the engine's resumable inflater
under a **mandatory** `maxOutput` bound (`ZIP_INFLATE_OUTPUT_OVERFLOW`
past it). `method: 'store'` is a bounded pass-through; only methods 0
and 8 exist in this server.

| Input | Type | Default | Notes |
|---|---|---|---|
| `dataBase64` / `sourcePath` | string | — | exactly one |
| `maxOutput` | integer ≥ 1 | — | **required**; inline results capped at 50 MiB, file results at 4 GiB |
| `method` | string | `'deflate'` | `'deflate'`, `'store'` or a numeric id |
| `allowTrailing` | boolean | `false` | accept bytes after the stream silently (always reported as `leftover`) |
| `outputMode` / `outputPath` | | `'base64'` | file mode: `.bin .dat .txt .json .xml .md .csv .html .zip .tar .raw` |

```jsonc
{ "dataBase64": "<raw deflate payload>", "maxOutput": 1048576 }
```

### `describe_engine`

Offline preflight, no archive needed: server / engine / tool-API
versions, the deflate tier in use and the pinned deterministic tier,
runtime codecs and worker threads, the codec registry, the engine's
default `limits`, the operator ceilings, every server cap, whether the
sandbox and the cache are enabled, the 39 frozen `ZIP_*` error codes
and 11 diagnostic codes, and the engine exports this server
deliberately does not expose (with why). `network` is always `'none'`.
Never cached.

```jsonc
{ "fields": ["defaultLimits", "ceilings", "caps", "sandbox"] }
```

### `draft_governance_issue`

Draft a governance-compliant GitHub issue **locally** for a human to
review and submit — for `zipnative` or `zipnative-mcp`. The server
never contacts GitHub (it has no network code path); it returns the
draft Markdown and a machine-readable `compliance` report. Present
both, then stop: the human submits under their own identity.

| Input | Type | Default | Notes |
|---|---|---|---|
| `title` | string 8–160 | — | required |
| `summary` | string ≥ 16 | — | required |
| `issueType` | `'bug'` \| `'feature'` \| `'security'` \| `'docs'` \| `'performance'` | — | required |
| `targetRepo` | string | `'zipnative-mcp'` | |
| `reproduction` | `{ command, result }` | — | required |
| `expectedBehavior` | string ≥ 4 | — | required |
| `actualBehavior` | string | the reproduction result | |
| `affectedPackages` | string[] ≤ 16 | `['zipnative-mcp']` | |
| `duplicateSearchPerformed` | boolean | — | required, must be `true` |
| `outputMode` / `outputPath` | `'inline'` \| `'file'` | `'inline'` | file mode writes a relative `.md` inside the sandbox |

A draft that proposes a runtime dependency, omits a reproduction or sets
`duplicateSearchPerformed: false` is `GOVERNANCE_VIOLATION`.

## Prompts

Seven read-only prompts (no arguments) that teach the assistant the
workflows and the rules:

| Prompt | Teaches |
|---|---|
| `secure_extraction` | verify → inspect with checks → extract, guards on; when to relax which one |
| `reproducible_archive` | byte-identical archives with `create_zip` — `deterministic`, pinned dates, `includeSha256` as proof |
| `incremental_update` | `modify_zip` append vs compact, data remanence, the 7-Zip differential |
| `forensic_scan` | when and how to use `scan_zip_forward`, what a local-headers-only result can and cannot tell you |
| `verify_before_trust` | the discipline: never extract what you have not verified |
| `governance_contract` | the human-in-the-loop rules the server enforces |
| `draft_issue_workflow` | how to build a compliant issue draft with `draft_governance_issue` |

## Resources

With `ZIPNATIVE_MCP_OUTPUT_DIR` set, every regular file in the sandbox
is a resource under the `zipnative://output/{+path}` template:
`resources/list` walks the sandbox (≤ 1000 files, depth ≤ 8),
`resources/templates/list` advertises the one template (named
`sandbox-file`, declared `mimeType: application/zip`), and
`resources/read` returns `{ uri, mimeType, blob }` up to 50 MiB. Without
a sandbox both lists are empty. The MIME type is chosen by extension
from a fixed table of 32 — the nineteen container extensions (`.zip`,
`.jar` / `.war` / `.ear` as `application/java-archive`, the Office and
OpenDocument types, `.epub`, `.apk`, `.xpi`, `.crx`, `.kmz`, and
`.vsix` / `.nupkg` / `.whl` / `.ipa` as plain `application/zip`) plus
`.md .txt .json .xml .html .css .js .png .jpg .jpeg .gif .svg .pdf` —
and `application/octet-stream` for anything else.

## Token-frugal reads

Every read tool accepts `verbosity: 'summary'` (only the scalar summary
fields) and `fields: [...]` (≤ 16 dot paths). When a `fields` entry
matches nothing the result carries `_meta.unmatchedFields` and
`_meta.availableFields` so the assistant can correct itself in one
turn. Archive-producing tools deliver the bytes **once**, as an
embedded `resource` with a `data:` URI (never duplicated into
`structuredContent`); file mode returns a `resource_link` instead.

## Error model

An execution failure is `{ content: [{ type: 'text', text: '<tool>
failed [CODE]: message' }], isError: true, _meta: { error: { code,
message, data? } } }`. Sixteen wrapper codes — `VALIDATION_ERROR`,
`INPUT_TOO_LARGE`, `INPUT_NOT_FOUND`, `ENTRY_TOO_LARGE`,
`ENTRY_NOT_UTF8`, `OUTPUT_TOO_LARGE`, `OUTPUT_EXISTS`, `IO_ERROR`,
`SECURITY_VIOLATION`, `MISSING_OUTPUT_PATH`, `INVALID_PATH`,
`INVALID_EXTENSION`, `LIMIT_CEILING_EXCEEDED`, `UNKNOWN_RESOURCE`,
`GOVERNANCE_VIOLATION`, `ENGINE_ERROR` — sit next to the engine's 39
frozen `ZIP_*` codes, passed through verbatim (`ZIP_PATH_TRAVERSAL`,
`ZIP_LIMIT_EXCEEDED`, `ZIP_UNSUPPORTED_ENCRYPTION`, …), and
`ZIP_STRICT_DIAGNOSTIC` under `strict: true`. `data.remedy` names the
way out where one exists (a `range`, a tool to call first, a ceiling).

## Security model

- **Every archive is untrusted input**, and the engine's guards are on
  by default: zip-slip and device names (CWE-22 / 67), bombs
  (CWE-400 / 409, enforced *while* inflating), symlinks (CWE-59),
  overlapping entries (CWE-405, no opt-out), parser-differential
  smuggling (CWE-436, the central directory is authoritative), Zip64
  spoofing (CWE-1288, no opt-out), duplicate names (CWE-694).
- **The sandbox is the one filesystem boundary** for reads and writes;
  paths must be relative, real paths must stay inside, files are opened
  exclusively, a failed write removes the partial file.
- **Inputs are validated at the boundary** by strict JSON Schemas and
  Zod — unknown or misspelt keys are `VALIDATION_ERROR`.
- **No encryption, ever**: encrypted entries are detected, skipped by
  `verify_zip`, refused by the read paths. There is no password input —
  see [Case 5](use-cases.html#case-5--encrypt-first-then-archive) for
  how confidentiality is delegated to the document layer.
- **No network, ever**: no telemetry, no GitHub, no update check, no URL
  from any argument, no operator-configurable endpoint of any kind.
  `describe_engine` reports `network: 'none'`.

## Further reading

- [MCP playground](../playgrounds/mcp.html) — the thirteen tools with
  their real payloads, most of them executable in your browser against
  the published engine.
- [Choosing your surface](choose.html) · [CLI guide](cli.html) — the
  same engine for scripts and CI.
- [Security model](security.html) · [Errors and error codes](errors.html)
  · [Use cases](use-cases.html).
- The full README, AGENTS.md (with the complete error reference),
  `server.json` and thirty executable JSON examples live in the
  [zipnative-mcp repository](https://github.com/Nizoka/zipnative-mcp).
