---
summary: "ntfy://{topic} resource timestamps become ISO 8601 and body-truncated to match ntfy_fetch_messages; ntfy_publish_message's message limit is enforced by byte length; five error-classification and message-ordering fixes; framework ^0.11.0."
breaking: true
security: false
---

# 2.2.0 — 2026-07-29

## Fixed

- **`ntfy_publish_message` 413 misclassification** — an oversize payload now reads the canonical `data.status` off the thrown error, so HTTP 413 reaches `payload_too_large` instead of bubbling unclassified (413 maps to `InvalidRequest`, a code the 4xx gate never let through). ([#15](https://github.com/cyanheads/ntfy-mcp-server/issues/15))
- **`ntfy_publish_message` invalid `attach` URL** — no longer misreported as `payload_too_large`; a new `invalid_attachment` reason states that shortening the message will not help and asks for an absolute URL to an already-hosted file. ([#15](https://github.com/cyanheads/ntfy-mcp-server/issues/15))
- **`message` byte-length enforcement** — `ntfy_publish_message`'s `message` field is now rejected locally when it exceeds 4096 bytes, even if it's under 4096 characters (multibyte bodies cost 2–4 bytes per character). ([#15](https://github.com/cyanheads/ntfy-mcp-server/issues/15))
- **Upstream error detail dropped from error messages** — `NtfyService` now parses ntfy's JSON error body (`{ error, link }`) and folds it into the thrown `McpError` message at the one point every tool and the resource shares, so e.g. `invalid delay parameter: unable to parse delay` reaches `content[]` and survives a `ctx.fail()` re-throw instead of collapsing to `ntfy returned HTTP 400 Bad Request.`. ([#16](https://github.com/cyanheads/ntfy-mcp-server/issues/16))
- **`ntfy_publish_message` `time` output ambiguity** — the description and `format()` label now distinguish acceptance time from scheduled delivery time: `Delivers: <timestamp>` when `delay` was set (`scheduled: true`), `Time: <timestamp>` otherwise. ([#19](https://github.com/cyanheads/ntfy-mcp-server/issues/19))
- **`ntfy_fetch_messages` truncation kept the oldest messages, not the latest** — the cached-message slice now takes the tail (`slice(-limit)`) instead of the head, matching the "latest N" the description already advertised; the kept window stays chronological (oldest-first). ([#20](https://github.com/cyanheads/ntfy-mcp-server/issues/20))
- **`ntfy://{topic}` resource timestamps and truncation** — `time`/`expires` are now ISO 8601 strings (were raw Unix seconds), and message bodies over ~500 characters truncate with the dropped count reported as `messageTruncated`, matching `ntfy_fetch_messages`. The same ordering fix as above applies (`slice(-20)`). Normalization moved into `src/services/ntfy/message-shape.ts` so the tool and resource share one `shapeMessage()` implementation. ([#8](https://github.com/cyanheads/ntfy-mcp-server/issues/8))

## Changed

- **`.codex-plugin/plugin.json` `interface.capabilities`** — corrected from `["Read"]` to `["Read", "Write"]`; the server sends and manages notifications, not just reads them. ([#14](https://github.com/cyanheads/ntfy-mcp-server/issues/14))
- **`bunfig.toml` supply-chain guard** — `install.minimumReleaseAge` blocks package versions published less than 3 days ago (excluding `@cyanheads/mcp-ts-core`, adopted same-day), and `install.security.scanner` runs the Socket scanner on every install.
- **`Dockerfile`** — pinned `oven/bun:1.3.14` (was floating `1.3`/`1.3-slim`), added `--mount=type=cache` for Bun's install cache across all three `bun install`/`bun add` steps, and the build stage now runs `--ignore-scripts`.
- **`.gitignore`** — `data/` anchored to `/data/` so the pattern no longer shadows a same-named directory under `src/`.
- **`.github/FUNDING.yml`** — trimmed to the two funding platforms actually in use (`github`, `buy_me_a_coffee`); removed the unused placeholder entries.
- **`devcheck`'s Packaging check** now also validates `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json` (description non-empty, unscoped display identity, scoped install arg).
- **`package.json` `author`** — `cyanheads <casey@caseyjhand.com> (...)` → `Casey Hand <casey@caseyjhand.com> (https://caseyjhand.com)`.
- **`manifest.json`** — `license` field added (`Apache-2.0`).
- **README** — install badges added (Claude Desktop `.mcpb`, Cursor, VS Code); `ntfy://{topic}` resource row documents the shared ISO 8601 / truncation shape.

## Added

- **`.github/SECURITY.md`** — vulnerability-reporting policy.
- **`.gitattributes`** — normalizes line endings to LF, marks binary asset types, and collapses generated files (`bun.lock`, `CHANGELOG.md`, `docs/tree.md`) in PR diffs.
- **`scripts/check-dependency-specifiers.ts`** + devcheck `Dependency Specifiers` gate — rejects floating specifiers (`latest`, `*`, pre-release dist-tags) in `package.json` and `bun.lock`'s workspace map.
- **`MCP_LOG_RATE_LIMIT_THRESHOLD` / `MCP_LOG_RATE_LIMIT_WINDOW_MS`** documented in `.env.example`.

## Dependencies

- `@cyanheads/mcp-ts-core` `^0.10.6` → `^0.11.0` — across the range: devcheck gained the plugin-manifest packaging check and the floating-specifier gate, `.mcpb` bundling strips platform-specific native bindings in addition to agent docs, `withRetry` honors `Retry-After`, and 0.11.0 moves the TypeScript toolchain to TS 7 (this project holds `typescript` at `^6.0.3`, allowlisted in `devcheck.config.json`'s outdated gate).
- `@biomejs/biome` (dev) `^2.4.16` → `^2.5.5`
- `@types/node` (dev) `^25.9.3` → `^26.1.1`
- `ignore` (dev) `^7.0.5` → `^7.0.6`
- `tsc-alias` (dev) `^1.8.17` → `^1.9.1`
- `vitest` (dev) `^4.1.8` → `^4.1.10`
- `@socketsecurity/bun-security-scanner` (dev) added at `^1.1.2` — installed by `bunfig.toml`'s `[install.security]` scanner config.
