@cyanheads/brapi-mcp-server 0.4.14 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +23 -19
- package/README.md +40 -26
- package/changelog/0.5.x/0.5.0.md +59 -0
- package/changelog/0.5.x/0.5.1.md +13 -0
- package/dist/config/builtin-aliases.d.ts +1 -1
- package/dist/config/builtin-aliases.js +1 -1
- package/dist/config/server-config.d.ts +3 -3
- package/dist/config/server-config.d.ts.map +1 -1
- package/dist/config/server-config.js +13 -13
- package/dist/config/server-config.js.map +1 -1
- package/dist/index.js +62 -15
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/prompts/definitions/brapi-eda-study.prompt.js +1 -1
- package/dist/mcp-server/prompts/definitions/brapi-eda-study.prompt.js.map +1 -1
- package/dist/mcp-server/prompts/definitions/brapi-meta-analysis.prompt.js +3 -3
- package/dist/mcp-server/prompts/definitions/brapi-meta-analysis.prompt.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-dataframe-describe.tool.d.ts +8 -11
- package/dist/mcp-server/tools/definitions/brapi-dataframe-describe.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-dataframe-describe.tool.js +15 -25
- package/dist/mcp-server/tools/definitions/brapi-dataframe-describe.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-dataframe-drop.tool.d.ts +6 -10
- package/dist/mcp-server/tools/definitions/brapi-dataframe-drop.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-dataframe-drop.tool.js +6 -16
- package/dist/mcp-server/tools/definitions/brapi-dataframe-drop.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.d.ts +69 -0
- package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.js +233 -0
- package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/brapi-dataframe-query.tool.d.ts +19 -11
- package/dist/mcp-server/tools/definitions/brapi-dataframe-query.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-dataframe-query.tool.js +80 -28
- package/dist/mcp-server/tools/definitions/brapi-dataframe-query.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.d.ts +4 -6
- package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.js +28 -36
- package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-germplasm.tool.d.ts +3 -5
- package/dist/mcp-server/tools/definitions/brapi-find-germplasm.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-germplasm.tool.js +20 -19
- package/dist/mcp-server/tools/definitions/brapi-find-germplasm.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-images.tool.d.ts +4 -6
- package/dist/mcp-server/tools/definitions/brapi-find-images.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-images.tool.js +17 -16
- package/dist/mcp-server/tools/definitions/brapi-find-images.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-locations.tool.d.ts +3 -5
- package/dist/mcp-server/tools/definitions/brapi-find-locations.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-locations.tool.js +16 -15
- package/dist/mcp-server/tools/definitions/brapi-find-locations.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-observations.tool.d.ts +4 -6
- package/dist/mcp-server/tools/definitions/brapi-find-observations.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-observations.tool.js +18 -17
- package/dist/mcp-server/tools/definitions/brapi-find-observations.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-studies.tool.d.ts +4 -6
- package/dist/mcp-server/tools/definitions/brapi-find-studies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-studies.tool.js +23 -21
- package/dist/mcp-server/tools/definitions/brapi-find-studies.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-variables.tool.d.ts +2 -4
- package/dist/mcp-server/tools/definitions/brapi-find-variables.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-variables.tool.js +18 -17
- package/dist/mcp-server/tools/definitions/brapi-find-variables.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-variants.tool.d.ts +3 -5
- package/dist/mcp-server/tools/definitions/brapi-find-variants.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-find-variants.tool.js +18 -15
- package/dist/mcp-server/tools/definitions/brapi-find-variants.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.d.ts +17 -2
- package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.js +51 -7
- package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.d.ts +16 -2
- package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.js +51 -5
- package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/index.d.ts +141 -165
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/index.js +28 -17
- package/dist/mcp-server/tools/definitions/index.js.map +1 -1
- package/dist/mcp-server/tools/shared/find-helpers.d.ts +76 -45
- package/dist/mcp-server/tools/shared/find-helpers.d.ts.map +1 -1
- package/dist/mcp-server/tools/shared/find-helpers.js +159 -72
- package/dist/mcp-server/tools/shared/find-helpers.js.map +1 -1
- package/dist/mcp-server/tools/shared/raw-routing-hints.d.ts.map +1 -1
- package/dist/mcp-server/tools/shared/raw-routing-hints.js +6 -6
- package/dist/mcp-server/tools/shared/raw-routing-hints.js.map +1 -1
- package/dist/services/canvas-bridge/canvas-bridge.d.ts +69 -44
- package/dist/services/canvas-bridge/canvas-bridge.d.ts.map +1 -1
- package/dist/services/canvas-bridge/canvas-bridge.js +143 -112
- package/dist/services/canvas-bridge/canvas-bridge.js.map +1 -1
- package/dist/services/canvas-bridge/index.d.ts +1 -1
- package/dist/services/canvas-bridge/index.d.ts.map +1 -1
- package/dist/services/canvas-bridge/index.js +1 -1
- package/dist/services/canvas-bridge/index.js.map +1 -1
- package/dist/services/canvas-bridge/types.d.ts +11 -12
- package/dist/services/canvas-bridge/types.d.ts.map +1 -1
- package/dist/services/canvas-bridge/types.js +4 -4
- package/package.json +3 -11
- package/server.json +3 -3
- package/dist/mcp-server/resources/definitions/brapi-dataset.resource.d.ts +0 -13
- package/dist/mcp-server/resources/definitions/brapi-dataset.resource.d.ts.map +0 -1
- package/dist/mcp-server/resources/definitions/brapi-dataset.resource.js +0 -26
- package/dist/mcp-server/resources/definitions/brapi-dataset.resource.js.map +0 -1
- package/dist/mcp-server/tools/definitions/brapi-manage-dataset.tool.d.ts +0 -89
- package/dist/mcp-server/tools/definitions/brapi-manage-dataset.tool.d.ts.map +0 -1
- package/dist/mcp-server/tools/definitions/brapi-manage-dataset.tool.js +0 -306
- package/dist/mcp-server/tools/definitions/brapi-manage-dataset.tool.js.map +0 -1
- package/dist/services/dataset-store/dataset-store.d.ts +0 -44
- package/dist/services/dataset-store/dataset-store.d.ts.map +0 -1
- package/dist/services/dataset-store/dataset-store.js +0 -208
- package/dist/services/dataset-store/dataset-store.js.map +0 -1
- package/dist/services/dataset-store/index.d.ts +0 -8
- package/dist/services/dataset-store/index.d.ts.map +0 -1
- package/dist/services/dataset-store/index.js +0 -7
- package/dist/services/dataset-store/index.js.map +0 -1
- package/dist/services/dataset-store/types.d.ts +0 -72
- package/dist/services/dataset-store/types.d.ts.map +0 -1
- package/dist/services/dataset-store/types.js +0 -8
- package/dist/services/dataset-store/types.js.map +0 -1
package/CLAUDE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Agent Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** brapi-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
4
|
+
**Version:** 0.5.1
|
|
5
5
|
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
|
|
6
6
|
|
|
7
7
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
@@ -79,15 +79,15 @@ export const brapiConnect = tool('brapi_connect', {
|
|
|
79
79
|
});
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
### Tool — find with
|
|
82
|
+
### Tool — find with dataframe spillover
|
|
83
83
|
|
|
84
|
-
`find_*` tools share a pattern: pull one page capped at `loadLimit`, compute distributions across the returned rows, and if the upstream total exceeds `loadLimit`
|
|
84
|
+
`find_*` tools share a pattern: pull one page capped at `loadLimit`, compute distributions across the returned rows, and if the upstream total exceeds `loadLimit` materialize the full union as a canvas dataframe and return a handle. Spilled rows live in DuckDB only — there is no parallel JSON store. Canvas is mandatory: startup fails closed when `core.canvas` is undefined.
|
|
85
85
|
|
|
86
86
|
```ts
|
|
87
87
|
// src/mcp-server/tools/definitions/brapi-find-germplasm.tool.ts (abbreviated)
|
|
88
88
|
export const brapiFindGermplasm = tool('brapi_find_germplasm', {
|
|
89
89
|
description:
|
|
90
|
-
'Find germplasm by name, synonym, accession, PUI, crop, or free-text.
|
|
90
|
+
'Find germplasm by name, synonym, accession, PUI, crop, or free-text. Spills to a canvas dataframe when the upstream total exceeds loadLimit — query with brapi_dataframe_query (SQL).',
|
|
91
91
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
92
92
|
input: z.object({
|
|
93
93
|
alias: AliasInput,
|
|
@@ -101,15 +101,16 @@ export const brapiFindGermplasm = tool('brapi_find_germplasm', {
|
|
|
101
101
|
async handler(input, ctx) {
|
|
102
102
|
const connection = await getServerRegistry().get(ctx, input.alias ?? DEFAULT_ALIAS);
|
|
103
103
|
await getCapabilityRegistry().ensure(connection.baseUrl, { service: 'germplasm', method: 'GET' }, ctx);
|
|
104
|
+
const bridge = getCanvasBridge();
|
|
104
105
|
|
|
105
106
|
const filters = mergeFilters(/* named + extraFilters */, warnings);
|
|
106
107
|
const firstPage = await loadInitialPage(client, connection, '/germplasm', filters, loadLimit, ctx);
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
return { /* results + distributions + refinementHint +
|
|
109
|
+
const { fullRows, dataframe } = await maybeSpill({
|
|
110
|
+
firstPage, client, connection, bridge,
|
|
111
|
+
path: '/germplasm', filters, source: 'find_germplasm', loadLimit, ctx,
|
|
112
|
+
});
|
|
113
|
+
return { /* results + distributions + refinementHint + dataframe? */ };
|
|
113
114
|
},
|
|
114
115
|
format: (result) => [{ type: 'text', text: renderFindResult(result) }],
|
|
115
116
|
});
|
|
@@ -124,7 +125,7 @@ import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
|
|
|
124
125
|
|
|
125
126
|
const ServerConfigSchema = z.object({
|
|
126
127
|
defaultBaseUrl: z.string().url().optional(),
|
|
127
|
-
loadLimit: z.coerce.number().int().positive().default(
|
|
128
|
+
loadLimit: z.coerce.number().int().positive().default(1_000),
|
|
128
129
|
maxConcurrentRequests: z.coerce.number().int().positive().default(4),
|
|
129
130
|
retryMaxAttempts: z.coerce.number().int().min(0).default(3),
|
|
130
131
|
datasetTtlSeconds: z.coerce.number().int().positive().default(86_400),
|
|
@@ -159,7 +160,7 @@ Handlers receive a unified `ctx` object. Currently used surface:
|
|
|
159
160
|
| Property | Description |
|
|
160
161
|
|:---------|:------------|
|
|
161
162
|
| `ctx.log` | Request-scoped logger — `.debug()`, `.info()`, `.notice()`, `.warning()`, `.error()`. Auto-correlates requestId, traceId, tenantId. |
|
|
162
|
-
| `ctx.state` | Tenant-scoped KV — used by `ServerRegistry` (connection aliases), `
|
|
163
|
+
| `ctx.state` | Tenant-scoped KV — used by `ServerRegistry` (connection aliases), `CanvasBridge` (per-tenant default canvasId + per-table provenance), and `CapabilityRegistry` (cached profiles). Spilled `find_*` rows live on the canvas (DuckDB), not in `ctx.state`. |
|
|
163
164
|
| `ctx.signal` | `AbortSignal` — threaded into every BrAPI HTTP call so client-side cancellation aborts the upstream request. |
|
|
164
165
|
| `ctx.requestId` | Unique request ID — auto-attached to every `ctx.log` entry. |
|
|
165
166
|
| `ctx.tenantId` | Tenant ID from JWT or `'default'` for stdio — scopes all `ctx.state` reads/writes. |
|
|
@@ -172,7 +173,7 @@ Handlers receive a unified `ctx` object. Currently used surface:
|
|
|
172
173
|
|
|
173
174
|
Handlers throw — the framework catches, classifies, and formats.
|
|
174
175
|
|
|
175
|
-
**Default for new tools: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` to receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. The `recovery` field is required descriptive metadata (≥ 5 words, lint-validated); to surface it on the wire, spread `...ctx.recoveryFor('reason')` into `data` or pass an explicit `{ recovery: { hint: '...' } }` when runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring. Live across the BrAPI surface today: `brapi_connect`, `brapi_describe_filters`, `brapi_find_genotype_calls`, `brapi_get_germplasm`, `brapi_get_image`, `brapi_get_study`, `
|
|
176
|
+
**Default for new tools: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()` to receive a typed `ctx.fail(reason, …)` keyed by the declared reason union. TypeScript catches `ctx.fail('typo')` at compile time, `data.reason` is auto-populated for observability, and the linter enforces conformance against the handler body. The `recovery` field is required descriptive metadata (≥ 5 words, lint-validated); to surface it on the wire, spread `...ctx.recoveryFor('reason')` into `data` or pass an explicit `{ recovery: { hint: '...' } }` when runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`) bubble freely and don't need declaring. Live across the BrAPI surface today: `brapi_connect`, `brapi_dataframe_query`, `brapi_describe_filters`, `brapi_find_genotype_calls`, `brapi_get_germplasm`, `brapi_get_image`, `brapi_get_study`, `brapi_raw_get`, `brapi_raw_search`, `brapi_submit_observations`, plus the `brapi://study/{studyDbId}`, `brapi://germplasm/{germplasmDbId}`, and `brapi://filters/{endpoint}` resources.
|
|
176
177
|
|
|
177
178
|
```ts
|
|
178
179
|
errors: [
|
|
@@ -215,7 +216,7 @@ Available factories include `notFound`, `validationError`, `forbidden`, `unautho
|
|
|
215
216
|
|
|
216
217
|
```text
|
|
217
218
|
src/
|
|
218
|
-
index.ts # createApp() entry point — registers
|
|
219
|
+
index.ts # createApp() entry point — registers 21 tools, 5 resources, 2 prompts; inits 7 services
|
|
219
220
|
config/
|
|
220
221
|
server-config.ts # BRAPI_* env vars (Zod schema, lazy-parsed)
|
|
221
222
|
alias-credentials.ts # Per-alias env-var resolution (BRAPI_<ALIAS>_*) for brapi_connect
|
|
@@ -223,8 +224,8 @@ src/
|
|
|
223
224
|
brapi-client/ # HTTP client — retry, concurrency cap, async-search poll, private-IP guard, binary fetch, POST/PUT
|
|
224
225
|
brapi-dialect/ # Per-server filter / payload adapters (spec, cassavabase) — translates plural→singular, drops searchText, declares known-dead POST /search routes; envelope surfaces id + source + disabled-search nouns
|
|
225
226
|
brapi-filters/ # Static v2.1 filter catalog
|
|
227
|
+
canvas-bridge/ # Per-tenant default-canvas resolver, df_<uuid> table generator, provenance store
|
|
226
228
|
capability-registry/ # Per-connection /serverinfo cache + call guard
|
|
227
|
-
dataset-store/ # Tenant-scoped handles for spilled find_* results
|
|
228
229
|
ontology-resolver/ # Free-text → ontology-term matcher for variables
|
|
229
230
|
reference-data-cache/ # Programs / trials / locations / crops lookup cache
|
|
230
231
|
server-registry/ # Alias → live connection map with auth resolution
|
|
@@ -245,15 +246,18 @@ src/
|
|
|
245
246
|
brapi-get-image.tool.ts # Fetch image bytes inline (imagecontent → imageURL fallback)
|
|
246
247
|
brapi-find-locations.tool.ts # find_* — locations, optional client-side bbox filter
|
|
247
248
|
brapi-find-variants.tool.ts # find_* — variants, 1-based inclusive/exclusive genomic region
|
|
248
|
-
brapi-find-genotype-calls.tool.ts # Async-search genotype calls with maxCalls cap + spillover
|
|
249
|
-
brapi-
|
|
249
|
+
brapi-find-genotype-calls.tool.ts # Async-search genotype calls with maxCalls cap + dataframe spillover
|
|
250
|
+
brapi-dataframe-describe.tool.ts # List / describe canvas dataframes with columns, row counts, provenance
|
|
251
|
+
brapi-dataframe-query.tool.ts # Run SQL across canvas dataframes (SELECT only); typed columns response
|
|
252
|
+
brapi-dataframe-drop.tool.ts # Drop a dataframe by name (opt-in via BRAPI_CANVAS_DROP_ENABLED)
|
|
253
|
+
brapi-dataframe-export.tool.ts # Write CSV/Parquet/JSON to BRAPI_EXPORT_DIR (opt-in, stdio-only)
|
|
250
254
|
brapi-submit-observations.tool.ts # Two-phase observation write — preview / apply (POST + PUT) with elicit gate
|
|
251
255
|
brapi-raw-get.tool.ts # Last-resort GET passthrough with routing nudge
|
|
252
256
|
brapi-raw-search.tool.ts # Last-resort POST /search passthrough with async polling
|
|
253
257
|
shared/
|
|
254
258
|
connect-auth-schema.ts # Tagged-union auth input
|
|
255
259
|
orientation-envelope.ts # Shared envelope builder + formatter
|
|
256
|
-
find-helpers.ts # Alias / loadLimit / extraFilters fragments, mergeFilters, maybeSpill,
|
|
260
|
+
find-helpers.ts # Alias / loadLimit / extraFilters fragments, mergeFilters, maybeSpill, DataframeHandleSchema
|
|
257
261
|
raw-routing-hints.ts # Routing nudges emitted by raw_get / raw_search when a curated tool exists
|
|
258
262
|
resources/
|
|
259
263
|
definitions/
|
|
@@ -261,7 +265,6 @@ src/
|
|
|
261
265
|
brapi-calls.resource.ts # brapi://calls — raw capability profile
|
|
262
266
|
brapi-study.resource.ts # brapi://study/{studyDbId} — single study with FKs
|
|
263
267
|
brapi-germplasm.resource.ts # brapi://germplasm/{germplasmDbId} — single germplasm with attributes + parents
|
|
264
|
-
brapi-dataset.resource.ts # brapi://dataset/{datasetId} — dataset metadata + provenance
|
|
265
268
|
brapi-filters.resource.ts # brapi://filters/{endpoint} — filter catalog
|
|
266
269
|
prompts/
|
|
267
270
|
definitions/
|
|
@@ -308,6 +311,7 @@ Available skills:
|
|
|
308
311
|
| `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
|
|
309
312
|
| `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
|
|
310
313
|
| `api-auth` | Auth modes, scopes, JWT/OAuth |
|
|
314
|
+
| `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
|
|
311
315
|
| `api-config` | AppConfig, parseConfig, env vars |
|
|
312
316
|
| `api-context` | Context interface, logger, state, progress |
|
|
313
317
|
| `api-errors` | McpError, JsonRpcErrorCode, error patterns |
|
|
@@ -387,7 +391,7 @@ import { getMyService } from '@/services/my-domain/my-service.js';
|
|
|
387
391
|
- [ ] BrAPI tool: gates the call with `CapabilityRegistry.ensure(...)` — never fires against an endpoint the server didn't advertise
|
|
388
392
|
- [ ] BrAPI tool: raw / domain / output schemas reviewed against real upstream sparsity (most `/germplasm` and `/studies` fields are optional in the wild)
|
|
389
393
|
- [ ] BrAPI tool: normalization and `format()` preserve uncertainty — never fabricate missing IDs, names, or counts
|
|
390
|
-
- [ ] BrAPI tool with
|
|
394
|
+
- [ ] BrAPI tool with dataframe spillover: rows beyond `loadLimit` materialize as a `df_<uuid>` canvas table via `CanvasBridge.registerDataframe`, handle surfaces in `result.dataframe`, `hasMore` set correctly
|
|
391
395
|
- [ ] Tests include at least one sparse upstream payload (fields omitted) alongside the happy path
|
|
392
396
|
- [ ] Registered in the `tools` array of `createApp()` in `src/index.ts`
|
|
393
397
|
- [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
|
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1>@cyanheads/brapi-mcp-server</h1>
|
|
3
3
|
<p><b>BrAPI v2.1 MCP server — studies, germplasm, observations, genotypes, images, and pedigrees across Breedbase, T3, Sweetpotatobase, and any BrAPI-compliant server.</b>
|
|
4
|
-
<div>22 Tools •
|
|
4
|
+
<div>22 Tools • 5 Resources • 2 Prompts</div>
|
|
5
5
|
</p>
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](https://www.npmjs.com/package/@cyanheads/brapi-mcp-server) [](https://www.npmjs.com/package/@cyanheads/brapi-mcp-server) [](./CHANGELOG.md) [](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) [](https://modelcontextprotocol.io/)
|
|
11
11
|
|
|
12
12
|
[](./LICENSE) [](https://www.typescriptlang.org/) [](https://bun.sh/) [](./CHANGELOG.md)
|
|
13
13
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
## Tools
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
22 tools grouped by shape — connection tools bootstrap a session, `find_*` tools return a summarized page plus distributions and spill overflow rows into a canvas dataframe, `get_*` tools fetch a single record with companion counts, plus pedigree walking, an embedded SQL workspace over spilled rows (DuckDB-backed), file export for human handoff, an additive write surface for observations, and raw passthrough escape hatches.
|
|
21
21
|
|
|
22
22
|
### Orient
|
|
23
23
|
|
|
@@ -31,32 +31,27 @@ Twenty-two tools grouped by shape — connection tools bootstrap a session, `fin
|
|
|
31
31
|
|
|
32
32
|
| Tool | Description |
|
|
33
33
|
|:-----|:------------|
|
|
34
|
-
| `brapi_find_studies` | Find studies by crop / trial type / season / location / program. Distributions +
|
|
34
|
+
| `brapi_find_studies` | Find studies by crop / trial type / season / location / program. Distributions + dataframe spillover. |
|
|
35
35
|
| `brapi_get_study` | Fetch a study with program / trial / location FKs resolved and companion counts (observations, units, variables). |
|
|
36
|
-
| `brapi_find_germplasm` | Find germplasm by name, synonym, accession, PUI, crop, or free-text. Distributions +
|
|
36
|
+
| `brapi_find_germplasm` | Find germplasm by name, synonym, accession, PUI, crop, or free-text. Distributions + dataframe spillover. |
|
|
37
37
|
| `brapi_get_germplasm` | Fetch a germplasm with attributes, direct parents, and companion counts (studies, parents, descendants). |
|
|
38
38
|
| `brapi_walk_pedigree` | BFS-walk ancestry / descendancy as a deduplicated DAG with cycle detection, depth limits, and traversal stats. |
|
|
39
39
|
| `brapi_find_variables` | Find observation variables by name / class / ontology / free-text; ranked client-side via `OntologyResolver` when `text` is supplied. |
|
|
40
|
-
| `brapi_find_observations` | Pull observation records by study / germplasm / variable / season / unit / timestamp.
|
|
40
|
+
| `brapi_find_observations` | Pull observation records by study / germplasm / variable / season / unit / timestamp. Dataframe spillover. |
|
|
41
41
|
| `brapi_find_images` | Filter image metadata by unit / study / ontology / MIME type. Bytes via `brapi_get_image`. |
|
|
42
42
|
| `brapi_get_image` | Fetch image bytes for up to 5 imageDbIds inline as `type: image` blocks. Prefers `/imagecontent`, falls back to `imageURL`. |
|
|
43
43
|
| `brapi_find_locations` | Find research stations by country / type / abbreviation, with optional client-side bbox filter. |
|
|
44
44
|
| `brapi_find_variants` | Find variant records by variant set, reference, or genomic region (1-based inclusive / exclusive). |
|
|
45
45
|
| `brapi_find_genotype_calls` | Pull genotype calls via async-search polling. Upstream pull bounded by `BRAPI_GENOTYPE_CALLS_MAX_PULL` (default 100k, max 500k). |
|
|
46
46
|
|
|
47
|
-
###
|
|
47
|
+
### Analyze
|
|
48
48
|
|
|
49
49
|
| Tool | Description |
|
|
50
50
|
|:-----|:------------|
|
|
51
|
-
| `
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
| Tool | Description |
|
|
56
|
-
|:-----|:------------|
|
|
57
|
-
| `brapi_dataframe_query` | SELECT SQL across in-memory dataframes (DuckDB-backed). Spilled `find_*` datasets auto-register as `ds_<datasetId>`. Read-only — multi-statement, non-SELECT, file-reads, and exports rejected. |
|
|
58
|
-
| `brapi_dataframe_describe` | List dataframes with column schema, row counts, and dataset provenance. |
|
|
59
|
-
| `brapi_dataframe_drop` | Drop a dataframe by name. Idempotent. Underlying dataset is unaffected. |
|
|
51
|
+
| `brapi_dataframe_describe` | Start here after a spillover. Lists dataframes (or describes one) with column schema, row counts, and originating-source provenance. |
|
|
52
|
+
| `brapi_dataframe_query` | SELECT SQL across in-memory dataframes (DuckDB-backed). Spilled `find_*` rows auto-register as `df_<uuid>`. Read-only — multi-statement, non-SELECT, file-reads, and exports rejected. Returns typed columns (`{ name, type }[]`). |
|
|
53
|
+
| `brapi_dataframe_drop` | _Opt-in via `BRAPI_CANVAS_DROP_ENABLED=true`._ Drop a dataframe by name. Idempotent. Dataframes also expire via TTL when left unmanaged. |
|
|
54
|
+
| `brapi_dataframe_export` | _Opt-in via `BRAPI_EXPORT_DIR=<path>`, stdio-only._ Export a dataframe to disk (CSV / Parquet / JSON) under the configured directory and return the absolute path for the human to open. Optional `columns` projection or `sql` filter materializes a derived table for the export, dropped after. |
|
|
60
55
|
|
|
61
56
|
### Write (opt-in: `BRAPI_ENABLE_WRITES=true`)
|
|
62
57
|
|
|
@@ -85,7 +80,6 @@ URI-addressable mirrors of the curated tool surface for clients that prefer reso
|
|
|
85
80
|
| `brapi://calls` | Raw capability profile |
|
|
86
81
|
| `brapi://study/{studyDbId}` | `brapi_get_study` |
|
|
87
82
|
| `brapi://germplasm/{germplasmDbId}` | `brapi_get_germplasm` |
|
|
88
|
-
| `brapi://dataset/{datasetId}` | Dataset metadata + provenance |
|
|
89
83
|
| `brapi://filters/{endpoint}` | `brapi_describe_filters` |
|
|
90
84
|
|
|
91
85
|
---
|
|
@@ -106,20 +100,38 @@ Multi-step BrAPI workflow templates — pure user-message generators, no side ef
|
|
|
106
100
|
- **Multi-server session** — `ServerRegistry` maps aliases to live BrAPI connections; one session can span Breedbase, T3, and Sweetpotatobase in parallel.
|
|
107
101
|
- **Capability-aware calls** — `CapabilityRegistry` caches `/serverinfo` per connection and guards every tool call against unsupported endpoints. Falls back to `/calls` when `/serverinfo` is sparse.
|
|
108
102
|
- **Built-in known-server registry** — `bti-cassava`, `bti-sweetpotato`, `bti-breedbase-demo`, `t3-wheat`, `t3-oat`, `t3-barley` resolve out-of-the-box without env vars; orientation envelope carries CC-BY attribution.
|
|
109
|
-
- **
|
|
110
|
-
- **
|
|
103
|
+
- **Dataframe spillover** — `find_*` tools cap in-context rows at `loadLimit` and materialize larger unions (up to 50k rows / 50 pages) as DuckDB-backed `df_<uuid>` canvas dataframes. Discover with `brapi_dataframe_describe`, query with `brapi_dataframe_query` (SQL paging via `LIMIT/OFFSET`, projection, aggregation). Read-only enforcement at the SQL gate; per-tenant workspace.
|
|
104
|
+
- **DuckDB required** — `@duckdb/node-api` is a regular dependency; startup fails closed when the framework canvas is unavailable. Not supported on Cloudflare Workers (no native binary in that runtime).
|
|
111
105
|
- **Async-search transparency** — `brapi_find_genotype_calls` and `brapi_raw_search` handle the `POST /search/{noun}` → `GET /search/{noun}/{id}` 202-retry pattern automatically.
|
|
112
106
|
- **Pedigree DAG walks** — `brapi_walk_pedigree` BFS-traverses ancestry / descendancy with cycle detection (BrAPI only exposes one generation per call).
|
|
113
107
|
- **Image content** — `brapi_get_image` fetches bytes inline as MCP `type: image` blocks, preferring `/images/{id}/imagecontent` with `imageURL` fallback.
|
|
114
108
|
- **Free-text variable ranking** — `OntologyResolver` scores variables against a query (PUI / name / synonym / trait-class) so `find_variables text:"..."` returns ranked candidates even without `/ontologies`.
|
|
115
109
|
- **Auth variants in one schema** — tagged-union covers `none` / `bearer` / `api_key` / `sgn` (session-token exchange) / `oauth2` (client-credentials).
|
|
116
110
|
- **Typed error contracts** — every declared failure mode carries a stable `data.reason`, an HTTP-style `code`, and a `recovery.hint` so clients can route deterministically.
|
|
117
|
-
- **Compatibility matrix** — live server probes tracked in [docs/compatibility.md](./docs/compatibility.md).
|
|
118
111
|
|
|
119
112
|
Built on [`@cyanheads/mcp-ts-core`](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) — declarative definitions, unified error handling, pluggable auth (`none` / `jwt` / `oauth`), swappable storage, structured logging with optional OTel, STDIO + Streamable HTTP transports.
|
|
120
113
|
|
|
121
114
|
---
|
|
122
115
|
|
|
116
|
+
## Working with large results
|
|
117
|
+
|
|
118
|
+
When a `find_*` tool's upstream total exceeds `loadLimit`, the full union materializes as a canvas dataframe and the response carries an inline `dataframe` handle (`{ tableName, rowCount, columns, createdAt, expiresAt, … }`). SQL is the paging idiom — use `LIMIT/OFFSET` to walk pages, projection (`SELECT col1, col2`) to trim columns, and aggregation (`COUNT`, `GROUP BY`, `AVG`) to summarize without materializing every row.
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
1. brapi_find_observations { studies: ["s-422"] }
|
|
122
|
+
→ first-page rows inline + dataframe.tableName = "df_<uuid>" (when totalCount > loadLimit)
|
|
123
|
+
2. brapi_dataframe_describe { dataframe: "df_<uuid>" }
|
|
124
|
+
→ schema + provenance (originating tool, baseUrl, query, expiry)
|
|
125
|
+
3. brapi_dataframe_query { sql: "SELECT germplasmName, value FROM df_<uuid> WHERE observationVariableDbId = 'V1' LIMIT 100" }
|
|
126
|
+
→ typed columns + bounded rows
|
|
127
|
+
4. brapi_dataframe_query { sql: "SELECT COUNT(*) AS n, AVG(CAST(value AS DOUBLE)) AS mean FROM df_<uuid> WHERE observationVariableDbId = 'V1'" }
|
|
128
|
+
→ aggregate without round-tripping all rows
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Dataframes auto-expire via TTL (`BRAPI_DATASET_TTL_SECONDS`, default 24h). Set `BRAPI_CANVAS_DROP_ENABLED=true` to expose `brapi_dataframe_drop` for explicit cleanup.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
123
135
|
## Getting started
|
|
124
136
|
|
|
125
137
|
Add to your MCP client config — pick one runner:
|
|
@@ -148,7 +160,7 @@ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
|
|
|
148
160
|
|
|
149
161
|
No env vars are required — the six built-in aliases (`bti-cassava`, `bti-sweetpotato`, `bti-breedbase-demo`, `t3-wheat`, `t3-oat`, `t3-barley`) resolve out-of-the-box, and agents can connect to any other BrAPI v2 URL at runtime via `brapi_connect`. **For credentialed servers, prefer env vars over agent input** so passwords / tokens / API keys stay out of the LLM context — see [Per-alias credentials](#per-alias-credentials).
|
|
150
162
|
|
|
151
|
-
**Prerequisites:** [Bun v1.3.11+](https://bun.sh/) or Node.js v22+.
|
|
163
|
+
**Prerequisites:** [Bun v1.3.11+](https://bun.sh/) or Node.js v22+. [`@duckdb/node-api`](https://www.npmjs.com/package/@duckdb/node-api) is a required dependency — supported on Linux/macOS/Windows × x64 plus Linux/macOS arm64 (no Windows arm64; no Cloudflare Workers).
|
|
152
164
|
|
|
153
165
|
---
|
|
154
166
|
|
|
@@ -163,18 +175,20 @@ Every variable is optional.
|
|
|
163
175
|
| `BRAPI_DEFAULT_OAUTH_CLIENT_ID` / `_OAUTH_CLIENT_SECRET` | OAuth2 client-credentials for the default connection. | — |
|
|
164
176
|
| `BRAPI_DEFAULT_API_KEY` / `_API_KEY_HEADER` | Static API key for the default connection. | header `Authorization` |
|
|
165
177
|
| `BRAPI_BUILTIN_ALIASES_DISABLED` | Comma-separated alias names (case-insensitive) to remove from the built-in registry. | — |
|
|
166
|
-
| `BRAPI_LOAD_LIMIT` | In-context row cap before `find_*` spills to `
|
|
178
|
+
| `BRAPI_LOAD_LIMIT` | In-context row cap before `find_*` spills to a canvas dataframe; also doubles as the upstream pageSize during spillover walks (`loadLimit × 50` is the dataframe ceiling). | `1000` |
|
|
167
179
|
| `BRAPI_MAX_CONCURRENT_REQUESTS` | Per-connection concurrency cap. | `4` |
|
|
168
180
|
| `BRAPI_RETRY_MAX_ATTEMPTS` / `BRAPI_RETRY_BASE_DELAY_MS` | Retry policy for 429/5xx with exponential backoff. | `3` / `500` |
|
|
169
181
|
| `BRAPI_REQUEST_TIMEOUT_MS` | Per-request HTTP timeout. | `30000` |
|
|
182
|
+
| `BRAPI_COMPANION_TIMEOUT_MS` | Tighter timeout for non-critical companion enrichments (FK lookups, count probes). Companions also bypass the retry budget so a slow upstream surfaces as a warning instead of stretching the response. | `8000` |
|
|
170
183
|
| `BRAPI_SEARCH_POLL_TIMEOUT_MS` / `_INTERVAL_MS` | Async `/search` polling budget + interval. | `60000` / `1000` |
|
|
171
|
-
| `BRAPI_DATASET_TTL_SECONDS`
|
|
184
|
+
| `BRAPI_DATASET_TTL_SECONDS` | TTL for dataframe provenance metadata persisted alongside spilled rows. | `86400` |
|
|
172
185
|
| `BRAPI_REFERENCE_CACHE_TTL_SECONDS` | TTL for programs / trials / locations / crops cache. | `3600` |
|
|
173
186
|
| `BRAPI_ALLOW_PRIVATE_IPS` | Allow RFC 1918 / loopback targets. Dev-only. | `false` |
|
|
174
187
|
| `BRAPI_ENABLE_WRITES` | Opt-in for `brapi_submit_observations` registration. | `false` |
|
|
175
188
|
| `BRAPI_GENOTYPE_CALLS_MAX_PULL` | Upstream row ceiling per `brapi_find_genotype_calls` invocation. Max 500,000. | `100000` |
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
189
|
+
| `BRAPI_CANVAS_DROP_ENABLED` | Opt-in for `brapi_dataframe_drop` registration. Off by default; dataframes expire via TTL when left unmanaged. | `false` |
|
|
190
|
+
| `BRAPI_EXPORT_DIR` | Directory for `brapi_dataframe_export` output files. Setting a path is the opt-in (no separate enable flag); unset leaves the tool out of `tools/list`. Stdio-only — the tool stays disabled under HTTP transport regardless of this value. Bridged to the framework's `CANVAS_EXPORT_PATH` automatically. | — |
|
|
191
|
+
| `BRAPI_CANVAS_MAX_ROWS` / `BRAPI_CANVAS_QUERY_TIMEOUT_MS` | Per-query response row cap and wall-clock timeout for `brapi_dataframe_query`. | `10000` / `30000` |
|
|
178
192
|
| `MCP_TRANSPORT_TYPE` / `MCP_HTTP_PORT` | Transport (`stdio` \| `http`) + HTTP port. | `stdio` / `3010` |
|
|
179
193
|
| `MCP_AUTH_MODE` / `MCP_LOG_LEVEL` / `STORAGE_PROVIDER_TYPE` / `OTEL_ENABLED` | Auth mode (`none` \| `jwt` \| `oauth`), log level, storage backend, OpenTelemetry. | `none` / `info` / `in-memory` / `false` |
|
|
180
194
|
|
|
@@ -263,7 +277,7 @@ Defaults to HTTP transport, stateless session mode, logs to `/var/log/brapi-mcp-
|
|
|
263
277
|
|
|
264
278
|
### Multi-user HTTP deployments
|
|
265
279
|
|
|
266
|
-
`ctx.state` (where `ServerRegistry`, `
|
|
280
|
+
`ctx.state` (where `ServerRegistry`, `CanvasBridge`, and `CapabilityRegistry` live) is **scoped by `tenantId`, not session id**:
|
|
267
281
|
|
|
268
282
|
| Mode | `tenantId` |
|
|
269
283
|
|:-----|:-----------|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Consolidate spillover onto canvas-only dataframes — remove brapi_manage_dataset and the brapi://dataset/{id} resource, mandate DuckDB, gate brapi_dataframe_drop opt-in, and return typed columns from brapi_dataframe_query."
|
|
3
|
+
breaking: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 0.5.0 — 2026-05-05
|
|
7
|
+
|
|
8
|
+
Collapses the dual-store spillover model. Pre-canvas, `find_*` tools wrote spilled rows twice — once as JSON in `ctx.state` (driving `brapi_manage_dataset`) and once as a DuckDB table on the canvas (driving `brapi_dataframe_*`). The two surfaces did the same job, so this release retires the JSON store and makes SQL the single way to work with spilled results. DuckDB is now a regular dependency rather than an optional peer; the server fails fast at startup when it isn't available. Resolves [#25](https://github.com/cyanheads/brapi-mcp-server/issues/25), subsumes [#24](https://github.com/cyanheads/brapi-mcp-server/issues/24).
|
|
9
|
+
|
|
10
|
+
## Migration
|
|
11
|
+
|
|
12
|
+
| Before (≤ 0.4.x) | After (0.5.0) |
|
|
13
|
+
|:------------------|:---------------|
|
|
14
|
+
| `brapi_manage_dataset { mode: 'list' }` | `brapi_dataframe_describe` (no args) |
|
|
15
|
+
| `brapi_manage_dataset { mode: 'summary', datasetId }` | `brapi_dataframe_describe { dataframe: 'df_<uuid>' }` |
|
|
16
|
+
| `brapi_manage_dataset { mode: 'load', datasetId, page, pageSize, columns }` | `brapi_dataframe_query { sql: 'SELECT col1, col2 FROM df_<uuid> LIMIT 100 OFFSET 0' }` |
|
|
17
|
+
| `brapi_manage_dataset { mode: 'delete', datasetId }` | `brapi_dataframe_drop { dataframe }` when `BRAPI_CANVAS_DROP_ENABLED=true`; otherwise wait for canvas TTL |
|
|
18
|
+
| `brapi://dataset/{id}` resource | `brapi_dataframe_describe { dataframe }` |
|
|
19
|
+
| `find_*` tool output `dataset.datasetId` | `find_*` tool output `dataframe.tableName` |
|
|
20
|
+
|
|
21
|
+
## Removed
|
|
22
|
+
|
|
23
|
+
- **Tool: `brapi_manage_dataset`.** The lifecycle surface for ctx.state-stored datasets is gone. Use `brapi_dataframe_describe` to discover dataframes, `brapi_dataframe_query` to read or aggregate, and `brapi_dataframe_drop` (opt-in, see Added) to release one explicitly.
|
|
24
|
+
- **Resource: `brapi://dataset/{datasetId}`.** Provenance now lives on the dataframe itself; describe surfaces source / baseUrl / query / createdAt / expiresAt under `provenance` for any auto-registered `df_*` table.
|
|
25
|
+
- **Service: `src/services/dataset-store/`** (entire module). Spilled rows go directly to the canvas — no parallel JSON copy.
|
|
26
|
+
- **Env var: `BRAPI_CANVAS_ENABLED`.** Canvas is mandatory now. Startup fails with a clear `ConfigurationError` when `core.canvas` is undefined (e.g., DuckDB unavailable).
|
|
27
|
+
- **Env var: `BRAPI_DATASET_STORE_DIR`** (filesystem path for the deprecated DatasetStore). No longer read.
|
|
28
|
+
- **Helper: `spillToDataset` in `src/mcp-server/tools/shared/find-helpers.ts`.** Replaced by `spillToCanvas`. The shared `DatasetHandleSchema` / `renderDatasetHandle` / `toDatasetHandle` exports are renamed to their `Dataframe*` equivalents.
|
|
29
|
+
- **`docs/compatibility.md`.** Stale live-server probe matrix dropped — to be regenerated as needed against the current dialect surface. README cross-link removed.
|
|
30
|
+
|
|
31
|
+
## Added
|
|
32
|
+
|
|
33
|
+
- **Env var: `BRAPI_CANVAS_DROP_ENABLED`** (default `false`). Opt-in for `brapi_dataframe_drop` — when unset, the tool is omitted from `tools/list` entirely and dataframes expire via TTL when left unmanaged. Resolves [#24](https://github.com/cyanheads/brapi-mcp-server/issues/24).
|
|
34
|
+
- **`brapi_dataframe_query` returns typed columns.** Output `columns` field changes from `string[]` (names only) to `{ name: string; type: string }[]` (name + SQL/DuckDB type). Types come from DuckDB's describe in both directions: when `registerAs` is set the user-named table is described directly, and when it isn't the handler runs the query under an internal `_brapi_probe_<uuid>` `registerAs` so DuckDB's authoritative types survive the JSON round-trip (which would otherwise flatten BIGINT and other non-JSON-native types to VARCHAR strings), then drops the probe before returning. The agent learns the schema implicitly through any query response — no need to round-trip through `dataframe_describe` between queries.
|
|
35
|
+
- **Server-side default `CANVAS_PROVIDER_TYPE=duckdb`.** The framework defaults this to `none`, which would fail every startup now that canvas is mandatory. The entry point sets the env var to `duckdb` before `createApp` reads config, so the server works out-of-the-box without operator setup. Operator-set values pass through untouched (including `none` for diagnostic runs that should fail closed).
|
|
36
|
+
- **`brapi_raw_get` and `brapi_raw_search` spill to a canvas dataframe.** When the upstream advertises more rows than `loadLimit` AND the result is a list shape (top-level array or BrAPI `result.data` envelope), the handler walks subsequent pages via `spillToCanvas` and attaches a `dataframe` handle on the response — same `df_<uuid>` shape as `find_*`. Inline `result` is returned **unchanged** so the raw escape hatch keeps its semantics; the dataframe is the "and there is more, here" pointer. Both tools now accept an optional `loadLimit` input (defaults to `serverConfig.loadLimit`, 1,000). Spillover is intentionally skipped when the caller drives paging via `params.page` / `params.pageSize` (raw_get) or `body.page` / `body.pageSize` (raw_search) — they are walking pages explicitly. Non-list results (single objects, scalars) pass through as before regardless of total count. New `extractListRows` helper in `find-helpers.ts` factors out the list-shape detection.
|
|
37
|
+
- **Tool: `brapi_dataframe_export`.** Write a dataframe to disk in CSV, Parquet, or JSON for human review — the agent calls it when the researcher signals they want to open the data in Excel, Tad, DuckDB CLI, etc. Returns the absolute path; the file lands inside the operator-configured `BRAPI_EXPORT_DIR`. Optional `columns` (thin projection) or `sql` (full SELECT, mutually exclusive) materializes a derived canvas table for the export and drops it after. Default filename is `<dataframe>-<unixSeconds>.<ext>` so re-exports never collide; explicit `filename` overwrites on collision. Best-effort sweep of stale files (mtime past `BRAPI_DATASET_TTL_SECONDS`) runs on each call; `brapi_dataframe_drop` unlinks paired export files alongside the dataframe. Resolves [#18](https://github.com/cyanheads/brapi-mcp-server/issues/18).
|
|
38
|
+
- **Env var: `BRAPI_EXPORT_DIR`.** Setting a writable path is the opt-in for `brapi_dataframe_export` (no separate enable flag). Bridged automatically to the framework's `CANVAS_EXPORT_PATH` so the existing canvas path-traversal sandbox (absolute paths and `..` segments rejected) governs filename inputs. Tool registration also requires `MCP_TRANSPORT_TYPE=stdio`; under HTTP transport the tool stays disabled regardless of this value because the returned path lives on the server, not the user. Operators see two distinct gate reasons in `/.well-known/mcp.json` — "stdio transport required" and "BRAPI_EXPORT_DIR unset" — depending on which check failed.
|
|
39
|
+
- **`CanvasBridge.export(ctx, tableName, target, options)`** wraps the framework's `instance.export()` with optional path tracking via `CanvasBridge.trackExport(ctx, sourceName, path)`. Tracked paths key off the *source* dataframe so `brapi_dataframe_drop(ctx, sourceName)` unlinks the file alongside the canvas table — the bridge's `drop()` reads `brapi/canvas/exports/<name>` and unlinks before deleting the state entry. Tracking is caller-driven (the export tool tracks; the bridge does not auto-track) so a transient projection table can be exported then dropped without unlinking the file.
|
|
40
|
+
|
|
41
|
+
## Changed
|
|
42
|
+
|
|
43
|
+
- **`find_*` tool output: `dataset` field renamed to `dataframe`.** Shape simplified: drops `datasetId` and `sizeBytes`; keeps `tableName`, `rowCount`, `columns`, `createdAt`, `expiresAt`, `truncated`, `maxRows`. Eight tools affected: `brapi_find_germplasm`, `brapi_find_studies`, `brapi_find_observations`, `brapi_find_locations`, `brapi_find_variables`, `brapi_find_variants`, `brapi_find_images`, `brapi_find_genotype_calls`.
|
|
44
|
+
- **`find_*` tool descriptions emit "dataframe handle" language.** Render output points at `brapi_dataframe_describe` and `brapi_dataframe_query` instead of `brapi_manage_dataset`.
|
|
45
|
+
- **`brapi_dataframe_query` description: explicit SQL-as-paging guidance.** `LIMIT/OFFSET`, projection (`SELECT col1, col2`), and aggregation (`COUNT`, `GROUP BY`, `AVG`) are the paging idiom — no need for a structured-paging shim on top.
|
|
46
|
+
- **`brapi_dataframe_describe` description: leads with "start here after a spillover."** The discovery surface is the natural first step after a `find_*` call returns a `dataframe` handle.
|
|
47
|
+
- **`brapi_dataframe_drop` description and error contract.** Drops the `dataframe_disabled` typed error (canvas is mandatory now); idempotent semantics unchanged — returns `dropped: false` rather than failing for unknown names.
|
|
48
|
+
- **`brapi_dataframe_describe` provenance schema: drop `datasetId`.** The dataframe name (`df_<uuid>`) is the only identity now. `provenance` exposes `source`, `baseUrl`, `query`, `createdAt`, `expiresAt`.
|
|
49
|
+
- **Internal canvas table prefix: `ds_` → `df_`** (in `canvas-bridge.ts`). Matches the new noun. The agent always sees the full table name from the handle, so this is a purely internal rename.
|
|
50
|
+
- **Prompts updated.** `brapi_eda_study` and `brapi_meta_analysis` now point at `brapi_dataframe_query` (with SQL paging guidance) instead of `brapi_manage_dataset` for accessing spilled rows.
|
|
51
|
+
- **Raw routing hints (`raw-routing-hints.ts`) updated** to reference dataframe spillover instead of dataset spillover.
|
|
52
|
+
- **`BRAPI_LOAD_LIMIT` default raised 200 → 1,000.** `loadLimit` doubles as the upstream `pageSize` during spillover walks, so the prior default silently capped dataframes at `200 × 50 pages = 10,000` rows even though `MAX_SPILLOVER_ROWS = 50,000` was the documented ceiling. The new default lines up the math: `1,000 × 50 = 50,000`, matching the cap. Operators on small/test BrAPI servers can lower it via env; raising it above 1,000 is rarely useful because most BrAPI servers cap server-side `pageSize` at 1,000.
|
|
53
|
+
- **`brapi_find_genotype_calls` honors `BRAPI_LOAD_LIMIT`.** The handler had a hardcoded `?? 200` fallback that ignored the configured default. Now reads `getServerConfig().loadLimit` like every other find_* tool. Tool description and `loadLimit` input description both updated to drop the stale "default 200" claim. Genotype calls remain a special case in one respect: upstream pageSize is fixed at 10,000 (decoupled from `loadLimit`), so the dual-role footgun other find_* tools have doesn't apply here — the input describe calls this out explicitly.
|
|
54
|
+
- **`LoadLimitInput.describe()` rewritten for honest agent guidance.** The shared input fragment now warns that lowering `loadLimit` to "see fewer rows inline" silently shrinks dataframe spillover capacity proportionally (because `loadLimit` doubles as upstream `pageSize`), and points the agent at SQL `LIMIT` on the dataframe for sampling instead. Drops env-var names and specific numeric values from the agent-facing copy.
|
|
55
|
+
- **`brapi_meta_analysis` prompt drops hardcoded `loadLimit: 200`.** The Step 2 directive let the deployment default win instead of pinning a number that's no longer the default. One-line fix.
|
|
56
|
+
|
|
57
|
+
## Dependencies
|
|
58
|
+
|
|
59
|
+
- **`@duckdb/node-api` promoted from optional peer to regular dependency.** Spillover is the primary value of this server; making DuckDB optional only protected users we don't target. Cloudflare Workers deployments are not supported until a non-DuckDB canvas provider lands upstream — documented as a known constraint.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Verify extraFilters honor on every find_* tool and relabel the rendered filters block as `Filters sent to server` so the trace stops implying the upstream applied them."
|
|
3
|
+
breaking: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 0.5.1 — 2026-05-05
|
|
7
|
+
|
|
8
|
+
Two field-test wrong-results regressions on `find_*` tools. Both classes shared a root cause: the trace claimed filter values were honored when the upstream had silently dropped them.
|
|
9
|
+
|
|
10
|
+
## Fixed
|
|
11
|
+
|
|
12
|
+
- **`find_*` tools now post-hoc-verify `extraFilters` keys against the returned rows.** New `buildExtraFilterChecks` helper in `src/mcp-server/tools/shared/find-helpers.ts` infers the row column for each `extraFilters` key (exact match, then trailing-`s` strip — `locationDbIds → locationDbId`, `locationNames → locationName`) and feeds a `FilterMatchCheck` into `checkFilterMatchRates`. When the upstream silently ignored a filter (e.g. `locationName` on `/studies` — not a valid spec key), every returned row carries an unrequested value and the validator emits a warning instead of letting the empty trace falsely confirm the filter was applied. Keys that don't resolve to a column produce a single grouped warning so the agent knows the trace can't speak to whether they were honored. Wired across `find_studies`, `find_germplasm`, `find_observations`, `find_variables`, `find_variants`, `find_images`, and `find_locations`. `find_variants` had no post-hoc validator at all before this change — it now runs `checkFilterMatchRates` for the first time.
|
|
13
|
+
- **`find_*` tools render the wire-shape filters block as `Filters sent to server` instead of `Applied filters`.** "Applied" suggested the upstream honored every key; "sent to server" is true regardless of honor and pairs cleanly with the new verification warnings. Updated label and JSDoc in `renderAppliedFilters` (function name kept for diff minimality — only the rendered string changes).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Built-in known-server registry. Curated list of public BrAPI v2
|
|
3
3
|
* endpoints that ship as default connection aliases — operators can call
|
|
4
|
-
* `brapi_connect({ alias: 'cassava' })` (or any other entry) without setting
|
|
4
|
+
* `brapi_connect({ alias: 'bti-cassava' })` (or any other entry) without setting
|
|
5
5
|
* `BRAPI_<ALIAS>_BASE_URL`. Each entry carries CC-BY attribution metadata so
|
|
6
6
|
* the orientation envelope can surface license + citation alongside the data.
|
|
7
7
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Built-in known-server registry. Curated list of public BrAPI v2
|
|
3
3
|
* endpoints that ship as default connection aliases — operators can call
|
|
4
|
-
* `brapi_connect({ alias: 'cassava' })` (or any other entry) without setting
|
|
4
|
+
* `brapi_connect({ alias: 'bti-cassava' })` (or any other entry) without setting
|
|
5
5
|
* `BRAPI_<ALIAS>_BASE_URL`. Each entry carries CC-BY attribution metadata so
|
|
6
6
|
* the orientation envelope can surface license + citation alongside the data.
|
|
7
7
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Server-specific configuration for brapi-mcp-server. Covers
|
|
3
3
|
* default connection details, HTTP client behavior (timeouts, retries,
|
|
4
|
-
* concurrency), async-search polling,
|
|
4
|
+
* concurrency), async-search polling, dataframe lifecycle, and reference-data
|
|
5
5
|
* cache tuning. Lazy-parsed via `parseEnvConfig` so env-var names appear in
|
|
6
6
|
* validation errors.
|
|
7
7
|
*
|
|
@@ -17,7 +17,6 @@ declare const ServerConfigSchema: z.ZodObject<{
|
|
|
17
17
|
defaultApiKey: z.ZodOptional<z.ZodString>;
|
|
18
18
|
defaultApiKeyHeader: z.ZodDefault<z.ZodString>;
|
|
19
19
|
datasetTtlSeconds: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
20
|
-
datasetStoreDir: z.ZodOptional<z.ZodString>;
|
|
21
20
|
loadLimit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
22
21
|
maxConcurrentRequests: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
23
22
|
retryMaxAttempts: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
@@ -36,10 +35,11 @@ declare const ServerConfigSchema: z.ZodObject<{
|
|
|
36
35
|
false: "false";
|
|
37
36
|
}>>, z.ZodTransform<boolean, "true" | "false">>;
|
|
38
37
|
genotypeCallsMaxPull: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
39
|
-
|
|
38
|
+
canvasDropEnabled: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
40
39
|
true: "true";
|
|
41
40
|
false: "false";
|
|
42
41
|
}>>, z.ZodTransform<boolean, "true" | "false">>;
|
|
42
|
+
exportDir: z.ZodOptional<z.ZodString>;
|
|
43
43
|
canvasMaxRows: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
44
44
|
canvasQueryTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
45
45
|
}, z.core.$strip>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4ItB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CA4B9C;AAED,uDAAuD;AACvD,wBAAgB,iBAAiB,IAAI,IAAI,CAExC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Server-specific configuration for brapi-mcp-server. Covers
|
|
3
3
|
* default connection details, HTTP client behavior (timeouts, retries,
|
|
4
|
-
* concurrency), async-search polling,
|
|
4
|
+
* concurrency), async-search polling, dataframe lifecycle, and reference-data
|
|
5
5
|
* cache tuning. Lazy-parsed via `parseEnvConfig` so env-var names appear in
|
|
6
6
|
* validation errors.
|
|
7
7
|
*
|
|
@@ -38,17 +38,13 @@ const ServerConfigSchema = z.object({
|
|
|
38
38
|
.int()
|
|
39
39
|
.positive()
|
|
40
40
|
.default(86_400)
|
|
41
|
-
.describe('TTL for
|
|
42
|
-
datasetStoreDir: z
|
|
43
|
-
.string()
|
|
44
|
-
.optional()
|
|
45
|
-
.describe('Filesystem path for DatasetStore payloads when filesystem storage is active.'),
|
|
41
|
+
.describe('TTL for dataframe provenance metadata persisted alongside spilled rows (seconds).'),
|
|
46
42
|
loadLimit: z.coerce
|
|
47
43
|
.number()
|
|
48
44
|
.int()
|
|
49
45
|
.positive()
|
|
50
|
-
.default(
|
|
51
|
-
.describe('Default row cap returned in-context before spilling to
|
|
46
|
+
.default(1_000)
|
|
47
|
+
.describe('Default row cap returned in-context before spilling to a canvas dataframe. Also doubles as the upstream `pageSize` used during spillover walks — the dataframe ceiling is `loadLimit × MAX_SPILLOVER_PAGES (50)`, so the 1,000 default lines up with the documented 50,000-row spillover cap. Operators on small/test BrAPI servers can lower it; raising it above 1,000 is rarely useful because most BrAPI servers cap server-side pageSize at 1,000.'),
|
|
52
48
|
maxConcurrentRequests: z.coerce
|
|
53
49
|
.number()
|
|
54
50
|
.int()
|
|
@@ -114,11 +110,15 @@ const ServerConfigSchema = z.object({
|
|
|
114
110
|
.max(500_000)
|
|
115
111
|
.default(100_000)
|
|
116
112
|
.describe('Hard ceiling on rows pulled from the upstream BrAPI server in a single brapi_find_genotype_calls invocation. Bounds total page count per query — protects the upstream from unbounded pagination loops. Default 100,000 (≈10 pages at the standard pageSize=10,000); maximum 500,000 (≈50 pages, matching the per-query budget of other find_* tools).'),
|
|
117
|
-
|
|
113
|
+
canvasDropEnabled: z
|
|
118
114
|
.enum(['true', 'false'])
|
|
119
|
-
.default('
|
|
115
|
+
.default('false')
|
|
120
116
|
.transform((v) => v === 'true')
|
|
121
|
-
.describe('
|
|
117
|
+
.describe('Opt-in flag for `brapi_dataframe_drop`. Off by default — the tool is omitted from `tools/list` unless the operator opts in. Dataframes still expire via TTL when left unmanaged.'),
|
|
118
|
+
exportDir: z
|
|
119
|
+
.string()
|
|
120
|
+
.optional()
|
|
121
|
+
.describe('Filesystem directory for `brapi_dataframe_export` output files. When unset, the export tool is omitted from `tools/list` (no separate enable flag — setting the path *is* the opt-in). The framework reads the resolved path from `CANVAS_EXPORT_PATH`; the entry point bridges `BRAPI_EXPORT_DIR` → `CANVAS_EXPORT_PATH` so operators only need to set the brapi-prefixed name. Stdio-only — under `MCP_TRANSPORT_TYPE=http` the tool stays disabled regardless of this value because the returned path lives on the server, not the user.'),
|
|
122
122
|
canvasMaxRows: z.coerce
|
|
123
123
|
.number()
|
|
124
124
|
.int()
|
|
@@ -143,7 +143,6 @@ export function getServerConfig() {
|
|
|
143
143
|
defaultApiKey: 'BRAPI_DEFAULT_API_KEY',
|
|
144
144
|
defaultApiKeyHeader: 'BRAPI_DEFAULT_API_KEY_HEADER',
|
|
145
145
|
datasetTtlSeconds: 'BRAPI_DATASET_TTL_SECONDS',
|
|
146
|
-
datasetStoreDir: 'BRAPI_DATASET_STORE_DIR',
|
|
147
146
|
loadLimit: 'BRAPI_LOAD_LIMIT',
|
|
148
147
|
maxConcurrentRequests: 'BRAPI_MAX_CONCURRENT_REQUESTS',
|
|
149
148
|
retryMaxAttempts: 'BRAPI_RETRY_MAX_ATTEMPTS',
|
|
@@ -156,7 +155,8 @@ export function getServerConfig() {
|
|
|
156
155
|
allowPrivateIps: 'BRAPI_ALLOW_PRIVATE_IPS',
|
|
157
156
|
enableWrites: 'BRAPI_ENABLE_WRITES',
|
|
158
157
|
genotypeCallsMaxPull: 'BRAPI_GENOTYPE_CALLS_MAX_PULL',
|
|
159
|
-
|
|
158
|
+
canvasDropEnabled: 'BRAPI_CANVAS_DROP_ENABLED',
|
|
159
|
+
exportDir: 'BRAPI_EXPORT_DIR',
|
|
160
160
|
canvasMaxRows: 'BRAPI_CANVAS_MAX_ROWS',
|
|
161
161
|
canvasQueryTimeoutMs: 'BRAPI_CANVAS_QUERY_TIMEOUT_MS',
|
|
162
162
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,cAAc,EAAE,CAAC;SACd,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,QAAQ,CAAC,yEAAyE,CAAC;IACtF,eAAe,EAAE,CAAC;SACf,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,qDAAqD,CAAC;IAClE,eAAe,EAAE,CAAC;SACf,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,qDAAqD,CAAC;IAClE,oBAAoB,EAAE,CAAC;SACpB,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,uDAAuD,CAAC;IACpE,wBAAwB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;IACzF,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yBAAyB,CAAC;IACxE,mBAAmB,EAAE,CAAC;SACnB,MAAM,EAAE;SACR,OAAO,CAAC,eAAe,CAAC;SACxB,QAAQ,CAAC,0CAA0C,CAAC;IAEvD,iBAAiB,EAAE,CAAC,CAAC,MAAM;SACxB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,MAAM,CAAC;SACf,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAE/D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,cAAc,EAAE,CAAC;SACd,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,QAAQ,CAAC,yEAAyE,CAAC;IACtF,eAAe,EAAE,CAAC;SACf,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,qDAAqD,CAAC;IAClE,eAAe,EAAE,CAAC;SACf,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,qDAAqD,CAAC;IAClE,oBAAoB,EAAE,CAAC;SACpB,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,uDAAuD,CAAC;IACpE,wBAAwB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;IACzF,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,yBAAyB,CAAC;IACxE,mBAAmB,EAAE,CAAC;SACnB,MAAM,EAAE;SACR,OAAO,CAAC,eAAe,CAAC;SACxB,QAAQ,CAAC,0CAA0C,CAAC;IAEvD,iBAAiB,EAAE,CAAC,CAAC,MAAM;SACxB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,MAAM,CAAC;SACf,QAAQ,CAAC,mFAAmF,CAAC;IAEhG,SAAS,EAAE,CAAC,CAAC,MAAM;SAChB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CACP,ybAAyb,CAC1b;IACH,qBAAqB,EAAE,CAAC,CAAC,MAAM;SAC5B,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,CAAC,CAAC;SACV,QAAQ,CAAC,+DAA+D,CAAC;IAC5E,gBAAgB,EAAE,CAAC,CAAC,MAAM;SACvB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,CAAC,CAAC;SACV,QAAQ,CAAC,0CAA0C,CAAC;IACvD,gBAAgB,EAAE,CAAC,CAAC,MAAM;SACvB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,GAAG,CAAC;SACZ,QAAQ,CAAC,8CAA8C,CAAC;IAC3D,wBAAwB,EAAE,CAAC,CAAC,MAAM;SAC/B,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CAAC,0EAA0E,CAAC;IAEvF,gBAAgB,EAAE,CAAC,CAAC,MAAM;SACvB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,MAAM,CAAC;SACf,QAAQ,CAAC,2BAA2B,CAAC;IACxC,kBAAkB,EAAE,CAAC,CAAC,MAAM;SACzB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CACP,4MAA4M,CAC7M;IACH,mBAAmB,EAAE,CAAC,CAAC,MAAM;SAC1B,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,MAAM,CAAC;SACf,QAAQ,CAAC,0DAA0D,CAAC;IACvE,oBAAoB,EAAE,CAAC,CAAC,MAAM;SAC3B,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,KAAK,CAAC;SACd,QAAQ,CAAC,mDAAmD,CAAC;IAChE,eAAe,EAAE,CAAC;SACf,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;SACvB,OAAO,CAAC,OAAO,CAAC;SAChB,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC;SAC9B,QAAQ,CAAC,4DAA4D,CAAC;IACzE,YAAY,EAAE,CAAC;SACZ,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;SACvB,OAAO,CAAC,OAAO,CAAC;SAChB,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC;SAC9B,QAAQ,CACP,0KAA0K,CAC3K;IACH,oBAAoB,EAAE,CAAC,CAAC,MAAM;SAC3B,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,GAAG,CAAC,OAAO,CAAC;SACZ,OAAO,CAAC,OAAO,CAAC;SAChB,QAAQ,CACP,wVAAwV,CACzV;IAEH,iBAAiB,EAAE,CAAC;SACjB,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;SACvB,OAAO,CAAC,OAAO,CAAC;SAChB,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,MAAM,CAAC;SAC9B,QAAQ,CACP,kLAAkL,CACnL;IACH,SAAS,EAAE,CAAC;SACT,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACP,6gBAA6gB,CAC9gB;IACH,aAAa,EAAE,CAAC,CAAC,MAAM;SACpB,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,MAAM,CAAC;SACf,QAAQ,CACP,4JAA4J,CAC7J;IACH,oBAAoB,EAAE,CAAC,CAAC,MAAM;SAC3B,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,OAAO,CAAC,MAAM,CAAC;SACf,QAAQ,CAAC,yDAAyD,CAAC;CACvE,CAAC,CAAC;AAIH,IAAI,OAAiC,CAAC;AAEtC,MAAM,UAAU,eAAe;IAC7B,OAAO,KAAK,cAAc,CAAC,kBAAkB,EAAE;QAC7C,cAAc,EAAE,wBAAwB;QACxC,eAAe,EAAE,wBAAwB;QACzC,eAAe,EAAE,wBAAwB;QACzC,oBAAoB,EAAE,+BAA+B;QACrD,wBAAwB,EAAE,mCAAmC;QAC7D,aAAa,EAAE,uBAAuB;QACtC,mBAAmB,EAAE,8BAA8B;QACnD,iBAAiB,EAAE,2BAA2B;QAC9C,SAAS,EAAE,kBAAkB;QAC7B,qBAAqB,EAAE,+BAA+B;QACtD,gBAAgB,EAAE,0BAA0B;QAC5C,gBAAgB,EAAE,2BAA2B;QAC7C,wBAAwB,EAAE,mCAAmC;QAC7D,gBAAgB,EAAE,0BAA0B;QAC5C,kBAAkB,EAAE,4BAA4B;QAChD,mBAAmB,EAAE,8BAA8B;QACnD,oBAAoB,EAAE,+BAA+B;QACrD,eAAe,EAAE,yBAAyB;QAC1C,YAAY,EAAE,qBAAqB;QACnC,oBAAoB,EAAE,+BAA+B;QACrD,iBAAiB,EAAE,2BAA2B;QAC9C,SAAS,EAAE,kBAAkB;QAC7B,aAAa,EAAE,uBAAuB;QACtC,oBAAoB,EAAE,+BAA+B;KACtD,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,uDAAuD;AACvD,MAAM,UAAU,iBAAiB;IAC/B,OAAO,GAAG,SAAS,CAAC;AACtB,CAAC"}
|