@cyanheads/brapi-mcp-server 0.7.13 → 0.8.0

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.
Files changed (54) hide show
  1. package/AGENTS.md +15 -8
  2. package/CLAUDE.md +15 -8
  3. package/README.md +206 -240
  4. package/changelog/0.8.x/0.8.0.md +48 -0
  5. package/dist/config/alias-credentials.d.ts +28 -7
  6. package/dist/config/alias-credentials.d.ts.map +1 -1
  7. package/dist/config/alias-credentials.js +91 -24
  8. package/dist/config/alias-credentials.js.map +1 -1
  9. package/dist/config/builtin-aliases.d.ts +11 -6
  10. package/dist/config/builtin-aliases.d.ts.map +1 -1
  11. package/dist/config/builtin-aliases.js +23 -37
  12. package/dist/config/builtin-aliases.js.map +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/mcp-server/resources/definitions/brapi-server-info.resource.js +1 -1
  16. package/dist/mcp-server/resources/definitions/brapi-server-info.resource.js.map +1 -1
  17. package/dist/mcp-server/tools/definitions/brapi-connect.tool.d.ts +46 -0
  18. package/dist/mcp-server/tools/definitions/brapi-connect.tool.d.ts.map +1 -1
  19. package/dist/mcp-server/tools/definitions/brapi-connect.tool.js +65 -15
  20. package/dist/mcp-server/tools/definitions/brapi-connect.tool.js.map +1 -1
  21. package/dist/mcp-server/tools/definitions/brapi-describe-filters.tool.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/definitions/brapi-describe-filters.tool.js +7 -2
  23. package/dist/mcp-server/tools/definitions/brapi-describe-filters.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/brapi-server-info.tool.d.ts +12 -0
  25. package/dist/mcp-server/tools/definitions/brapi-server-info.tool.d.ts.map +1 -1
  26. package/dist/mcp-server/tools/definitions/brapi-server-info.tool.js +1 -1
  27. package/dist/mcp-server/tools/definitions/brapi-server-info.tool.js.map +1 -1
  28. package/dist/mcp-server/tools/definitions/index.d.ts +123 -65
  29. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
  30. package/dist/mcp-server/tools/shared/find-helpers.d.ts +6 -0
  31. package/dist/mcp-server/tools/shared/find-helpers.d.ts.map +1 -1
  32. package/dist/mcp-server/tools/shared/find-helpers.js +17 -5
  33. package/dist/mcp-server/tools/shared/find-helpers.js.map +1 -1
  34. package/dist/mcp-server/tools/shared/orientation-envelope.d.ts +26 -0
  35. package/dist/mcp-server/tools/shared/orientation-envelope.d.ts.map +1 -1
  36. package/dist/mcp-server/tools/shared/orientation-envelope.js +75 -6
  37. package/dist/mcp-server/tools/shared/orientation-envelope.js.map +1 -1
  38. package/dist/services/brapi-client/brapi-client.d.ts.map +1 -1
  39. package/dist/services/brapi-client/brapi-client.js +29 -26
  40. package/dist/services/brapi-client/brapi-client.js.map +1 -1
  41. package/dist/services/capability-registry/capability-registry.d.ts +8 -1
  42. package/dist/services/capability-registry/capability-registry.d.ts.map +1 -1
  43. package/dist/services/capability-registry/capability-registry.js +37 -6
  44. package/dist/services/capability-registry/capability-registry.js.map +1 -1
  45. package/dist/services/reference-data-cache/reference-data-cache.d.ts.map +1 -1
  46. package/dist/services/reference-data-cache/reference-data-cache.js +9 -4
  47. package/dist/services/reference-data-cache/reference-data-cache.js.map +1 -1
  48. package/dist/services/server-registry/server-registry.d.ts +17 -3
  49. package/dist/services/server-registry/server-registry.d.ts.map +1 -1
  50. package/dist/services/server-registry/server-registry.js +30 -4
  51. package/dist/services/server-registry/server-registry.js.map +1 -1
  52. package/manifest.json +1 -1
  53. package/package.json +2 -2
  54. package/server.json +3 -3
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.7.13-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/cyanheads/brapi-mcp-server/pkgs/container/brapi-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/brapi-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/brapi-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/) [![Status](https://img.shields.io/badge/Status-Beta-yellow.svg?style=flat-square)](./CHANGELOG.md)
10
+ [![Version](https://img.shields.io/badge/Version-0.8.0-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/cyanheads/brapi-mcp-server/pkgs/container/brapi-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/brapi-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/brapi-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/) [![Status](https://img.shields.io/badge/Status-Beta-yellow.svg?style=flat-square)](./CHANGELOG.md)
11
11
 
12
12
  </div>
13
13
 
@@ -29,343 +29,295 @@
29
29
 
30
30
  ## Overview
31
31
 
32
- BrAPI v2.1 (the Breeding API) data from Breedbase, T3, Sweetpotatobase, and any BrAPI v2-compliant server. Search studies, germplasm, observations, genotypes, images, locations, and variants — result sets beyond the per-call cap spill into a DuckDB-backed dataframe workspace that agents on the same session can query with SQL or hand off by name, and connections to multiple upstream servers can be held open in parallel under named aliases. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
32
+ Plant-breeding data from any BrAPI (Breeding API) v2 server, including Breedbase instances such as Cassavabase and Sweetpotatobase and the Triticeae Toolbox (T3), with several servers connected at once under named aliases. Search studies, germplasm, observations, genotype calls, images, locations, and variants, walk pedigrees, and build phenotype and genotype matrices; results past the per-call cap spill into a DuckDB dataframe workspace that agents in the same session query with SQL. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
33
33
 
34
34
  ### Tools
35
35
 
36
36
  | Tool | Description |
37
37
  |:---|:---|
38
- | `brapi_connect` | Authenticate to a BrAPI v2 server, register the connection under an alias, and return the full orientation envelope in one call. |
39
- | `brapi_server_info` | Re-fetch the orientation envelope for a registered alias, optionally forcing a capability refresh. |
40
- | `brapi_describe_filters` | List valid filter names for a BrAPI endpoint — companion lookup for `extraFilters` on any `find_*` tool. |
41
- | `brapi_find_studies` | Find studies by crop, trial type, season, location, or program, with distributions and dataframe spillover. |
42
- | `brapi_get_study` | Fetch a study with program/trial/location resolved and companion counts (observations, units, variables). |
43
- | `brapi_find_germplasm` | Find germplasm by name, synonym, accession, PUI, crop, or free text, with distributions and dataframe spillover. |
44
- | `brapi_get_germplasm` | Fetch a germplasm with attributes, direct parents, and companion counts (studies, parents, descendants). |
45
- | `brapi_walk_pedigree` | BFS-walk ancestry or descendancy as a deduplicated DAG with cycle detection and depth limits. |
46
- | `brapi_find_variables` | Find observation variables by name, trait class, ontology term, or free text, ranked via `OntologyResolver`. |
47
- | `brapi_find_observations` | Pull observation records by study, germplasm, variable, season, or unit, with dataframe spillover. |
48
- | `brapi_find_images` | Filter image metadata by unit, observation, study, ontology term, or MIME type. Bytes via `brapi_get_image`. |
49
- | `brapi_get_image` | Fetch image bytes for up to 5 `imageDbId`s inline as `type: image` content blocks. |
50
- | `brapi_find_locations` | Find research stations by country, type, abbreviation, or bounding box. |
51
- | `brapi_find_variants` | Find variant records by variant set, reference, or genomic region. |
52
- | `brapi_find_genotype_calls` | Pull genotype calls via async-search polling, bounded by an upstream pull ceiling. |
53
- | `brapi_dataframe_describe` | List dataframes (or describe one) with column schema, row counts, and originating-source provenance. |
54
- | `brapi_dataframe_query` | Run read-only SQL across in-memory dataframes (DuckDB-backed). |
55
- | `brapi_dataframe_drop` | _Opt-in._ Drop a dataframe by name. Idempotent. |
56
- | `brapi_dataframe_export` | _Opt-in, stdio-only._ Export a dataframe to disk as CSV, Parquet, or JSON. |
57
- | `brapi_build_phenotype_matrix` | Build a germplasm × trait matrix from one or more studies, materialized as a canvas dataframe. |
58
- | `brapi_germplasm_performance` | Per-variable performance aggregates (n, mean, median, sd, min, max) for a single germplasm across its studies. |
59
- | `brapi_export_genotype_matrix` | Export genotype calls for a variant set as a germplasm × variant matrix, plus VCF-lite / PLINK serialization. |
60
- | `brapi_submit_observations` | _Opt-in._ Two-phase observation write — `preview` validates, `apply` confirms and writes. |
61
- | `brapi_raw_get` | Passthrough to any BrAPI `GET /{path}` endpoint not covered by a curated tool. |
62
- | `brapi_raw_search` | Passthrough to any `POST /search/{noun}` endpoint, with async polling handled transparently. |
38
+ | `brapi_connect` | Authenticate to a BrAPI v2 server, register it under an alias, and return the orientation envelope |
39
+ | `brapi_server_info` | Re-fetch the orientation envelope for a registered alias, optionally refreshing capabilities |
40
+ | `brapi_describe_filters` | List valid filter names for an endpoint, for use in any finder's `extraFilters` |
41
+ | `brapi_find_studies` | Find studies by crop, trial type, season, location, or program |
42
+ | `brapi_get_study` | Fetch a study with program, trial, and location resolved, plus companion counts |
43
+ | `brapi_find_germplasm` | Find germplasm by name, synonym, accession, PUI, crop, or free text |
44
+ | `brapi_get_germplasm` | Fetch a germplasm with attributes, direct parents, and companion counts |
45
+ | `brapi_walk_pedigree` | Walk ancestry or descendancy as a deduplicated DAG with cycle detection |
46
+ | `brapi_find_variables` | Find observation variables by name, trait class, or ontology term, with free-text ranking |
47
+ | `brapi_find_observations` | Pull observation records by study, germplasm, variable, season, or unit |
48
+ | `brapi_find_images` | Find image metadata by unit, observation, study, ontology term, or MIME type |
49
+ | `brapi_get_image` | Fetch up to 5 images inline as image content blocks |
50
+ | `brapi_find_locations` | Find research stations by country, type, abbreviation, or bounding box |
51
+ | `brapi_find_variants` | Find variants by variant set, reference, or genomic region |
52
+ | `brapi_find_genotype_calls` | Pull genotype calls through async search, bounded by a deployment pull ceiling |
53
+ | `brapi_dataframe_describe` | List dataframes, or describe one with columns, row count, and provenance |
54
+ | `brapi_dataframe_query` | Run read-only SQL across dataframes |
55
+ | `brapi_dataframe_drop` | _Opt-in._ Drop a dataframe by name |
56
+ | `brapi_dataframe_export` | _Opt-in, stdio-only._ Export a dataframe to disk as CSV, Parquet, or JSON |
57
+ | `brapi_build_phenotype_matrix` | Build a germplasm × trait matrix from one or more studies as a dataframe |
58
+ | `brapi_germplasm_performance` | Per-variable aggregates (n, mean, median, sd, min, max) for one germplasm across its studies |
59
+ | `brapi_export_genotype_matrix` | Pivot a variant set's calls into a germplasm × variant matrix, with VCF-lite or PLINK text |
60
+ | `brapi_submit_observations` | _Opt-in._ Two-phase observation write: `preview` validates, `apply` confirms and writes |
61
+ | `brapi_raw_get` | Passthrough to any `GET /{path}` endpoint no curated tool covers |
62
+ | `brapi_raw_search` | Passthrough to any `POST /search/{noun}` endpoint, with async polling handled |
63
63
 
64
64
  ### Resources
65
65
 
66
- URI-addressable mirrors of the curated tool surface for clients that prefer resources. All resources use the default connection — multi-server workflows route through tools.
67
-
68
66
  | Resource | Description |
69
67
  |:---|:---|
70
- | `brapi://server/info` | Orientation envelope for the default connection — mirrors `brapi_server_info`. |
71
- | `brapi://calls` | Raw capability profile (`/serverinfo` + `/calls`) for the default connection. |
72
- | `brapi://study/{studyDbId}` | Single study record with program/trial/location resolved — mirrors `brapi_get_study`. |
73
- | `brapi://germplasm/{germplasmDbId}` | Single germplasm record with attributes and parents — mirrors `brapi_get_germplasm`. |
74
- | `brapi://filters/{endpoint}` | Filter catalog for one endpoint — mirrors `brapi_describe_filters`. |
75
- | `brapi://variable/{observationVariableDbId}` | Single observation-variable record (trait, scale, method, ontology). |
68
+ | `brapi://server/info` | Orientation envelope for the default connection |
69
+ | `brapi://calls` | Raw capability profile (`/serverinfo` + `/calls`) for the default connection |
70
+ | `brapi://study/{studyDbId}` | One study with program, trial, and location resolved |
71
+ | `brapi://germplasm/{germplasmDbId}` | One germplasm with attributes and parents |
72
+ | `brapi://filters/{endpoint}` | Filter catalog for one endpoint |
73
+ | `brapi://variable/{observationVariableDbId}` | One observation variable (trait, scale, method, ontology) |
74
+
75
+ Every resource reads the `default` connection and mirrors a tool; tool-only clients and multi-server workflows use the tools.
76
76
 
77
77
  ### Prompts
78
78
 
79
79
  | Prompt | Description |
80
80
  |:---|:---|
81
- | `brapi_eda_study` | EDA playbook for one study — orient, variables, coverage, missing data, outliers, pedigree, then a structured report. Args: `studyDbId`, optional `alias`. |
82
- | `brapi_meta_analysis` | Cross-study meta-analysis for a germplasm × trait combination — resolve trait, discover studies, harmonize scales, summarize within and across studies. Args: `germplasmDbIds` (CSV), `traitName`, optional `alias`. |
81
+ | `brapi_eda_study` | Exploratory-data-analysis playbook for one study, ending in a structured report |
82
+ | `brapi_meta_analysis` | Cross-study meta-analysis playbook for a germplasm × trait combination |
83
83
 
84
84
  ## Capability reference
85
85
 
86
86
  ### `brapi_connect` <sub>tool</sub>
87
87
 
88
- - `baseUrl` and `auth` are optional — when omitted, resolved from `BRAPI_<ALIAS>_*` env vars, then the built-in registry, then `BRAPI_DEFAULT_*`, so credentials never enter the LLM context
89
- - `alias` (default `default`, pattern `^[a-zA-Z0-9_-]+$`) registers multiple concurrent connections in one session
90
- - Auth is a tagged union: `none` / `bearer` / `api_key` / `sgn` (Breedbase `/token` exchange) / `oauth2` (client-credentials)
91
- - Typed errors: `auth_token_exchange_failed`, `auth_no_access_token`
92
- - Returns the full orientation envelope (identity, capabilities, content counts, attribution) — one call fully orients the agent; re-fetch on demand via `brapi_server_info`
88
+ - `baseUrl` and `auth` are optional: omitted values come from `BRAPI_<ALIAS>_*` env vars, the built-in aliases, then `BRAPI_DEFAULT_*` (see [Per-alias credentials](#per-alias-credentials)); `alias` (default `default`, pattern `^[a-zA-Z0-9_-]+$`) keeps several servers registered at once; `auth.mode` is `none`, `bearer`, `api_key`, `sgn` (Breedbase `/token` exchange), or `oauth2` (client credentials)
89
+ - Returns the orientation envelope: `server` identity, `auth` summary, `capabilities` (`supported`, `notableGaps`), active `dialect`, `content` counts, `attribution` for built-in servers, and `nextToolSuggestions` naming the entry-point finders this server can serve
90
+ - Typed errors: `auth_session_required`, `auth_base_url_mismatch`, `alias_base_url_unset`, `auth_token_exchange_failed`, `auth_no_access_token`, `upstream_unauthorized`, `upstream_forbidden`; a failed connect leaves any earlier registration under the alias intact
93
91
 
94
92
  ---
95
93
 
96
94
  ### `brapi_server_info` <sub>tool</sub>
97
95
 
98
- - `alias` optional (defaults to the connection registered under `default`); `forceRefresh` (default `false`) bypasses the cached capability profile
99
- - Typed error: `unknown_alias`
100
- - Returns the same orientation envelope shape as `brapi_connect`
96
+ - `alias` optional; `forceRefresh` (default `false`) refetches the capability profile instead of reading the cache
97
+ - Returns the same orientation envelope as `brapi_connect`
101
98
 
102
99
  ---
103
100
 
104
101
  ### `brapi_describe_filters` <sub>tool</sub>
105
102
 
106
- - `endpoint` required — one of `studies`, `germplasm`, `observations`, `variables`, `images`, `variants`, `locations`
107
- - Each entry carries `name`, `type` (`string` / `integer` / `number` / `boolean` / `date` / `string[]` / `integer[]`), `description`, and an example value
108
- - Typed error: `unknown_endpoint` (response carries `availableEndpoints` as recovery data)
109
- - Catalog reflects the BrAPI v2.1 spec; individual servers may implement subsets
103
+ - `endpoint` is one of `studies`, `germplasm`, `observations`, `variables`, `images`, `variants`, `locations`; `unknown_endpoint` carries `availableEndpoints`
104
+ - Each filter has `name`, `type`, `description`, and `example`; the catalog follows the v2.1 spec, and individual servers may honor a subset
110
105
 
111
106
  ---
112
107
 
113
108
  ### `brapi_find_studies` <sub>tool</sub>
114
109
 
115
- - Filters: `crop`, `trialTypes`, `seasons`, `locations`, `programs`, `trials`, `studyNames`, `active`, plus `extraFilters` passthrough
116
- - `loadLimit` caps in-context rows; beyond it the full result set materializes as a canvas dataframe (query with `brapi_dataframe_query`)
117
- - `distributions` cover `programName`, `studyType`, `seasons`, `locationName`, `commonCropName`
118
- - Typed errors: `unknown_alias`, `all_filters_dropped` (every supplied filter was unsupported by the active dialect)
119
- - Response enrichment: `totalCount`, `returnedCount`, `appliedFilters`, `refinementHint`, `notice`, `warnings`
110
+ - Filters: `crop`, `trialTypes`, `seasons`, `locations`, `programs`, `trials`, `studyNames`, `active`
111
+ - `distributions` over `programName`, `studyType`, `seasons`, `locationName`, `commonCropName`
120
112
 
121
113
  ---
122
114
 
123
115
  ### `brapi_get_study` <sub>tool</sub>
124
116
 
125
- - `studyDbId` required; resolves `program`, `trial`, and `location` FKs inline
126
- - Companion counts: `observationCount`, `observationUnitCount`, `variableCount` — omitted (with a warning) rather than reported as a server-wide total when the upstream can't scope a count to the study
127
- - Typed errors: `unknown_alias`, `study_not_found`
117
+ - `studyDbId` required; resolves `program`, `trial`, and `location` inline; `study_not_found` when the upstream has no such study
118
+ - Companion counts `observationCount`, `observationUnitCount`, `variableCount`; a count the server can't scope to the study is omitted with a warning, never reported as the server-wide total
128
119
 
129
120
  ---
130
121
 
131
122
  ### `brapi_find_germplasm` <sub>tool</sub>
132
123
 
133
- - Filters: `names`, `germplasmDbIds`, `germplasmPUIs`, `accessionNumbers`, `crops`, `synonyms`, `collections`, `genus`, `species`, plus `extraFilters`
134
- - `text` is a client-side substring match against `germplasmName`, `accessionNumber`, `defaultDisplayName`, and registered synonyms — combine with a server-side filter to narrow the upstream pull first
135
- - `loadLimit` caps in-context rows; beyond it the full result set materializes as a canvas dataframe
136
- - `distributions` cover `commonCropName`, `genus`, `species`, `collection`, `countryOfOriginCode`
137
- - Typed errors: `unknown_alias`, `all_filters_dropped`
124
+ - Filters: `names`, `germplasmDbIds`, `germplasmPUIs`, `accessionNumbers`, `crops`, `synonyms`, `collections`, `genus`, `species`; `text` is a client-side substring match on name, accession, display name, and synonyms that drops non-matching rows, so pair it with a server-side filter
125
+ - `distributions` over `commonCropName`, `genus`, `species`, `collection`, `countryOfOriginCode`
138
126
 
139
127
  ---
140
128
 
141
129
  ### `brapi_get_germplasm` <sub>tool</sub>
142
130
 
143
- - `germplasmDbId` required; returns attributes (`/germplasm/{id}/attributes`) and direct parents (`/germplasm/{id}/pedigree`)
144
- - Companions: `studyCount`, `directParentCount`, `directDescendantCount` (from `/germplasm/{id}/progeny`) — signals for pedigree depth and observation coverage
145
- - Typed errors: `unknown_alias`, `germplasm_not_found`
131
+ - `germplasmDbId` required; returns `attributes` and direct `parents`; `germplasm_not_found` when the upstream has no such germplasm
132
+ - Companion counts `studyCount`, `directParentCount`, `directDescendantCount`
146
133
 
147
134
  ---
148
135
 
149
136
  ### `brapi_walk_pedigree` <sub>tool</sub>
150
137
 
151
- - 1–20 root `germplasmDbIds`, walked concurrently; `direction` is `ancestors` (default), `descendants`, or `both`; `maxDepth` 1–10 (default 3)
152
- - Deduplicates nodes and breaks cycles; a 1,000-node safety cap sets `truncated` when reached
153
- - Traversal stats: `depthReached`, `rootCount`, `leafCount`, `cycleCount`, `deadEndCount`
154
- - `loadLimit` bounds the inline `nodes`/`edges` preview; beyond it both sets spill to JOINable canvas dataframes (`nodesDataframe`, `edgesDataframe`)
155
- - Typed error: `unknown_alias`
138
+ - 1–20 root `germplasmDbIds`; `direction` is `ancestors` (default), `descendants`, or `both`; `maxDepth` 1–10 (default 3); the walk stops at 1,000 nodes and sets `truncated`
139
+ - Deduplicated `nodes` and `edges` with `depthReached`, `rootCount`, `leafCount`, `cycleCount`, `deadEndCount`; past `loadLimit`, both sets spill to `nodesDataframe` and `edgesDataframe`
156
140
 
157
141
  ---
158
142
 
159
143
  ### `brapi_find_variables` <sub>tool</sub>
160
144
 
161
- - Filters: `variables`, `variableNames`, `variablePUIs`, `traitClasses`, `ontologies`, `studies`, `methods`, `scales`, `crop`, plus `extraFilters`
162
- - `text` ranks the full upstream union via `OntologyResolver` (PUI / name / synonym / trait-class match) and fills the in-context window with matches first, unmatched rows for context — unlike `brapi_find_germplasm.text`, unmatched rows aren't dropped
163
- - `ontologyCandidates` in the response carries the ranked matches with their match `source`
164
- - `loadLimit` caps in-context rows; beyond it the full result set materializes as a canvas dataframe
165
- - Typed errors: `unknown_alias`, `all_filters_dropped`
145
+ - Filters: `variables`, `variableNames`, `variablePUIs`, `traitClasses`, `ontologies`, `studies`, `methods`, `scales`, `crop`
146
+ - `text` ranks the full result set and moves matches to the top without dropping the rest; `ontologyCandidates` lists the ranked matches, each with `source` (`puiMatch`, `nameMatch`, `synonymMatch`, `traitClassMatch`)
147
+ - `distributions` over `ontologyDbId`, `traitClass`, `scaleName`
166
148
 
167
149
  ---
168
150
 
169
151
  ### `brapi_find_observations` <sub>tool</sub>
170
152
 
171
- - Filters: `studies`, `germplasm`, `variables`, `observationUnits`, `observations`, `seasons`, `programs`, `trials`, `observationLevels`, `timestampFrom`/`timestampTo`, plus `extraFilters`
172
- - `loadLimit` caps in-context rows; beyond it the full result set materializes as a canvas dataframe
173
- - `distributions` cover `observationVariableName`, `studyName`, `germplasmName`, `observationLevel`, `season`
174
- - Typed errors: `unknown_alias`, `all_filters_dropped`
153
+ - Filters: `studies`, `germplasm`, `variables`, `observationUnits`, `observations`, `seasons`, `programs`, `trials`, `observationLevels`, `timestampFrom` / `timestampTo`
154
+ - `distributions` over `observationVariableName`, `studyName`, `germplasmName`, `observationLevel`, `season`
175
155
 
176
156
  ---
177
157
 
178
158
  ### `brapi_find_images` <sub>tool</sub>
179
159
 
180
- - Filters: `images`, `observationUnits`, `observations`, `studies`, `imageFileNames`, `mimeTypes`, `descriptiveOntologyTerms`, plus `extraFilters`
181
- - Metadata only — fetch bytes via `brapi_get_image`
182
- - `loadLimit` caps in-context rows; beyond it the full result set materializes as a canvas dataframe
183
- - Typed errors: `unknown_alias`, `all_filters_dropped`
160
+ - Filters: `images`, `observationUnits`, `observations`, `studies`, `imageFileNames`, `mimeTypes`, `descriptiveOntologyTerms`; returns metadata only, with bytes via `brapi_get_image`
161
+ - `distributions` over `mimeType`, `studyName`, `observationUnitName`, `descriptiveOntologyTerms`
184
162
 
185
163
  ---
186
164
 
187
165
  ### `brapi_get_image` <sub>tool</sub>
188
166
 
189
- - 1–5 `imageDbIds` per call
190
- - Prefers `/images/{id}/imagecontent`; falls back to the metadata `imageURL` — `source` on each payload names which path served it
191
- - Per-image `errors[]` for failed fetches and `warnings[]` for loaded-but-suspect content (e.g. a non-image MIME from the `imageURL` fallback) — a partial batch never fails as a whole
192
- - Typed errors: `unknown_alias`, `images_unsupported` (server doesn't advertise `/images`)
167
+ - 1–5 `imageDbIds` per call, up to 20 MB each; `images_unsupported` when the server doesn't advertise `/images`
168
+ - Each image's `source` is `imagecontent` or the `imageURL` fallback; failed fetches land in per-image `errors[]` and suspect payloads (a non-image MIME type) in `warnings[]`, so a partial batch still returns
193
169
 
194
170
  ---
195
171
 
196
172
  ### `brapi_find_locations` <sub>tool</sub>
197
173
 
198
- - Filters: `locations`, `locationNames`, `countryCodes` (ISO 3166-1 alpha-3), `countryNames` (free-form English, resolved client-side to alpha-3), `locationTypes`, `abbreviations`, plus `extraFilters`
199
- - Optional post-fetch `bbox` (`minLat`/`maxLat`/`minLon`/`maxLon`, all four required to activate); retries once with axes swapped when the spec-correct `[lon, lat]` reading yields zero matches on a server that stores `[lat, lon]`, and reports `coordinateAxisOrder: "swapped"`
200
- - `loadLimit` caps in-context rows; beyond it the full result set materializes as a canvas dataframe
201
- - Typed errors: `unknown_alias`, `all_filters_dropped`
174
+ - Filters: `locations`, `locationNames`, `countryCodes` (ISO 3166-1 alpha-3), `countryNames` (English names resolved to alpha-3), `locationTypes`, `abbreviations`; optional `bbox` needs all four of `minLat`, `maxLat`, `minLon`, `maxLon` and applies after the fetch
175
+ - `distributions` over `countryCode`, `locationType`; `coordinateAxisOrder: "swapped"` reports a server that stores coordinates as `[lat, lon]`
202
176
 
203
177
  ---
204
178
 
205
179
  ### `brapi_find_variants` <sub>tool</sub>
206
180
 
207
- - Filters: `variantSets`, `variants`, `references`, `referenceName` + `start`/`end` (1-based inclusive/exclusive genomic region), plus `extraFilters`
208
- - `loadLimit` caps in-context rows; beyond it the full result set materializes as a canvas dataframe
209
- - `distributions` cover `variantType`, `referenceName`, `variantSetDbId`
210
- - Typed errors: `unknown_alias`, `all_filters_dropped`
181
+ - Filters: `variantSets`, `variants`, `references`, and a genomic region of `referenceName` + `start` (inclusive) / `end` (exclusive), 1-based
182
+ - `distributions` over `variantType`, `referenceName`, `variantSetDbId`
211
183
 
212
184
  ---
213
185
 
214
186
  ### `brapi_find_genotype_calls` <sub>tool</sub>
215
187
 
216
- - Requires at least one of `variantSetDbId`, `variantSetDbIds`, `germplasmDbIds`, `callSetDbIds`, or `variantDbIds` — unfiltered pulls are rejected
217
- - Upstream pull bounded by `BRAPI_GENOTYPE_CALLS_MAX_PULL` (default 100,000, max 500,000) via the async `POST /search/calls` → `GET /search/calls/{id}` pattern
218
- - `loadLimit` bounds the inline preview; the full collected set materializes as a dataframe when it exceeds `loadLimit`
219
- - Typed errors: `unknown_alias`, `no_filters`, `search_endpoint_disabled` (dialect marks this server's search route as known-dead)
188
+ - Needs at least one of `variantSetDbId`, `variantSetDbIds`, `germplasmDbIds`, `callSetDbIds`, `variantDbIds` (`no_filters` otherwise); optional `callFormat` (`VCF`, `FLAPJACK`, `DARTSEQ`, `JSON`)
189
+ - `distributions` over `callSetName`, `variantName`, `variantSetDbId`, plus the server's `callFormatting`; `search_endpoint_disabled` when the active dialect marks `POST /search/calls` as dead
190
+ - The upstream pull stops at `BRAPI_GENOTYPE_CALLS_MAX_PULL` (default 100,000) and sets `truncated`; `loadLimit` bounds only the inline preview
220
191
 
221
192
  ---
222
193
 
223
194
  ### `brapi_dataframe_describe` <sub>tool</sub>
224
195
 
225
- - `dataframe` optional — omit to list all, or name one for full detail (columns, row count, provenance)
226
- - Provenance (originating tool, `baseUrl`, query, expiry) is present only for auto-registered `df_*` dataframes, not user-derived ones from `registerAs`
227
- - Typed error: `list_all_disabled_on_shared_http` — listing without a name is refused on a shared HTTP deployment without per-caller auth, since every caller shares one tenant workspace
196
+ - `dataframe` optional: omit to list every dataframe, or name one for columns, row count, and provenance (originating tool, `baseUrl`, query, expiry), which only auto-registered `df_*` tables carry
197
+ - Listing without a name fails with `list_all_disabled_on_shared_http` on an HTTP deployment where every caller shares the `default` tenant
228
198
 
229
199
  ---
230
200
 
231
201
  ### `brapi_dataframe_query` <sub>tool</sub>
232
202
 
233
- - `sql` must be a single `SELECT` — writes, DDL, `COPY`, `PRAGMA`, `ATTACH`, and file reads are rejected at a three-layer gate (single statement → SELECT only → plan-walk allowlist); system-catalog reads (`information_schema`, `pg_catalog`, `sqlite_master`, `duckdb_*`) are denied separately
234
- - `LIMIT`/`OFFSET` is the paging idiom; projection and aggregation (`COUNT`, `GROUP BY`, `AVG`) summarize without materializing every row
235
- - `registerAs` (letters/digits/underscore, ≤63 chars) persists the result as a new dataframe; `preview` (≤1000) and `rowLimit` bound what's returned inline
236
- - Typed error: `sql_rejected` (carries the granular gate reason on `data.gateReason`)
237
- - Response enrichment: `truncated`, `shown`, `cap`, `notice`
203
+ - `sql` is a single `SELECT`; writes, DDL, `COPY`, `PRAGMA`, `ATTACH`, file reads, and system-catalog reads fail as `sql_rejected`, with the specific reason in `data.gateReason`
204
+ - Returns `rowCount`, typed `columns`, and `rows` bounded by `preview` (≤1,000), `rowLimit`, and `BRAPI_CANVAS_MAX_ROWS`; `truncated`, `shown`, `cap`, and `notice` disclose the cut
205
+ - `registerAs` (identifier, ≤63 characters) saves the full result as a new dataframe for chaining
238
206
 
239
207
  ---
240
208
 
241
209
  ### `brapi_dataframe_drop` <sub>tool</sub>
242
210
 
243
- - _Opt-in via `BRAPI_CANVAS_DROP_ENABLED=true`_ — omitted from `tools/list` otherwise
244
- - Idempotent: returns `dropped: false` (not an error) for an unknown name
245
- - Dataframes also expire via TTL when left unmanaged, so explicit drop is only needed to free workspace memory immediately
211
+ - `dataframe` required; returns `dropped: false`, not an error, for an unknown name
212
+ - Registered only when `BRAPI_CANVAS_DROP_ENABLED=true`
246
213
 
247
214
  ---
248
215
 
249
216
  ### `brapi_dataframe_export` <sub>tool</sub>
250
217
 
251
- - _Opt-in via `BRAPI_EXPORT_DIR`, stdio-only_ — omitted from `tools/list` under HTTP transport or when unset
252
- - `format` is `csv`, `parquet`, or `json`; optional `columns` (thin projection) or `sql` (full SELECT, mutually exclusive with `columns`) materializes a temporary derived table first
253
- - `filename` rejects path separators and `..` segments; omit for a timestamp-suffixed default
218
+ - `format` is `csv`, `parquet`, or `json`; optional `columns` or `sql` (mutually exclusive) and `filename` (no path separators or `..`; omit for a timestamp-suffixed default); returns the absolute `path`, `sizeBytes`, and `rowCount`
254
219
  - Typed errors: `export_dir_unset`, `dataframe_not_found`, `invalid_filename`, `mutually_exclusive_projection`
220
+ - Registered only over stdio with `BRAPI_EXPORT_DIR` set
255
221
 
256
222
  ---
257
223
 
258
224
  ### `brapi_build_phenotype_matrix` <sub>tool</sub>
259
225
 
260
- - `studies` required (≥1) — study-anchored to avoid full-table scans; optional `variables`/`germplasm` subsets
261
- - `shape`: `wide` (one row per germplasm, one column per variable) or `long` (one row per observation); `aggregate`: `mean` (default), `median`, `first`, or `all` (forces long form even when `shape:"wide"`)
262
- - Wide-matrix column names are SQL-safe identifiers derived from `observationVariableDbId`; `variableLegend` maps them back to display names
263
- - Typed errors: `unknown_alias`, `all_filters_dropped`, `no_observation_path` (neither `/observations` nor `/observationunits` returned data)
264
- - Response enrichment: `truncated`, `shown`, `cap`, `notice`
226
+ - `studies` required (≥1), optional `variables` / `germplasm` subsets; `shape` `wide` (default) or `long`; `aggregate` `mean` (default), `median`, `first`, or `all` (always long form); `loadLimit` caps observations per study
227
+ - Returns the matrix as a dataframe plus `observationCount`, `germplasmCount`, `variableCount`, and `variableLegend` mapping SQL-safe column names to variable names; `truncated` / `cap` flag a study that hit `loadLimit`; `no_observation_path` when neither `/observations` nor `/observationunits` returns data
265
228
 
266
229
  ---
267
230
 
268
231
  ### `brapi_germplasm_performance` <sub>tool</sub>
269
232
 
270
- - `germplasmDbId` required; discovers the germplasm's studies automatically (capped at 200) unless an explicit `studyDbIds` set is supplied, which skips discovery entirely
271
- - Per-variable aggregates: `n`, `mean`, `median`, `sd` (omitted when n < 2 or non-numeric), `min`/`max`, `studyCount`, `studyDbIds`, `seasons`
272
- - Typed errors: `unknown_alias`, `germplasm_not_found`
233
+ - `germplasmDbId` required; discovers its studies (up to 200) unless `studyDbIds` is supplied; optional `variables` subset; `germplasm_not_found` when the upstream has no such germplasm
234
+ - `perVariable` rows carry `n`, `mean`, `median`, `sd` (omitted when n < 2 or non-numeric), `min`, `max`, `studyCount`, `studyDbIds`, and `seasons`
273
235
 
274
236
  ---
275
237
 
276
238
  ### `brapi_export_genotype_matrix` <sub>tool</sub>
277
239
 
278
- - `variantSetDbId` required; `format` is `matrix-json` (dataframe only), `vcf-lite` (VCF-subset text in `vcf`, plus dataframe), or `plink` (`.ped`/`.map` text, plus dataframe)
279
- - `maxCalls`/`maxColumns` can only lower the deployment ceilings (`BRAPI_GENOTYPE_CALLS_MAX_PULL`, `BRAPI_GENOTYPE_MATRIX_MAX_COLUMNS`), never raise them
280
- - `variantColumnLegend` maps SQL-safe column names back to original variant IDs; `truncated` names which ceiling fired when the matrix is incomplete
281
- - Typed errors: `unknown_alias`, `no_filters`, `search_endpoint_disabled`
240
+ - `variantSetDbId` required (`no_filters` otherwise), optional `germplasmDbIds`; `format` is `matrix-json`, `vcf-lite` (adds `vcf` text), or `plink` (adds `ped` / `map` text), and every format registers the germplasm × variant dataframe
241
+ - `variantColumnLegend` maps SQL-safe column names back to variant IDs; `search_endpoint_disabled` when the active dialect marks `POST /search/calls` as dead
242
+ - `maxCalls` / `maxColumns` can lower `BRAPI_GENOTYPE_CALLS_MAX_PULL` / `BRAPI_GENOTYPE_MATRIX_MAX_COLUMNS` but never raise them; `truncated` means a ceiling fired, and `warnings` names which
282
243
 
283
244
  ---
284
245
 
285
246
  ### `brapi_submit_observations` <sub>tool</sub>
286
247
 
287
- - `studyDbId` required; 1–5,000 observation rows; `observationDbId` presence on a row routes it to `PUT`, absence to `POST`
288
- - `mode: "preview"` (default) validates only and returns a POST/PUT routing breakdown; `mode: "apply"` asks the caller to confirm via a multi-round-trip input request, then writes and verifies post-state with a cheap count probe
289
- - `force: true` skips the confirmation round — only for out-of-band-authorized writes
290
- - Additive only — no observation is ever destroyed
291
- - Requires `BRAPI_ENABLE_WRITES=true` to register; scoped to `brapi:write:observations`
292
- - Typed errors: `unknown_alias`, `observations_unsupported`, `study_not_found`, `post_unsupported`, `put_unsupported`, `user_declined`
248
+ - `studyDbId` plus 1–5,000 `observations`; a row with `observationDbId` updates via `PUT`, one without creates via `POST`, and nothing is deleted
249
+ - `mode: "preview"` (default) returns `valid`, `invalid`, `routing` counts, and `perRowWarnings` without writing; `mode: "apply"` asks the caller to confirm (`force: true` skips it), writes, and returns `posted`, `updated`, and `studyObservationCount`; failures are `observations_unsupported`, `study_not_found`, `post_unsupported`, `put_unsupported`, and `user_declined`
250
+ - Registered only when `BRAPI_ENABLE_WRITES=true`; requires the `brapi:write:observations` scope
293
251
 
294
252
  ---
295
253
 
296
254
  ### `brapi_raw_get` <sub>tool</sub>
297
255
 
298
- - `path` (relative BrAPI route, e.g. `/samples`) + optional `params`; last-resort escape hatch for endpoints no curated tool covers
299
- - Emits a `suggestion` when a curated tool exists for the same endpoint
300
- - Spills to a canvas dataframe when the upstream advertises more rows than `loadLimit` and the result is a list shape; skipped when the caller drives paging via `params.page`/`params.pageSize`
301
- - Typed errors: `unknown_alias`, `cross_origin_path` (a full URL was passed instead of a relative route)
256
+ - `path` is a relative route such as `/samples` (a full URL fails as `cross_origin_path`); optional `params` and `loadLimit`
257
+ - Returns the raw envelope (`url`, `metadata`, `result`) plus a `suggestion` when a curated tool covers the endpoint; list results past `loadLimit` spill to a dataframe unless `params.page` / `params.pageSize` drive paging
302
258
 
303
259
  ---
304
260
 
305
261
  ### `brapi_raw_search` <sub>tool</sub>
306
262
 
307
- - `noun` (e.g. `observations`, `calls`, `germplasm`) + `body` posted verbatim to `POST /search/{noun}`; async polling resolved transparently, `kind` reports `sync` or `async`
308
- - Emits a `suggestion` when a curated tool covers the same noun
309
- - Same spillover behavior as `brapi_raw_get`
310
- - Typed errors: `unknown_alias`, `search_endpoint_disabled`
263
+ - `noun` (e.g. `observations`, `calls`, `germplasm`) and a `body` posted verbatim to `POST /search/{noun}`; async searches are polled to completion
264
+ - Returns `kind` (`sync` or `async`), `searchResultsDbId`, `result`, and a `suggestion`; spills like `brapi_raw_get` unless `body.page` / `body.pageSize` is set; `search_endpoint_disabled` when the active dialect marks the route as dead
311
265
 
312
266
  ---
313
267
 
314
268
  ### `brapi://server/info` <sub>resource</sub>
315
269
 
316
- - No parameters — reads the cached capability profile for the `default` connection
317
- - Typed error: `unknown_alias`
270
+ - Orientation envelope for the `default` connection as `application/json`
271
+ - Same payload as `brapi_server_info` called with no arguments
318
272
 
319
273
  ---
320
274
 
321
275
  ### `brapi://calls` <sub>resource</sub>
322
276
 
323
- - No parameters — raw `/serverinfo` + `/calls` profile (server identity, crops, supported services) for the `default` connection
324
- - Typed error: `unknown_alias`
277
+ - Capability profile for the `default` connection: supported services with their HTTP methods and versions, plus crops
278
+ - Reflects what `/serverinfo` + `/calls` returned at the last load
325
279
 
326
280
  ---
327
281
 
328
282
  ### `brapi://study/{studyDbId}` <sub>resource</sub>
329
283
 
330
- - Same payload as `brapi_get_study`, addressed by URI on the default connection
331
- - Typed errors: `unknown_alias`, `study_not_found`
284
+ - Same payload as `brapi_get_study` on the `default` connection
285
+ - `study_not_found` when the upstream has no such study
332
286
 
333
287
  ---
334
288
 
335
289
  ### `brapi://germplasm/{germplasmDbId}` <sub>resource</sub>
336
290
 
337
- - Same payload as `brapi_get_germplasm`, addressed by URI on the default connection
338
- - Typed errors: `unknown_alias`, `germplasm_not_found`
291
+ - Same payload as `brapi_get_germplasm` on the `default` connection
292
+ - `germplasm_not_found` when the upstream has no such germplasm
339
293
 
340
294
  ---
341
295
 
342
296
  ### `brapi://filters/{endpoint}` <sub>resource</sub>
343
297
 
344
- - Same payload as `brapi_describe_filters`; listing the resource collection returns one entry per supported endpoint
345
- - Typed error: `unknown_endpoint`
298
+ - Same payload as `brapi_describe_filters`; `unknown_endpoint` for an endpoint outside the catalog
299
+ - Listing `brapi://filters` returns one resource per endpoint
346
300
 
347
301
  ---
348
302
 
349
303
  ### `brapi://variable/{observationVariableDbId}` <sub>resource</sub>
350
304
 
351
- - Canonical `/variables/{id}` record (trait, scale, method, ontology) on the default connection — the single-record counterpart to `brapi_find_variables`
352
- - Typed errors: `unknown_alias`, `variable_not_found`
305
+ - The `/variables/{id}` record (trait, scale, method, ontology) on the `default` connection
306
+ - `variable_not_found` when the upstream has no such variable
353
307
 
354
308
  ---
355
309
 
356
310
  ### `brapi_eda_study` <sub>prompt</sub>
357
311
 
358
312
  - Arguments: `studyDbId` required; `alias` optional
359
- - Six-step playbook — orient via `brapi_get_study`, enumerate variables, pull observation coverage, quantify missing data, flag numeric outliers (IQR), and an optional pedigree walk on the top-observed germplasm
360
- - Ends in a structured markdown report with a recommended-next-steps section
313
+ - Returns one user message: a six-step playbook (orient, variables, coverage, missing data, IQR outliers, optional pedigree walk) ending in a markdown report with recommended next steps
361
314
 
362
315
  ---
363
316
 
364
317
  ### `brapi_meta_analysis` <sub>prompt</sub>
365
318
 
366
- - Arguments: `germplasmDbIds` (comma-separated) and `traitName` required; `alias` optional (run once per alias for multi-server analyses)
367
- - Seven-step playbook — resolve the trait to one or more observation variables, discover contributing studies, harmonize units/scales/methods across studies, then per-germplasm × per-study and across-study summary statistics
368
- - Ends in a markdown report that cites every dataframe handle or filter map used, for reproducibility
319
+ - Arguments: `germplasmDbIds` (comma-separated) and `traitName` required; `alias` optional, run once per alias for multi-server analyses
320
+ - Returns one user message: a seven-step playbook (resolve the trait, discover studies, build the observation table, harmonize scales, summarize per study and across studies, optional pedigree walk) ending in a report that cites every dataframe handle or filter map used
369
321
 
370
322
  ## Features
371
323
 
@@ -373,22 +325,22 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): s
373
325
 
374
326
  BrAPI-specific:
375
327
 
376
- - Dataframe spillover — `find_*` tools cap in-context rows at `loadLimit` and materialize larger unions (up to 50,000 rows) as DuckDB-backed `df_<uuid>` canvas dataframes, queryable via `brapi_dataframe_query`
377
- - Dialect adaptation — five per-server-family adapters (`spec` / `brapi-test` / `breedbase` / `cassavabase` / `bms`) translate v2.1 plural filter keys to the singular form each family honors, drop known-broken filters, and escalate to `POST /search/{noun}` when `GET` would silently downcast
378
- - Multi-server session with a built-in known-server registry — `ServerRegistry` holds live connections under named aliases; six public Breedbase/T3 endpoints resolve out-of-the-box with no env vars
379
- - Capability-aware, rate-limited calls — `CapabilityRegistry` caches `/serverinfo` and guards every call against unsupported endpoints; a per-connection concurrency cap and exponential-backoff retry cover 429/5xx
380
- - Tagged-union auth (`none` / `bearer` / `api_key` / `sgn` session-token exchange / `oauth2` client-credentials), resolved per alias from env vars so credentials never enter the LLM context
328
+ - Shared finder contract: the seven filter finders (studies, germplasm, variables, observations, images, locations, variants) take `alias`, `loadLimit`, and `extraFilters` (keys from `brapi_describe_filters`), and return `results`, `hasMore`, `distributions`, and an optional `dataframe` handle
329
+ - Dataframe spillover: past `loadLimit`, finders page the rest of the result (up to 50 pages and 50,000 rows) into a DuckDB `df_<uuid>` table and return its handle
330
+ - Dialect adapters (`spec`, `brapi-test`, `breedbase`, `cassavabase`, `bms`), detected per connection, translate v2.1 plural filters into the form each server family honors, drop filters it ignores, and switch to `POST /search/{noun}` when a `GET` would narrow a multi-value filter; `BRAPI_<ALIAS>_DIALECT` pins one
331
+ - Several connections at once under named aliases, with three public Breedbase servers built in and credentials resolved per alias from env vars, so they stay out of the LLM context
332
+ - Capability-aware calls: each connection's `/serverinfo` + `/calls` profile is cached and checked before a tool calls an endpoint; a per-connection concurrency cap and exponential-backoff retries cover 429/5xx
381
333
 
382
334
  Agent-friendly output:
383
335
 
384
- - Provenance on every dataframe — `brapi_dataframe_describe` reports the originating tool, `baseUrl`, and query for every auto-registered `df_<uuid>` table
385
- - Graceful partial failure — `brapi_get_image` returns per-item `errors[]` and `warnings[]` rows instead of failing the whole batch when some images can't be loaded
386
- - Discriminated output contracts — `brapi_submit_observations` returns a `mode`-discriminated union (`preview` / `apply`); `brapi_export_genotype_matrix` and the raw-passthrough tools carry typed `format`/`kind` fields callers branch on instead of parsing strings
387
- - Response-shaping guidance — `find_*` tools echo `appliedFilters`, a `refinementHint` when results are broad, and typed `notice`/`warnings` so agents can see exactly what was queried and why a response looks the way it does
336
+ - Typed failures: every connection-scoped tool and resource fails with `unknown_alias` until `brapi_connect` registers the alias, and a filter finder or `brapi_build_phenotype_matrix` fails with `all_filters_dropped` instead of widening to an unfiltered pull when the dialect drops every filter supplied
337
+ - Query echo on every finder: `totalCount`, `returnedCount`, the exact `appliedFilters` sent upstream, a `refinementHint` on broad results, an empty-result `notice`, and `warnings`
338
+ - Graceful partial failure: `brapi_get_image` returns per-image `errors[]` and `warnings[]` rows instead of failing the batch
339
+ - Discriminated outputs: `brapi_submit_observations` returns a `mode`-discriminated result (`preview` / `apply`), `brapi_raw_search` reports `kind`, and `brapi_get_image` reports each image's `source`
388
340
 
389
- ## Working with dataframes
341
+ ### Working with dataframes
390
342
 
391
- 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, … }`). Upstream column names that aren't SQL-safe identifiers are sanitized, and a `columnLegend` on the handle maps each renamed column back to its original key.
343
+ When a finder's upstream total exceeds `loadLimit`, the response carries a `dataframe` handle: `tableName`, `rowCount`, `columns`, `createdAt`, `expiresAt`, plus `truncated`, `maxRows`, and `totalCount` when a cap fired. Columns renamed to pass the SQL identifier check map back to their upstream keys in `columnLegend`.
392
344
 
393
345
  ```text
394
346
  1. brapi_find_observations { studies: ["s-422"] }
@@ -399,7 +351,7 @@ When a `find_*` tool's upstream total exceeds `loadLimit`, the full union materi
399
351
  → typed columns + bounded rows
400
352
  ```
401
353
 
402
- Dataframe names are capability tokens, not row-level ACLs — anyone holding the name within the same session or tenant bucket (see [Deployment shapes](#deployment-shapes)) can read its rows. They auto-expire via TTL (`BRAPI_DATASET_TTL_SECONDS`, default 24h); set `BRAPI_CANVAS_DROP_ENABLED=true` to expose `brapi_dataframe_drop` for explicit cleanup.
354
+ Dataframe names are capability tokens, not row-level ACLs: anyone holding a name in the same session or tenant bucket (see [Deployment shapes](#deployment-shapes)) can read its rows. Provenance lasts `BRAPI_DATASET_TTL_SECONDS` (default 24h); set `BRAPI_CANVAS_DROP_ENABLED=true` to expose `brapi_dataframe_drop` for explicit cleanup.
403
355
 
404
356
  ## Getting started
405
357
 
@@ -477,12 +429,12 @@ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
477
429
  # Server listens at http://localhost:3010/mcp
478
430
  ```
479
431
 
480
- 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, and API keys stay out of the LLM context — see [Per-alias credentials](#per-alias-credentials).
432
+ No env vars are required: the built-in aliases (`bti-cassava`, `bti-sweetpotato`, `bti-breedbase-demo`) connect as-is, and `brapi_connect` accepts any other BrAPI v2 URL at runtime. For servers that need a login, set credentials as env vars so passwords, tokens, and keys stay out of the LLM context (see [Per-alias credentials](#per-alias-credentials)).
481
433
 
482
434
  ### Prerequisites
483
435
 
484
436
  - [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
485
- - [`@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).
437
+ - [`@duckdb/node-api`](https://www.npmjs.com/package/@duckdb/node-api), installed as a regular dependency, with prebuilt native bindings for macOS, Linux (glibc and musl), and Windows on x64 and arm64. Cloudflare Workers is not supported.
486
438
 
487
439
  ### Installation
488
440
 
@@ -517,48 +469,54 @@ Every variable is optional.
517
469
 
518
470
  | Variable | Description | Default |
519
471
  |:---------|:------------|:--------|
520
- | `BRAPI_DEFAULT_BASE_URL` | Default BrAPI v2 base URL (e.g. `https://test-server.brapi.org/brapi/v2`). | — |
521
- | `BRAPI_DEFAULT_USERNAME` / `_PASSWORD` | SGN session-token auth for the default connection. | — |
522
- | `BRAPI_DEFAULT_OAUTH_CLIENT_ID` / `_OAUTH_CLIENT_SECRET` | OAuth2 client-credentials for the default connection. | — |
523
- | `BRAPI_DEFAULT_API_KEY` / `_API_KEY_HEADER` | Static API key for the default connection. | header `Authorization` |
524
- | `BRAPI_BUILTIN_ALIASES_DISABLED` | Comma-separated alias names (case-insensitive) to remove from the built-in registry. | — |
525
- | `BRAPI_LOAD_LIMIT` | In-context row cap returned by `find_*` tools before spilling to a canvas dataframe. | `1000` |
526
- | `BRAPI_PAGE_SIZE` | Upstream `pageSize` used during canvas spillover walks (decoupled from `BRAPI_LOAD_LIMIT`). Dataframe ceiling = `pageSize × 50`. | `1000` |
472
+ | `BRAPI_DEFAULT_BASE_URL` | Base URL for the `default` alias (e.g. `https://test-server.brapi.org/brapi/v2`). | — |
473
+ | `BRAPI_DEFAULT_*` credentials | One credential family for the `default` alias; see [Per-alias credentials](#per-alias-credentials). | — |
474
+ | `BRAPI_DEFAULT_API_KEY_HEADER` | API-key header for the `default` alias, and the fallback header for any `api_key` auth that names none. | `Authorization` |
475
+ | `BRAPI_BUILTIN_ALIASES_DISABLED` | Comma-separated built-in aliases to remove (case-insensitive). | — |
476
+ | `BRAPI_LOAD_LIMIT` | Default inline row cap for finders before spilling to a dataframe. | `1000` |
477
+ | `BRAPI_PAGE_SIZE` | Upstream `pageSize` for spillover page walks. The dataframe ceiling is `pageSize × 50`, capped at 50,000 rows. | `1000` |
527
478
  | `BRAPI_MAX_CONCURRENT_REQUESTS` | Per-connection concurrency cap. | `4` |
528
- | `BRAPI_RETRY_MAX_ATTEMPTS` / `BRAPI_RETRY_BASE_DELAY_MS` | Retry policy for 429/5xx with exponential backoff. | `3` / `500` |
479
+ | `BRAPI_RETRY_MAX_ATTEMPTS` / `BRAPI_RETRY_BASE_DELAY_MS` | Retries on 429/5xx and the exponential-backoff base delay. | `3` / `500` |
529
480
  | `BRAPI_REQUEST_TIMEOUT_MS` | Per-request HTTP timeout. | `30000` |
530
- | `BRAPI_COMPANION_TIMEOUT_MS` | Tighter timeout for non-critical companion enrichments (FK lookups, count probes); companions also bypass the retry budget. | `8000` |
531
- | `BRAPI_SEARCH_POLL_TIMEOUT_MS` / `_INTERVAL_MS` | Async `/search` polling budget + interval. | `60000` / `1000` |
532
- | `BRAPI_DATASET_TTL_SECONDS` | TTL for dataframe provenance metadata persisted alongside spilled rows. | `86400` |
533
- | `BRAPI_REFERENCE_CACHE_TTL_SECONDS` | TTL for programs / trials / locations / crops cache. | `3600` |
534
- | `BRAPI_ALLOW_PRIVATE_IPS` | Allow RFC 1918 / loopback targets. Dev-only. | `false` |
535
- | `BRAPI_ENABLE_WRITES` | **Feature flag.** Registers `brapi_submit_observations` when `true`. | `false` |
536
- | `BRAPI_GENOTYPE_CALLS_MAX_PULL` | Upstream row ceiling per `brapi_find_genotype_calls` invocation. Max `500000`. | `100000` |
537
- | `BRAPI_GENOTYPE_MATRIX_MAX_COLUMNS` | Distinct-variant column ceiling per `brapi_export_genotype_matrix` matrix — bounds the wide dataframe, the `variantColumnLegend`, and any VCF/PLINK text. Max `500000`. | `10000` |
538
- | `BRAPI_CANVAS_DROP_ENABLED` | **Feature flag.** Registers `brapi_dataframe_drop` when `true`; dataframes still expire via TTL when left unmanaged. | `false` |
539
- | `BRAPI_EXPORT_DIR` | **Feature flag.** Directory for `brapi_dataframe_export` output files — setting a path is the opt-in (no separate enable flag). Stdio-only; the tool stays disabled under HTTP transport regardless of this value. | — |
540
- | `BRAPI_CANVAS_MAX_ROWS` / `BRAPI_CANVAS_QUERY_TIMEOUT_MS` | Per-query response row cap and wall-clock timeout for `brapi_dataframe_query`. | `10000` / `30000` |
541
- | `BRAPI_SESSION_ISOLATION` | When `true`, scope connection state and the default canvas to `ctx.sessionId` (HTTP stateful/auto) so concurrent `MCP_AUTH_MODE=none` callers get isolated workspaces. Set `false` for the shared-workspace model. No effect on stdio. | `true` |
481
+ | `BRAPI_COMPANION_TIMEOUT_MS` | Timeout for non-critical enrichment calls (FK lookups, count probes), which also skip retries. | `8000` |
482
+ | `BRAPI_SEARCH_POLL_TIMEOUT_MS` / `BRAPI_SEARCH_POLL_INTERVAL_MS` | Async `/search` polling budget and interval. | `60000` / `1000` |
483
+ | `BRAPI_DATASET_TTL_SECONDS` | Lifetime of dataframe provenance (the handle's `expiresAt`). | `86400` |
484
+ | `BRAPI_REFERENCE_CACHE_TTL_SECONDS` | TTL for cached capability profiles and reference data (programs, trials, locations, crops). | `3600` |
485
+ | `BRAPI_ALLOW_PRIVATE_IPS` | Allow RFC 1918 / loopback targets. Dev only. | `false` |
486
+ | `BRAPI_SESSION_ISOLATION` | Scope connections and the default canvas to the MCP session when one exists; `false` shares them across the tenant. See [Deployment shapes](#deployment-shapes). | `true` |
487
+ | `BRAPI_ENABLE_WRITES` | **Feature flag.** Registers `brapi_submit_observations`. | `false` |
488
+ | `BRAPI_CANVAS_DROP_ENABLED` | **Feature flag.** Registers `brapi_dataframe_drop`. | `false` |
489
+ | `BRAPI_EXPORT_DIR` | **Feature flag.** Output directory for `brapi_dataframe_export`; setting it registers the tool, over stdio only. | — |
490
+ | `BRAPI_CANVAS_MAX_ROWS` / `BRAPI_CANVAS_QUERY_TIMEOUT_MS` | Response row cap and per-query timeout for `brapi_dataframe_query`. | `10000` / `30000` |
491
+ | `BRAPI_GENOTYPE_CALLS_MAX_PULL` | Upstream call ceiling per `brapi_find_genotype_calls` or `brapi_export_genotype_matrix` call. Max `500000`. | `100000` |
492
+ | `BRAPI_GENOTYPE_MATRIX_MAX_COLUMNS` | Variant-column ceiling per `brapi_export_genotype_matrix` matrix. Max `500000`. | `10000` |
542
493
  | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
543
- | `MCP_HTTP_PORT` | Port for HTTP server. | `3010` |
544
- | `MCP_SESSION_MODE` | HTTP session mode: `stateful`, `stateless`, or `auto` (resolves to `stateful`). This server pins `stateful` — apply-mode observation writes need a durable session to ask for confirmation, and per-session isolation keys off `ctx.sessionId`. | `stateful` |
545
- | `MCP_AUTH_MODE` | Auth mode: `none`, `jwt`, or `oauth`. | `none` |
546
- | `MCP_LOG_LEVEL` | Log level (RFC 5424). | `info` |
547
- | `STORAGE_PROVIDER_TYPE` | Storage backend. | `in-memory` |
548
- | `OTEL_ENABLED` | Enable [OpenTelemetry instrumentation](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry). | `false` |
494
+ | `MCP_HTTP_PORT` | HTTP server port. | `3010` |
495
+ | `MCP_SESSION_MODE` | HTTP session mode. This server requires `stateful` (apply-mode writes confirm over the session, and isolation keys off it); an HTTP start fails if it resolves to `stateless`. | `stateful` |
496
+ | `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth`. | `none` |
497
+ | `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.). | `info` |
498
+ | `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1`. | `in-memory` |
499
+ | `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry). | `false` |
549
500
 
550
- Per-alias overrides follow the `BRAPI_<ALIAS>_*` pattern. See [`.env.example`](./.env.example) for the full list of optional overrides.
501
+ See [`.env.example`](./.env.example) for the full list of optional overrides.
551
502
 
552
503
  ### Per-alias credentials
553
504
 
554
- `brapi_connect` resolves `baseUrl` and `auth` from env vars when the agent omits them — credentials never enter the LLM context. Four layers of precedence:
505
+ `brapi_connect` fills `baseUrl` and `auth` from env vars when the agent omits them, in this order:
506
+
507
+ 1. **Agent input**, within the pairing rules below.
508
+ 2. **Per-alias env vars**: `BRAPI_<ALIAS>_*`, uppercased with hyphens as underscores (`my-server` → `BRAPI_MY_SERVER_*`).
509
+ 3. **Built-in aliases**: see [Built-in aliases](#built-in-aliases).
510
+ 4. **`BRAPI_DEFAULT_BASE_URL`**, for an alias with no URL or credentials of its own.
511
+
512
+ Env credentials go only to the server configured with them:
555
513
 
556
- 1. **Explicit agent input** — always wins.
557
- 2. **Per-alias env vars** — `BRAPI_<ALIAS>_*` (uppercased, hyphens → underscores: `my-server` → `BRAPI_MY_SERVER_*`).
558
- 3. **Built-in known-server registry** — see [Built-in aliases](#built-in-aliases).
559
- 4. **Default env vars** — `BRAPI_DEFAULT_*`, only when the alias differs from `default`. Not layered on top of a built-in URL — defaults belong to the default server.
514
+ - An alias's credentials pair with its `BRAPI_<ALIAS>_BASE_URL`, else its enabled built-in URL. A caller `baseUrl` that points elsewhere fails with `auth_base_url_mismatch`. Credentials with no URL of their own, including those left behind by a built-in disabled via `BRAPI_BUILTIN_ALIASES_DISABLED`, fail with `alias_base_url_unset` and are never sent to `BRAPI_DEFAULT_BASE_URL`.
515
+ - `BRAPI_DEFAULT_*` credentials attach only when the resolved URL is `BRAPI_DEFAULT_BASE_URL`; an alias pointed anywhere else connects without auth unless it has credentials of its own. With no `BRAPI_DEFAULT_BASE_URL` set, default credentials fail with `alias_base_url_unset`.
560
516
 
561
- Each alias carries **one** credential family — auth mode is derived from which fields are set:
517
+ URLs compare after normalizing host case, default ports, and trailing slashes. Caller-supplied `auth` is never mixed with env credentials.
518
+
519
+ Each alias carries one credential family, and the auth mode follows from which fields are set. Mixing families within an alias raises a `ValidationError`.
562
520
 
563
521
  | Vars set | Resolved `mode` |
564
522
  |:---------|:----------------|
@@ -568,13 +526,13 @@ Each alias carries **one** credential family — auth mode is derived from which
568
526
  | `_OAUTH_CLIENT_ID` + `_OAUTH_CLIENT_SECRET` (+ optional `_OAUTH_TOKEN_URL`) | `oauth2` |
569
527
  | _(none set)_ | `none` |
570
528
 
571
- Mixing families within an alias raises a `ValidationError`.
529
+ `BRAPI_<ALIAS>_DIALECT` pins the dialect adapter (`spec`, `brapi-test`, `breedbase`, `cassavabase`, `bms`) when detection picks the wrong one; `auto` or unset detects it.
572
530
 
573
531
  ```sh
574
532
  # .env — attach write credentials to the built-in 'bti-cassava' alias
575
533
  BRAPI_BTI_CASSAVA_USERNAME=alice
576
534
  BRAPI_BTI_CASSAVA_PASSWORD=...
577
- # (BASE_URL omitted — built-in registry covers it)
535
+ # (BASE_URL omitted — the built-in registry covers it)
578
536
 
579
537
  # Static API key as alias 'prod'
580
538
  BRAPI_PROD_BASE_URL=https://my-brapi.example.com/brapi/v2
@@ -582,42 +540,50 @@ BRAPI_PROD_API_KEY=...
582
540
  BRAPI_PROD_API_KEY_HEADER=X-API-Key
583
541
  ```
584
542
 
585
- Then the agent calls `brapi_connect({ alias: 'bti-cassava' })` — no `baseUrl`, no `auth`, no secrets in the prompt.
543
+ The agent then calls `brapi_connect({ alias: 'bti-cassava' })` with no `baseUrl`, no `auth`, and no secrets in the prompt.
586
544
 
587
545
  ### Built-in aliases
588
546
 
589
- The server ships with a curated registry of public BrAPI v2 endpoints. Each resolves out-of-the-box; the orientation envelope surfaces license, citation, and homepage in its `attribution` block under [Creative Commons Attribution](https://creativecommons.org/licenses/by/4.0/).
547
+ These public BrAPI v2 endpoints connect with no configuration. Their orientation envelope carries license, citation, and homepage in an `attribution` block ([Creative Commons Attribution](https://creativecommons.org/licenses/by/4.0/)).
590
548
 
591
549
  | Alias | Upstream | Hosted by | Crop | Notes |
592
550
  |:------|:---------|:----------|:-----|:------|
593
551
  | `bti-cassava` | [cassavabase.org](https://cassavabase.org/) | Boyce Thompson Institute | Cassava | NextGen Cassava |
594
552
  | `bti-sweetpotato` | [sweetpotatobase.org](https://sweetpotatobase.org/) | Boyce Thompson Institute | Sweet potato | |
595
- | `bti-breedbase-demo` | [breedbase.org](https://breedbase.org/) | Boyce Thompson Institute | _Demo_ | Sample data only — onboarding + tests. |
596
- | `t3-wheat` | [wheat.triticeaetoolbox.org](https://wheat.triticeaetoolbox.org/) | Triticeae Toolbox (T3) | Wheat | Wheat CAP / IWYP. |
597
- | `t3-oat` | [oat.triticeaetoolbox.org](https://oat.triticeaetoolbox.org/) | Triticeae Toolbox (T3) | Oat | Global Oat Genetics Database. |
598
- | `t3-barley` | [barley.triticeaetoolbox.org](https://barley.triticeaetoolbox.org/) | Triticeae Toolbox (T3) | Barley | T-CAP / US Wheat & Barley Scab Initiative. |
553
+ | `bti-breedbase-demo` | [breedbase.org](https://breedbase.org/) | Boyce Thompson Institute | _Demo_ | Sample data only, for onboarding and tests |
554
+
555
+ The registry holds only servers verified for anonymous reads. Servers that require login, including the Triticeae Toolbox (T3) wheat, oat, and barley hosts, connect through `BRAPI_<ALIAS>_BASE_URL` plus credentials (see [`.env.example`](./.env.example)).
599
556
 
600
- Set `BRAPI_<ALIAS>_BASE_URL` to repoint at a staging mirror or fork (env wins over the built-in URL — hyphens in the alias become underscores in the env var, so `t3-wheat` → `BRAPI_T3_WHEAT_BASE_URL`). Set `BRAPI_<ALIAS>_USERNAME` etc. to attach credentials on top of the built-in URL — each Breedbase instance has its own user table, so write access requires separate registration on each upstream. Use `BRAPI_BUILTIN_ALIASES_DISABLED=bti-cassava,t3-wheat` to strip specific entries.
557
+ `BRAPI_<ALIAS>_BASE_URL` overrides a built-in URL, e.g. to point `bti-sweetpotato` at a staging mirror via `BRAPI_BTI_SWEETPOTATO_BASE_URL`. `BRAPI_<ALIAS>_USERNAME` and friends attach credentials on top of the built-in URL; each Breedbase instance has its own user table, so write access needs a separate account on each. `BRAPI_BUILTIN_ALIASES_DISABLED=bti-cassava,bti-breedbase-demo` removes entries.
601
558
 
602
- **Citation:** all six built-ins reference Morales et al. 2022, _"Breedbase: a digital ecosystem for modern plant breeding."_ G3 12(7): jkac078. [doi:10.1093/g3journal/jkac078](https://doi.org/10.1093/g3journal/jkac078).
559
+ **Citation:** all three built-ins reference Morales et al. 2022, _"Breedbase: a digital ecosystem for modern plant breeding."_ G3 12(7): jkac078. [doi:10.1093/g3journal/jkac078](https://doi.org/10.1093/g3journal/jkac078).
603
560
 
604
561
  ## Running the server
605
562
 
606
- ```sh
607
- # Hot-reload dev (Bun runs TS directly)
608
- bun --watch src/index.ts
609
-
610
- # Production
611
- bun run rebuild
612
- bun run start # transport via MCP_TRANSPORT_TYPE (stdio default)
613
- bun run start:stdio # or pin explicitly
614
- bun run start:http
615
-
616
- # Checks
617
- bun run devcheck # lint + format + typecheck + security + changelog sync
618
- bun run test # Vitest
619
- bun run lint:mcp # validate MCP definitions
620
- ```
563
+ ### Local development
564
+
565
+ - **Build and run the production version**:
566
+
567
+ ```sh
568
+ # One-time build
569
+ bun run rebuild
570
+
571
+ # Run the built server
572
+ bun run start # transport from MCP_TRANSPORT_TYPE (stdio default)
573
+ bun run start:stdio
574
+ bun run start:http
575
+
576
+ # Or run from source with hot reload
577
+ bun --watch src/index.ts
578
+ ```
579
+
580
+ - **Run checks and tests**:
581
+
582
+ ```sh
583
+ bun run devcheck # Lint, format, typecheck, security, changelog sync
584
+ bun run test # Vitest suite
585
+ bun run lint:mcp # Validate MCP definitions
586
+ ```
621
587
 
622
588
  ### Docker
623
589
 
@@ -626,32 +592,32 @@ docker build -t brapi-mcp-server .
626
592
  docker run --rm -p 3010:3010 brapi-mcp-server
627
593
  ```
628
594
 
629
- Defaults to HTTP transport, stateful session mode (engages the `mcp-session-id` lifecycle — precondition for `BRAPI_SESSION_ISOLATION=true`; hijack protection requires layering `MCP_AUTH_MODE=jwt|oauth` on top), logs to `/var/log/brapi-mcp-server`. OTel peer deps are installed by default — `--build-arg OTEL_ENABLED=false` to omit.
595
+ The image defaults to HTTP transport, `stateful` session mode, and logs to `/var/log/brapi-mcp-server`. OpenTelemetry peer dependencies are installed by default; build with `--build-arg OTEL_ENABLED=false` to omit them.
630
596
 
631
597
  ### Deployment shapes
632
598
 
633
- Two stateful layers scope by tenant and, by default, by MCP session: **connection state** (registered aliases, exchanged upstream tokens) and **dataframes** (`df_<uuid>` tables — possession of the name grants full read/write/drop within its bucket, auto-expires in 24h by default, provenance recorded). `brapi-mcp-server` runs in three shapes that pick where those buckets end:
599
+ Two kinds of state scope by tenant and, by default, by MCP session: **connection state** (registered aliases and exchanged upstream tokens) and **dataframes** (`df_<uuid>` tables, usable by anyone who holds the name within its bucket). Three configurations set where those buckets end:
634
600
 
635
601
  | Shape | Settings | Isolation | Best for |
636
602
  |:------|:---------|:----------|:---------|
637
- | **Per-session (default)** | `MCP_AUTH_MODE=none` + HTTP stateful + `BRAPI_SESSION_ISOLATION=true` | Each MCP session carves its own connection state and canvas. Concurrent HTTP callers don't see each other's aliases, exchanged tokens, or `df_<uuid>` rows. | Multi-user host without SSO. Default for institutional / public deployment under shared-trust auth. |
638
- | **Per-user credentials** | `MCP_AUTH_MODE=jwt` or `oauth` (+ HTTP stateful) | Each user's JWT `tid` claim carves a tenant; sessions sub-scope inside each tenant when isolation is on. Cross-user spillover impossible at the framework level. | Multi-user host with institutional SSO — strongest separation. |
639
- | **Shared workspace** | `MCP_AUTH_MODE=none` + `BRAPI_SESSION_ISOLATION=false` | All callers in one tenant share connection state and one canvas. | Solo, lab, or hosting where every caller is one researcher running parallel agents on shared upstream credentials. |
603
+ | **Per-session (default)** | `MCP_AUTH_MODE=none` + HTTP stateful + `BRAPI_SESSION_ISOLATION=true` | Each MCP session gets its own connections and canvas. Requests without a session share one tenant-wide namespace, so `brapi_connect` refuses their caller-supplied `auth` with `auth_session_required`; keyless connections and operator env credentials still work. | Multi-user hosting without SSO |
604
+ | **Per-user credentials** | `MCP_AUTH_MODE=jwt` or `oauth` (+ HTTP stateful) | Each user's JWT `tid` claim is its own tenant; sessions sub-scope inside it when isolation is on. | Multi-user hosting with institutional SSO; the strongest separation |
605
+ | **Shared workspace** | `MCP_AUTH_MODE=none` + `BRAPI_SESSION_ISOLATION=false` | All callers share one tenant's connections and canvas. | One researcher running parallel agents on shared upstream credentials |
640
606
 
641
- Stdio is always one session, so isolation is moot there. Clients on MCP protocol revision 2026-07-28 are session-less by every transport (no `ctx.sessionId`), so they always land in the shared tenant workspace regardless of `BRAPI_SESSION_ISOLATION` — only the per-user-credentials shape isolates them.
607
+ Stdio is always a single session. Clients on MCP protocol revision 2026-07-28 carry no session on any transport, so outside the per-user-credentials shape they land in the shared tenant workspace, where re-registering an alias re-points every such caller's later calls to it.
642
608
 
643
- Belt-and-braces under shared trust: `brapi_dataframe_describe` requires an explicit `dataframe` name (no list-all enumeration) and `brapi_dataframe_query` rejects system-catalog reads, so a caller without a known `df_<uuid>` name can't fish through either surface even in the shared-workspace shape.
609
+ On HTTP without per-user auth, `brapi_dataframe_describe` won't list dataframes without a name, and `brapi_dataframe_query` rejects system-catalog reads in every shape, so a caller without a known `df_<uuid>` name can't enumerate other callers' tables.
644
610
 
645
611
  ## Project structure
646
612
 
647
613
  | Directory | Purpose |
648
614
  |:----------|:--------|
649
- | `src/index.ts` | `createApp()` entry point — registers tools/resources/prompts and inits services. |
650
- | `src/config` | Server-specific environment variable parsing and validation with Zod. |
651
- | `src/mcp-server/tools` | Tool definitions (`*.tool.ts`). Twenty-five tools across connection, retrieval, analysis, write, and raw-passthrough. |
615
+ | `src/index.ts` | `createApp()` entry point: registers tools (behind their feature flags), resources, and prompts, and inits services. |
616
+ | `src/config` | Server env parsing (Zod), per-alias credential resolution, and the built-in alias registry. |
617
+ | `src/mcp-server/tools` | Tool definitions (`*.tool.ts`) and shared helpers. Twenty-five tools across connection, retrieval, analysis, write, and raw passthrough. |
652
618
  | `src/mcp-server/resources` | Resource definitions (`*.resource.ts`). |
653
619
  | `src/mcp-server/prompts` | Prompt definitions (`*.prompt.ts`). |
654
- | `src/services` | Domain service integrations — BrAPI client, dialect adapters, canvas bridge, capability registry, ontology resolver, reference-data cache, server registry. |
620
+ | `src/services` | BrAPI client, dialect adapters, filter catalog, canvas bridge, capability registry, ISO country resolver, ontology resolver, reference-data cache, server registry. |
655
621
  | `tests/` | Unit and integration tests mirroring `src/`. |
656
622
 
657
623
  ## Development guide
@@ -660,7 +626,7 @@ See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rule
660
626
 
661
627
  - Handlers throw, framework catches — no `try/catch` in tool logic
662
628
  - Use `ctx.log` for logging, `ctx.state` for tenant-scoped storage — no `console`, no direct persistence access
663
- - Register new tools in the `tools` array of `createApp()` in `src/index.ts`
629
+ - Add new tools to the matching group in `src/mcp-server/tools/definitions/index.ts`; `src/index.ts` composes the groups behind their feature flags
664
630
  - Wrap upstream calls: validate raw → normalize → return output schema; never fabricate missing fields
665
631
 
666
632
  ## Contributing