---
summary: "Dataframe surface (Tier 3, opt-in) — spilled find_* rows auto-register as DuckDB-backed dataframes; agents run SELECT SQL across them via brapi_dataframe_query/describe/drop. Genotype-call cap promoted to operator policy."
breaking: false
---

# 0.4.10 — 2026-05-03

A SQL/analytical workspace lands on top of the existing dataset surface. When `find_*` tools spill to `DatasetStore`, the rows now also auto-register as `ds_<datasetId>` dataframes on the framework's DuckDB-backed `DataCanvas` — agents query them with SELECT SQL via `brapi_dataframe_query`, describe schemas via `brapi_dataframe_describe`, and drop them via `brapi_dataframe_drop`. The whole surface is Tier 3: optional peer dep `@duckdb/node-api`, gated behind `CANVAS_PROVIDER_TYPE=duckdb` and `BRAPI_CANVAS_ENABLED=true`. Bare installs and Cloudflare Workers are unaffected. Separately, `brapi_find_genotype_calls` retires its per-call `maxCalls` input in favor of a deployment-side `BRAPI_GENOTYPE_CALLS_MAX_PULL` operator policy.

## Added

- **`brapi_dataframe_query`** — run SELECT SQL across in-memory dataframes. Read-only enforcement uses the framework's three-layer SQL gate (single statement → SELECT only → plan-walk allowlist) plus a server-side deny-list for `read_json*` / `read_parquet*` / `iceberg_scan` / `delta_scan` table functions that bypass the canvas. `registerAs` materializes the result as a new dataframe; the response carries a bounded `preview` slice plus the full `rowCount`. Per-tenant default workspace cached in `ctx.state` — agents never see the underlying canvasId. Typed `sql_rejected` reason preserves the granular gate code on `data.gateReason`.
- **`brapi_dataframe_describe`** — list dataframes (or one by name) with column schema, row count, and originating-dataset provenance for `ds_*` entries (source tool, baseUrl, original query, createdAt, expiresAt). User-derived dataframes from `registerAs` show structural info only.
- **`brapi_dataframe_drop`** — release a dataframe by name. Idempotent — `dropped: false` rather than failing for unknown names. The underlying `DatasetStore` dataset is untouched; `brapi_manage_dataset mode=delete` drops both.
- **`CanvasBridge` service** (`src/services/canvas-bridge/`) — owns the three concerns the framework leaves to the consumer: per-tenant default canvas resolution (cached in `ctx.state`, refreshed on stale-id `NotFound`), SQL-safe table naming for UUID-shaped dataset IDs (`ds_<datasetId_with_underscores>`), and originating-dataset provenance tracking. `isEnabled()` is the single gate every dataframe tool checks. `composeSignal()` wraps `ctx.signal` with a wall-clock cap using the manual-AbortController pattern (avoids `AbortSignal.timeout()` realm-mismatch issues in Bun stdio).
- **`DatasetStore` ↔ canvas integration** — `create()` auto-registers spilled rows on the canvas when the bridge is enabled; the resulting dataframe name surfaces on `DatasetMetadata.dataframe`. `delete()` mirrors the drop. Both directions are best-effort — failure logs a warning but never breaks the dataset op. Dataset handles, `brapi_manage_dataset` summary/list output, and `find_*` formatters all surface `dataframe: <name>` when present so agents can pivot directly into SQL.
- **5 new env vars**:
  - `BRAPI_GENOTYPE_CALLS_MAX_PULL` (default 100,000, max 500,000) — operator policy ceiling on rows pulled in a single `brapi_find_genotype_calls` invocation.
  - `CANVAS_PROVIDER_TYPE` (framework, default `none`) — set to `duckdb` to enable the canvas service. Fails closed on Cloudflare Workers.
  - `BRAPI_CANVAS_ENABLED` (server, default `true`) — server-side gate for the `brapi_dataframe_*` surface. Effective only when the framework canvas is active.
  - `BRAPI_CANVAS_MAX_ROWS` (default 10,000) — hard cap on rows materialized into a `brapi_dataframe_query` response. Larger result sets must use `registerAs`.
  - `BRAPI_CANVAS_QUERY_TIMEOUT_MS` (default 30,000) — per-query wall-clock timeout layered on top of `ctx.signal`.
- **Optional peer dependency `@duckdb/node-api ^1.5.0`** — listed under `peerDependenciesMeta.optional: true` so bare installs stay light. Linux/macOS/Windows × x64 plus Linux/macOS arm64 supported (Windows arm64 unsupported by DuckDB upstream).
- **`api-canvas` and `code-simplifier` skills** synced from the framework upgrade. `api-config`, `api-workers`, `add-tool`, `design-mcp-server`, `security-pass`, and the `report-issue-*` pair gained canvas-aware sections.

## Changed

- **`brapi_find_genotype_calls` cap is deployment-side now** — the per-call `maxCalls` input is removed (Zod silently strips it; no wire break for callers still passing it). The cap reads from `BRAPI_GENOTYPE_CALLS_MAX_PULL` instead, so the upstream-protection budget is set by the operator, not the agent. Truncation warning, output `truncated` doc, and the `raw_search` routing nudge updated to point at the new env var. Recovery hint when truncated: narrow the filters and re-pull, or query the spilled dataframe.
- **Framework bump** — `@cyanheads/mcp-ts-core` `^0.8.8` → `^0.8.10`. Picks up the `DataCanvas` primitive, `core.canvas` wiring on `CoreServices`, and the canvas-related env-var schemas.
- **`src/index.ts`** registers the dataframe tools conditionally on `serverConfig.canvasEnabled`, mirroring the existing `enableWrites` gate. Tools are omitted from `tools/list` when the gate is off so agents don't see capabilities they can't use.
