|
| 1 | +--- |
| 2 | +summary: "Elicitation and cancellation now work over Streamable HTTP, SSRF blocking moves from pattern matching to full non-global IP range checks, parsers bound their input and stop echoing library diagnostics into client errors, and a hermetic lane verifies the published tarball." |
| 3 | +breaking: false |
| 4 | +security: true |
| 5 | +agent-notes: | |
| 6 | + `ctx.elicit` is now defined over Streamable HTTP for clients that advertise the |
| 7 | + capability. Servers whose handlers assumed the no-elicitation branch under HTTP |
| 8 | + — including any tool description that says elicitation is stdio-only — should |
| 9 | + re-read their fallback path. |
| 10 | +
|
| 11 | + Parser failures no longer carry the underlying library's message, stack, or a |
| 12 | + content sample in `McpError.data`. Consumers matching on those strings must |
| 13 | + match on `data.reason` instead (`json_parse_failed`, `yaml_parse_failed`, |
| 14 | + `xml_parse_failed`, `frontmatter_parse_failed`, `html_extract_failed`, |
| 15 | + `pdf_load_failed`, `pdf_text_extract_failed`, and siblings). |
| 16 | +
|
| 17 | + Text parsers now reject input over 1 MiB and binary parsers over 25 MiB. A |
| 18 | + server that legitimately handles larger documents passes `{ maxBytes }` — the |
| 19 | + final argument on `yamlParser.parse` / `xmlParser.parse` / `jsonParser.parse` / |
| 20 | + `frontmatterParser.parse` and the PDF methods, or the `maxBytes` option on |
| 21 | + `htmlExtractor.extractArticle` and `pdfParser.embedImage`. |
| 22 | +--- |
| 23 | + |
| 24 | +# 0.11.2 — 2026-08-10 |
| 25 | + |
| 26 | +## Added |
| 27 | + |
| 28 | +- **`normalizeLogLevelAlias(value)`** (`/config`) — resolves a raw `LOG_LEVEL` string through the same alias table `ConfigSchema` applies (`warn` → `warning`, `trace` → `debug`, `fatal`/`silent` → `emerg`, `err` → `error`, `information` → `info`). |
| 29 | +- **`SupabaseClientHandle`** (root) — the dependency-free default for `CoreServices['supabase']`. `createApp<SupabaseClient<MyDatabase>>({ … })` opts a consumer into the concrete client type without making Supabase's generic graph part of every consumer's declarations. |
| 30 | +- **`ParserInputBudgetOptions`, `DEFAULT_TEXT_PARSER_MAX_BYTES`, `DEFAULT_BINARY_PARSER_MAX_BYTES`** (`/utils`) — the input budget every parser now applies, and the knob to raise or lower it per call. |
| 31 | +- **`SqliteMirrorStoreLimits`** (`/mirror`) — per-store ceilings on `query` and `getByIds`, supplied as `limits` on `sqliteMirrorStore(spec)`. |
| 32 | +- **`expected` on `ToolContractSuccessCase`** (`/testing/vitest`) — a structured-output subset asserted with `toMatchObject`, so a success case pins behavior rather than schema shape alone. |
| 33 | +- **Package verification lane** — `bun run test:package` packs the tarball, installs it into an isolated production-only consumer, and exercises every public subpath through runtime imports, declaration typechecking, and the published `init` bin. `scripts/public-api-contract.ts` records the exact named exports each subpath promises; both the source barrels and the installed tarball are checked against it, and `bun run scripts/public-api-contract-update.ts` re-derives the manifest after an intentional barrel change. |
| 34 | +- **Deterministic-order and assertion-required lanes** — `bun run test:order` reruns the suites shuffled under a fixed seed, and every vitest config sets `expect.requireAssertions` (the typecheck project opts out, since `expectTypeOf` is not a runtime assertion). |
| 35 | +- **`.github/workflows/ci.yml`** — an eight-job matrix running static checks, Bun, real Node, Worker, integration, package, async-leak, and shuffled-order lanes on every push and pull request. |
| 36 | + |
| 37 | +## Changed |
| 38 | + |
| 39 | +- **Text parsers reject input over 1 MiB and binary parsers over 25 MiB.** `yamlParser.parse`, `xmlParser.parse`, `jsonParser.parse`, and `frontmatterParser.parse` take an optional budget as their final argument; `htmlExtractor.extractArticle`, `pdfParser.loadDocument`, `embedImage`, `mergePdfs`, `splitPdf`, and `extractText` take `maxBytes` alongside their existing options. PDF text extraction also caps decoded images at 4096×4096 pixels, which pdf.js leaves unbounded by default. |
| 40 | +- **`fetchWithTimeout` rejects non-`http:`/`https:` URLs** before any network work, independent of `rejectPrivateIPs`. |
| 41 | +- **`sqliteMirrorStore` bounds every query** — at most 32 filters, 500 bound values, `limit` 1–1000, `offset` 0–1,000,000, and 500 `ids` per `getByIds` call, each overridable through `limits`. |
| 42 | +- **`StorageService` caps `getMany`, `setMany`, and `deleteMany` at 10,000 keys** and rejects a non-array key list or a non-`Map` entry set before dispatching to a provider. |
| 43 | +- **`xmlParser` pins `maxNestedTags: 100`**, fast-xml-parser's own default, so an upstream change cannot silently raise the depth bound. |
| 44 | +- **Resource URIs are stripped of credentials, query, and fragment** before reaching logs, spans, or metrics; the protocol response still carries the original URI, and a `resourceHasQuery` flag records whether one was present. Worker request URLs log the same way. |
| 45 | +- **`git-wrapup` skill 1.9** — commit bodies are one or two lines and never carry a closing keyword; the changelog `summary` is written last, derived from the body. |
| 46 | + |
| 47 | +## Fixed |
| 48 | + |
| 49 | +- **`ctx.elicit` works over Streamable HTTP** ([#312](https://github.com/cyanheads/mcp-ts-core/issues/312)). Client capabilities live on the SDK `Server` instance, and HTTP builds a fresh one per request (GHSA-345p-7cg4-v4c7), so the capability check never passed and every handler took its no-elicitation branch. The session store now captures the capabilities declared on a successful `initialize` and restores them onto each per-request server; elicitation requests route back through `RequestHandlerExtra.sendRequest` so they reach the caller on the open response stream, and an accepted payload is validated against the schema its request advertised. |
| 50 | +- **`notifications/cancelled` aborts the handler over HTTP** ([#311](https://github.com/cyanheads/mcp-ts-core/issues/311)). The notification arrives on its own POST with its own `Server`, so the SDK's abort-controller lookup found nothing. Tool and resource invocations now register with the session store, the transport mirrors an incoming cancellation into that registry, and `ctx.signal` merges the SDK signal with the session-scoped one. Terminating a session, sweeping it as stale, or destroying the store aborts everything still in flight. |
| 51 | +- **`MCP_HTTP_MAX_BODY_BYTES` is stream-enforced** ([#244](https://github.com/cyanheads/mcp-ts-core/issues/244)). A body with no `Content-Length` was buffered whole and measured afterward, so the cap bounded nothing on that path. The middleware now reads at most one chunk past the limit, cancels the stream, and answers `413`; a within-budget body is seeded into Hono's cache so the SDK still parses it. |
| 52 | +- **`rejectPrivateIPs` blocks the IPv6 unspecified address** ([#314](https://github.com/cyanheads/mcp-ts-core/issues/314)). `::` fell through every pattern and was treated as public. The regex tables are replaced with range checks: IPv4 now covers the protocol assignments in `192.0.0.0/24`, `192.0.2.0/24`, `192.88.99.0/24`, `198.18.0.0/15`, `198.51.100.0/24`, `203.0.113.0/24`, and everything at or above `224.0.0.0` alongside the existing private space; IPv6 admits only globally routable `2000::/3`, minus Teredo, 6to4, benchmarking, ORCHID, and the two documentation ranges. |
| 53 | +- **A missing PDF peer dependency surfaces as `ConfigurationError`** ([#306](https://github.com/cyanheads/mcp-ts-core/issues/306)). The lazy import sat inside each method's catch-all `try`, so the remediation hint was re-wrapped as an `InternalError`. An `McpError` raised inside a PDF operation now propagates with its own code intact. |
| 54 | +- **Error bodies are read to the diagnostic cap, not in full.** `fetchWithTimeout` and `httpErrorFromResponse` buffered the entire non-2xx body before slicing it to 500 bytes. Both now stop at the cap and cancel the stream, trimming the capture to a UTF-8 boundary so a split code point cannot add a replacement marker past the budget. |
| 55 | +- **`WWW-Authenticate` points at the configured public URL.** The `resource_metadata` parameter was derived from the inbound request origin, which behind a proxy is the internal container address; `MCP_PUBLIC_URL` now wins when set. |
| 56 | +- **A failed shutdown step no longer strands the ones after it.** Transport stop, task-manager cleanup, rate-limiter disposal, scheduler teardown, and GC-pressure teardown each run independently, with the first failure rethrown after all of them have been attempted. A transport start that fails now rolls back everything startup allocated — process listeners, task and storage resources, telemetry — before the error reaches the caller. |
| 57 | +- **The per-request HTTP server and transport close exactly once.** The error path closed only the transport, and a request that produced no response closed neither. |
| 58 | +- **The Worker entry point resolves `LOG_LEVEL` aliases and rethrows fetch failures.** It screened the raw binding against the canonical level list, so `warn` was discarded and silently downgraded to `info`; a handler error was logged and then swallowed, hiding the failure from the runtime. |
| 59 | +- **`InMemoryProvider` tenant bookkeeping.** A read against an unknown tenant allocated a map for it, `setMany` could commit past `maxEntries` because expired keys were counted as overwrites, a list cursor issued to another tenant was accepted whenever the requested tenant was empty, and a non-integer `maxEntries` was accepted at construction. |
| 60 | +- **`DuckdbProvider` validates `rowLimit` and `preview`** before opening a connection, rejecting a non-integer, a negative, or a `rowLimit` above the configured default. |
| 61 | +- **A cancelled `DataCanvas.acquire` no longer leaks a canvas.** The `AcquireOptions` signal was ignored; it is now checked before and after provider initialization, and a canvas created while cancellation was pending is destroyed. |
| 62 | +- **DuckDB exports reject symlinked destinations.** Lexical confinement to the export root did not stop an in-sandbox symlink pointing outside it; every existing segment of the destination chain is now rejected if it is a link. The check/open race remains open ([#334](https://github.com/cyanheads/mcp-ts-core/issues/334)). |
| 63 | +- **Mirror syncs are serialized and honor an abort mid-run.** Two concurrent `runSync` calls interleaved writes into one store; the runner only observed `ctx.signal` between pages, so an abort during the final count or state write still stamped the run complete. `sqliteMirrorStore.open()` no longer caches a rejected initialization, and `close()` is safe to call while an open is in flight. |
| 64 | +- **Parser errors no longer echo library diagnostics.** JSON, YAML, XML, frontmatter, HTML, and PDF failures carried the underlying error's message, stack, and a 200-character content sample into client-facing `McpError.data`. Each now returns a fixed message plus `data.reason`, with the original error preserved on `cause` for the internal pipeline. |
| 65 | +- **`fuzzTool`/`fuzzResource` leak-check the whole client-visible payload.** Only `error.message` was scanned, so a stack trace or internal path carried in `error.data` went unreported. |
| 66 | +- **Integration suites fail on a stale or missing build.** They previously skipped themselves when `dist/` was absent and passed silently against an out-of-date one; a setup file now compares build-output mtimes against every TypeScript build input. |
| 67 | + |
| 68 | +## Security |
| 69 | + |
| 70 | +- **SSRF blocking covers the full non-global IP surface** — see the `rejectPrivateIPs` entry above. `::` was reachable, and the IPv6 side previously matched only link-local and ULA prefixes. |
| 71 | +- **Parser input budgets** bound the work one request can schedule inside a parser dependency, and the PDF image cap bounds raster allocation during text extraction. |
| 72 | +- **Export-path symlink rejection** closes the in-sandbox redirect for DuckDB `COPY … TO`. |
| 73 | +- **Error and log redaction** — parser diagnostics no longer reach clients, and credentials, query strings, and fragments are stripped from resource URIs and Worker request URLs before they reach logs or telemetry. |
| 74 | +- **Batch and query ceilings** on `StorageService` and `sqliteMirrorStore` bound the fan-out a single client-shaped request can create. |
| 75 | + |
| 76 | +## Dependencies |
| 77 | + |
| 78 | +Runtime: |
| 79 | + |
| 80 | +- `@hono/node-server` `^2.0.12` → `^2.1.0` |
| 81 | +- `hono` `^4.12.32` → `^4.13.1` |
| 82 | +- `jose` `^6.2.5` → `^6.2.8` |
| 83 | + |
| 84 | +Dev: |
| 85 | + |
| 86 | +- `@duckdb/node-api` `^1.5.5-r.2` → `^1.5.5-r.3` |
| 87 | +- `@supabase/supabase-js` `^2.111.0` → `^2.112.2` |
| 88 | +- `better-sqlite3` `^13.0.2` → `^13.0.3` |
| 89 | +- `execa` `^10.0.0` → `^10.0.1` |
| 90 | +- `js-yaml` `^5.2.2` → `^5.2.3` |
| 91 | +- `repomix` `^1.17.0` → `^1.18.0` |
| 92 | + |
| 93 | +Overrides: |
| 94 | + |
| 95 | +- `fast-uri` `^3.1.4` → `^3.1.5` ([#325](https://github.com/cyanheads/mcp-ts-core/issues/325) — `^3.1.4` is satisfied at 3.1.4, still inside the `>=3.0.0 <3.1.5` range of [GHSA-7p8r-x3mc-p8w7](https://github.com/advisories/GHSA-7p8r-x3mc-p8w7) and [GHSA-v2hh-gcrm-f6hx](https://github.com/advisories/GHSA-v2hh-gcrm-f6hx)) |
| 96 | +- `ip-address` `^10.3.1` added ([#325](https://github.com/cyanheads/mcp-ts-core/issues/325) — clears [GHSA-mwp4-54f8-5fhr](https://github.com/advisories/GHSA-mwp4-54f8-5fhr) at `<=10.3.0`, reached through `@modelcontextprotocol/sdk › express-rate-limit`) |
| 97 | +- `postcss` `^8.5.22` → `^8.5.23` |
| 98 | +- `@hono/node-server` `^2.0.11` override removed |
0 commit comments