---
summary: "SQL gate blocks read_json*/read_parquet* file disclosure (#100); appendValue routes TIMESTAMP/DATE/BLOB to typed appenders (#102); CANVAS_IDENTIFIER_REGEX + SQL_GATE_REASONS exported (#104); RequestContext widened to accept handler Context (#105)."
breaking: false
---

# 0.8.12 — 2026-05-02

Canvas hardening release. Closes a real file-disclosure bypass in the SQL gate, fixes silent data corruption in `appendValue`, promotes two consumer-drift surfaces to public exports, and widens `RequestContext` so consumers can pass the handler `Context` to canvas methods directly. Includes a comment-and-JSDoc scrub across `src/services/canvas/` and a few small DuckDB provider efficiency wins.

## Added

- **`assertNoDeniedFunctions`, `DENIED_TABLE_FUNCTIONS`, `collectPlanViolations`** — new exports from `@cyanheads/mcp-ts-core/canvas`. Layer 1 of the SQL gate now runs a pre-parse text scan with comment-stripping and string-literal stripping; Layer 4 (`assertPlanReadOnly`) gains a denied-function rescan over plan metadata strings (`extra_info`, `function`, `function_name`, `table_function`, `source`). Deny-list covers `read_json*`, `read_parquet*`, `parquet_scan`, `parquet_metadata`, plus extras (`read_csv*`, `read_text`, `read_blob`, `glob`, `iceberg_scan`, `delta_scan`, `postgres_scan`, `mysql_scan`, `sqlite_scan`). ([#100](https://github.com/cyanheads/mcp-ts-core/issues/100))
- **`CANVAS_IDENTIFIER_REGEX`** — `/^[A-Za-z_][A-Za-z0-9_]{0,62}$/` promoted from internal `IDENTIFIER_REGEX` to a public export so consumers surfacing the same shape constraint at the JSON Schema layer (`z.string().regex(CANVAS_IDENTIFIER_REGEX)` on `registerAs` inputs) don't redeclare it. ([#104](https://github.com/cyanheads/mcp-ts-core/issues/104))
- **`SQL_GATE_REASONS` const + `SqlGateReason` type** — single source of truth for the gate's eight reason codes (`multi_statement`, `non_select_statement`, `plan_operator_not_allowed`, `denied_function`, `denied_function_in_plan`, `identifier_empty`, `identifier_shape`, `identifier_reserved`). All eight throw sites in `sqlGate.ts` now source `data.reason` from the const — drift between the gate's emitted strings and consumer contract translations is compile-time impossible. ([#104](https://github.com/cyanheads/mcp-ts-core/issues/104))
- **Canvas-token capability rule** — new core rule in `CLAUDE.md` / `AGENTS.md` documenting that `canvasId` and any UUID-keyed primitive (datasets, derived row stores) are access tokens, not tenant-scoped state. Spells out the shared-`tenantId='default'` collision pattern that bites stateless / `MCP_AUTH_MODE=none` deployments when a tool caches a "default canvas" in `ctx.state`.
- **`canvas.absoluteCapMs >= ttlMs` config refine** — `ConfigSchema` now rejects configs where `CANVAS_ABSOLUTE_CAP_MS` is smaller than `CANVAS_TTL_MS` (otherwise canvases die at the absolute cap before the sliding TTL ever extends them). Error reports against `absoluteCapMs`.
- **Tests** — `tests/unit/services/canvas/appendValueCoerce.test.ts` (31 unit tests covering each typed-appender converter's accepted shapes, rejection paths, and `reason: 'invalid_value_for_type'` payload) and `tests/unit/services/canvas/toBigInt.test.ts`. New deny-list and plan-rescan coverage in `sqlGate.test.ts`. New round-trip TIMESTAMP / DATE / BLOB coverage in `tests/smoke/canvas-duckdb.test.ts`.

## Changed

- **`RequestContext` optional fields widened** from `?: T` to `?: T | undefined` (`auth`, `tenantId`, `traceId`, `spanId`). Under `exactOptionalPropertyTypes` the framework `Context` (which carries strict-optional fields with `| undefined`) is now structurally assignable to `RequestContext` — consumers can pass `ctx` directly to canvas methods (`DataCanvas.acquire`, `CanvasInstance.*`, `IDataCanvasProvider.*`) without an `asCanvasContext` slice. Pure widening; no existing call site needed an update. ([#105](https://github.com/cyanheads/mcp-ts-core/issues/105))
- **`DuckdbProvider` cleanup** — dropped the 28-line hand-maintained `STATEMENT_TYPE_BY_ID` map in favor of `duck.StatementType[id]` reverse lookup (DuckDB's exported numeric enum already provides this). Dropped the `as unknown as { statementType: number }` cast (proper typing exists on `DuckDBPreparedStatement`). Dropped the redundant `private duck` field — `lazyImport` already caches.
- **`DuckdbProvider.describe()` parallelized** — per-table fan-out via `Promise.all`, and within `describeOne` the column-info read and `COUNT(*)` are now concurrent (`Promise.all([colReader, countReader])`).
- **`DuckdbProvider.shutdown()` parallelized** — switched the `destroyCanvas` loop from sequential `await` to `Promise.allSettled` so a slow / failing canvas can't stall shutdown of healthy peers.
- **JSDoc and comment scrub across `src/services/canvas/`** — removed all `(issue #N)` cross-references, "Refinement N in issue #97" prose, and historical "previous `String(value)` fallback" anecdotes. File-overview headers tightened. JSDoc on `requestContext.ts` rewritten to be domain-neutral while preserving the structural-assignability rationale.

## Fixed

- **SQL gate read_json*/read_parquet* file-disclosure bypass** — previously, `read_json`, `read_json_auto`, `read_json_objects`, `read_ndjson`, `read_parquet`, and `parquet_scan` lowered into a generic scan operator that the plan-walk allowlist accepted, letting any caller able to influence canvas SQL read arbitrary parseable-JSON files the server process could reach (`read_json('/etc/passwd')` opened the file; valid JSON returned its contents, invalid JSON leaked the first byte via the parse error). Now caught at Layer 1 by the new text-level deny-list (which strips comments and string literals before scanning) and again at Layer 4 by the plan-metadata rescan. ([#100](https://github.com/cyanheads/mcp-ts-core/issues/100))
- **`appendValue` silent corruption for TIMESTAMP / DATE / BLOB** — the `case 'TIMESTAMP': case 'DATE': case 'BLOB':` branch fell through to `appender.appendVarchar(String(value))`. `String(new Date())` produced locale-formatted strings DuckDB couldn't cleanly parse; `String(new Uint8Array([1, 2, 3]))` produced `"1,2,3"` — total binary corruption, no error, no warning. Now routed through typed appenders: `toTimestampMicros` (accepts `Date`, `bigint`, ms-epoch `number`, ISO 8601 string) → `appendTimestamp`; `toDateDays` (accepts `Date`, ms-epoch `number`, ISO 8601 string; floors fractional days) → `appendDate`; `toUint8Array` (accepts `Uint8Array`, Node `Buffer`, `ArrayBuffer`, any `ArrayBufferView`) → `appendBlob`. Anything else fails fast with `validationError({ reason: 'invalid_value_for_type', column, type })`. ([#102](https://github.com/cyanheads/mcp-ts-core/issues/102))
- **`DuckdbProvider.export()` temp-file leak on COPY failure** — the stream-target branch called `tempFilePathFor()` to allocate a sandboxed path, then `conn.run('COPY ... TO ...')` to write it, then `pipeFileToStream()` to drain and unlink. If `COPY` threw, `pipeFileToStream` was never invoked and the temp file was orphaned in `CANVAS_EXPORT_PATH`. `COPY` now runs inside a try/catch that unlinks on failure before re-throwing.
