@cyanheads/brapi-mcp-server 0.7.12 → 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 (151) hide show
  1. package/AGENTS.md +460 -0
  2. package/CLAUDE.md +21 -8
  3. package/README.md +467 -184
  4. package/changelog/0.7.x/0.7.13.md +38 -0
  5. package/changelog/0.8.x/0.8.0.md +48 -0
  6. package/changelog/template.md +7 -7
  7. package/dist/config/alias-credentials.d.ts +28 -7
  8. package/dist/config/alias-credentials.d.ts.map +1 -1
  9. package/dist/config/alias-credentials.js +91 -24
  10. package/dist/config/alias-credentials.js.map +1 -1
  11. package/dist/config/builtin-aliases.d.ts +11 -6
  12. package/dist/config/builtin-aliases.d.ts.map +1 -1
  13. package/dist/config/builtin-aliases.js +23 -37
  14. package/dist/config/builtin-aliases.js.map +1 -1
  15. package/dist/index.js +8 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/mcp-server/resources/definitions/brapi-calls.resource.d.ts +1 -0
  18. package/dist/mcp-server/resources/definitions/brapi-calls.resource.d.ts.map +1 -1
  19. package/dist/mcp-server/resources/definitions/brapi-calls.resource.js +1 -0
  20. package/dist/mcp-server/resources/definitions/brapi-calls.resource.js.map +1 -1
  21. package/dist/mcp-server/resources/definitions/brapi-germplasm.resource.d.ts +1 -0
  22. package/dist/mcp-server/resources/definitions/brapi-germplasm.resource.d.ts.map +1 -1
  23. package/dist/mcp-server/resources/definitions/brapi-germplasm.resource.js +1 -0
  24. package/dist/mcp-server/resources/definitions/brapi-germplasm.resource.js.map +1 -1
  25. package/dist/mcp-server/resources/definitions/brapi-server-info.resource.d.ts +1 -0
  26. package/dist/mcp-server/resources/definitions/brapi-server-info.resource.d.ts.map +1 -1
  27. package/dist/mcp-server/resources/definitions/brapi-server-info.resource.js +2 -1
  28. package/dist/mcp-server/resources/definitions/brapi-server-info.resource.js.map +1 -1
  29. package/dist/mcp-server/resources/definitions/brapi-study.resource.d.ts +1 -0
  30. package/dist/mcp-server/resources/definitions/brapi-study.resource.d.ts.map +1 -1
  31. package/dist/mcp-server/resources/definitions/brapi-study.resource.js +1 -0
  32. package/dist/mcp-server/resources/definitions/brapi-study.resource.js.map +1 -1
  33. package/dist/mcp-server/resources/definitions/brapi-variable.resource.d.ts +1 -0
  34. package/dist/mcp-server/resources/definitions/brapi-variable.resource.d.ts.map +1 -1
  35. package/dist/mcp-server/resources/definitions/brapi-variable.resource.js +1 -0
  36. package/dist/mcp-server/resources/definitions/brapi-variable.resource.js.map +1 -1
  37. package/dist/mcp-server/tools/definitions/brapi-build-phenotype-matrix.tool.d.ts +2 -0
  38. package/dist/mcp-server/tools/definitions/brapi-build-phenotype-matrix.tool.d.ts.map +1 -1
  39. package/dist/mcp-server/tools/definitions/brapi-build-phenotype-matrix.tool.js +2 -0
  40. package/dist/mcp-server/tools/definitions/brapi-build-phenotype-matrix.tool.js.map +1 -1
  41. package/dist/mcp-server/tools/definitions/brapi-connect.tool.d.ts +46 -0
  42. package/dist/mcp-server/tools/definitions/brapi-connect.tool.d.ts.map +1 -1
  43. package/dist/mcp-server/tools/definitions/brapi-connect.tool.js +65 -15
  44. package/dist/mcp-server/tools/definitions/brapi-connect.tool.js.map +1 -1
  45. package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.d.ts +1 -0
  46. package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.d.ts.map +1 -1
  47. package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.js +1 -0
  48. package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.js.map +1 -1
  49. package/dist/mcp-server/tools/definitions/brapi-describe-filters.tool.d.ts.map +1 -1
  50. package/dist/mcp-server/tools/definitions/brapi-describe-filters.tool.js +7 -2
  51. package/dist/mcp-server/tools/definitions/brapi-describe-filters.tool.js.map +1 -1
  52. package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.d.ts +1 -0
  53. package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.d.ts.map +1 -1
  54. package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.js +1 -0
  55. package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.js.map +1 -1
  56. package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.d.ts +1 -0
  57. package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.d.ts.map +1 -1
  58. package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.js +1 -0
  59. package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.js.map +1 -1
  60. package/dist/mcp-server/tools/definitions/brapi-find-germplasm.tool.d.ts +2 -0
  61. package/dist/mcp-server/tools/definitions/brapi-find-germplasm.tool.d.ts.map +1 -1
  62. package/dist/mcp-server/tools/definitions/brapi-find-germplasm.tool.js +2 -0
  63. package/dist/mcp-server/tools/definitions/brapi-find-germplasm.tool.js.map +1 -1
  64. package/dist/mcp-server/tools/definitions/brapi-find-images.tool.d.ts +2 -0
  65. package/dist/mcp-server/tools/definitions/brapi-find-images.tool.d.ts.map +1 -1
  66. package/dist/mcp-server/tools/definitions/brapi-find-images.tool.js +2 -0
  67. package/dist/mcp-server/tools/definitions/brapi-find-images.tool.js.map +1 -1
  68. package/dist/mcp-server/tools/definitions/brapi-find-locations.tool.d.ts +2 -0
  69. package/dist/mcp-server/tools/definitions/brapi-find-locations.tool.d.ts.map +1 -1
  70. package/dist/mcp-server/tools/definitions/brapi-find-locations.tool.js +2 -0
  71. package/dist/mcp-server/tools/definitions/brapi-find-locations.tool.js.map +1 -1
  72. package/dist/mcp-server/tools/definitions/brapi-find-observations.tool.d.ts +2 -0
  73. package/dist/mcp-server/tools/definitions/brapi-find-observations.tool.d.ts.map +1 -1
  74. package/dist/mcp-server/tools/definitions/brapi-find-observations.tool.js +2 -0
  75. package/dist/mcp-server/tools/definitions/brapi-find-observations.tool.js.map +1 -1
  76. package/dist/mcp-server/tools/definitions/brapi-find-studies.tool.d.ts +2 -0
  77. package/dist/mcp-server/tools/definitions/brapi-find-studies.tool.d.ts.map +1 -1
  78. package/dist/mcp-server/tools/definitions/brapi-find-studies.tool.js +2 -0
  79. package/dist/mcp-server/tools/definitions/brapi-find-studies.tool.js.map +1 -1
  80. package/dist/mcp-server/tools/definitions/brapi-find-variables.tool.d.ts +2 -0
  81. package/dist/mcp-server/tools/definitions/brapi-find-variables.tool.d.ts.map +1 -1
  82. package/dist/mcp-server/tools/definitions/brapi-find-variables.tool.js +2 -0
  83. package/dist/mcp-server/tools/definitions/brapi-find-variables.tool.js.map +1 -1
  84. package/dist/mcp-server/tools/definitions/brapi-find-variants.tool.d.ts +2 -0
  85. package/dist/mcp-server/tools/definitions/brapi-find-variants.tool.d.ts.map +1 -1
  86. package/dist/mcp-server/tools/definitions/brapi-find-variants.tool.js +2 -0
  87. package/dist/mcp-server/tools/definitions/brapi-find-variants.tool.js.map +1 -1
  88. package/dist/mcp-server/tools/definitions/brapi-germplasm-performance.tool.d.ts +1 -0
  89. package/dist/mcp-server/tools/definitions/brapi-germplasm-performance.tool.d.ts.map +1 -1
  90. package/dist/mcp-server/tools/definitions/brapi-germplasm-performance.tool.js +1 -0
  91. package/dist/mcp-server/tools/definitions/brapi-germplasm-performance.tool.js.map +1 -1
  92. package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.d.ts +1 -0
  93. package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.d.ts.map +1 -1
  94. package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.js +1 -0
  95. package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.js.map +1 -1
  96. package/dist/mcp-server/tools/definitions/brapi-get-image.tool.d.ts +1 -0
  97. package/dist/mcp-server/tools/definitions/brapi-get-image.tool.d.ts.map +1 -1
  98. package/dist/mcp-server/tools/definitions/brapi-get-image.tool.js +1 -0
  99. package/dist/mcp-server/tools/definitions/brapi-get-image.tool.js.map +1 -1
  100. package/dist/mcp-server/tools/definitions/brapi-get-study.tool.d.ts +1 -0
  101. package/dist/mcp-server/tools/definitions/brapi-get-study.tool.d.ts.map +1 -1
  102. package/dist/mcp-server/tools/definitions/brapi-get-study.tool.js +1 -0
  103. package/dist/mcp-server/tools/definitions/brapi-get-study.tool.js.map +1 -1
  104. package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.d.ts +1 -0
  105. package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.d.ts.map +1 -1
  106. package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.js +1 -0
  107. package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.js.map +1 -1
  108. package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.d.ts +1 -0
  109. package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.d.ts.map +1 -1
  110. package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.js +1 -0
  111. package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.js.map +1 -1
  112. package/dist/mcp-server/tools/definitions/brapi-server-info.tool.d.ts +13 -0
  113. package/dist/mcp-server/tools/definitions/brapi-server-info.tool.d.ts.map +1 -1
  114. package/dist/mcp-server/tools/definitions/brapi-server-info.tool.js +2 -1
  115. package/dist/mcp-server/tools/definitions/brapi-server-info.tool.js.map +1 -1
  116. package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.d.ts +1 -0
  117. package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.d.ts.map +1 -1
  118. package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.js +1 -0
  119. package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.js.map +1 -1
  120. package/dist/mcp-server/tools/definitions/brapi-walk-pedigree.tool.d.ts +1 -0
  121. package/dist/mcp-server/tools/definitions/brapi-walk-pedigree.tool.d.ts.map +1 -1
  122. package/dist/mcp-server/tools/definitions/brapi-walk-pedigree.tool.js +1 -0
  123. package/dist/mcp-server/tools/definitions/brapi-walk-pedigree.tool.js.map +1 -1
  124. package/dist/mcp-server/tools/definitions/index.d.ts +151 -65
  125. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
  126. package/dist/mcp-server/tools/shared/find-helpers.d.ts +6 -0
  127. package/dist/mcp-server/tools/shared/find-helpers.d.ts.map +1 -1
  128. package/dist/mcp-server/tools/shared/find-helpers.js +17 -5
  129. package/dist/mcp-server/tools/shared/find-helpers.js.map +1 -1
  130. package/dist/mcp-server/tools/shared/orientation-envelope.d.ts +26 -0
  131. package/dist/mcp-server/tools/shared/orientation-envelope.d.ts.map +1 -1
  132. package/dist/mcp-server/tools/shared/orientation-envelope.js +75 -6
  133. package/dist/mcp-server/tools/shared/orientation-envelope.js.map +1 -1
  134. package/dist/services/brapi-client/brapi-client.d.ts +7 -0
  135. package/dist/services/brapi-client/brapi-client.d.ts.map +1 -1
  136. package/dist/services/brapi-client/brapi-client.js +45 -33
  137. package/dist/services/brapi-client/brapi-client.js.map +1 -1
  138. package/dist/services/capability-registry/capability-registry.d.ts +8 -1
  139. package/dist/services/capability-registry/capability-registry.d.ts.map +1 -1
  140. package/dist/services/capability-registry/capability-registry.js +37 -6
  141. package/dist/services/capability-registry/capability-registry.js.map +1 -1
  142. package/dist/services/reference-data-cache/reference-data-cache.d.ts.map +1 -1
  143. package/dist/services/reference-data-cache/reference-data-cache.js +9 -4
  144. package/dist/services/reference-data-cache/reference-data-cache.js.map +1 -1
  145. package/dist/services/server-registry/server-registry.d.ts +17 -3
  146. package/dist/services/server-registry/server-registry.d.ts.map +1 -1
  147. package/dist/services/server-registry/server-registry.js +30 -4
  148. package/dist/services/server-registry/server-registry.js.map +1 -1
  149. package/manifest.json +1 -1
  150. package/package.json +19 -8
  151. package/server.json +9 -3
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>A collaborative BrAPI v2.1 workspace for multi-agent research via MCP. Search studies, germplasm, genotypes, & more - across Breedbase, T3, Sweetpotatobase, & any BrAPI v2-compliant server.</b>
4
- <div>25 Tools • 6 Resources • 2 Prompts • Multi-agent collaboration</div>
4
+ <div>25 Tools • 6 Resources • 2 Prompts</div>
5
5
  </p>
6
6
  </div>
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![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) [![Version](https://img.shields.io/badge/Version-0.7.12-blue.svg?style=flat-square)](./CHANGELOG.md) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![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
 
@@ -19,137 +19,328 @@
19
19
 
20
20
  </div>
21
21
 
22
+ <div align="center">
23
+
24
+ **Public Hosted Server:** [https://brapi.caseyjhand.com/mcp](https://brapi.caseyjhand.com/mcp)
25
+
26
+ </div>
27
+
22
28
  ---
23
29
 
24
- ## Tools
30
+ ## Overview
25
31
 
26
- 25 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 that agents on the same session can query or hand off by ID, `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.
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.
27
33
 
28
- ### Orient
34
+ ### Tools
29
35
 
30
36
  | Tool | Description |
31
- |:-----|:------------|
32
- | `brapi_connect` | Authenticate, register the connection under an alias, cache the capability profile, and return the orientation envelope inline. One call fully orients the agent. |
33
- | `brapi_server_info` | Re-fetch the orientation envelope for a registered alias — identity, auth, capabilities, content counts, attribution, notes. |
34
- | `brapi_describe_filters` | Static BrAPI v2.1 filter catalog for any endpoint — powers `extraFilters` discovery on every `find_*` tool. |
37
+ |:---|:---|
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
+
64
+ ### Resources
65
+
66
+ | Resource | Description |
67
+ |:---|:---|
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
+
77
+ ### Prompts
78
+
79
+ | Prompt | Description |
80
+ |:---|:---|
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
+
84
+ ## Capability reference
85
+
86
+ ### `brapi_connect` <sub>tool</sub>
87
+
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
35
91
 
36
- ### Retrieve
92
+ ---
37
93
 
38
- | Tool | Description |
39
- |:-----|:------------|
40
- | `brapi_find_studies` | Find studies by crop / trial type / season / location / program. Distributions + dataframe spillover. |
41
- | `brapi_get_study` | Fetch a study with program / trial / location FKs resolved and companion counts (observations, units, variables). |
42
- | `brapi_find_germplasm` | Find germplasm by name, synonym, accession, PUI, crop, or free-text. Distributions + dataframe spillover. |
43
- | `brapi_get_germplasm` | Fetch a germplasm with attributes, direct parents, and companion counts (studies, parents, descendants). |
44
- | `brapi_walk_pedigree` | BFS-walk ancestry / descendancy as a deduplicated DAG with cycle detection, depth limits, and traversal stats. |
45
- | `brapi_find_variables` | Find observation variables by name / class / ontology / free-text; ranked client-side via `OntologyResolver` when `text` is supplied. |
46
- | `brapi_find_observations` | Pull observation records by study / germplasm / variable / season / unit / timestamp. Dataframe spillover. |
47
- | `brapi_find_images` | Filter image metadata by unit / study / ontology / MIME type. Bytes via `brapi_get_image`. |
48
- | `brapi_get_image` | Fetch image bytes for up to 5 imageDbIds inline as `type: image` blocks. Prefers `/imagecontent`, falls back to `imageURL`. |
49
- | `brapi_find_locations` | Find research stations by country (ISO alpha-3 code, or English country name resolved client-side) / type / abbreviation, with optional client-side bbox filter. |
50
- | `brapi_find_variants` | Find variant records by variant set, reference, or genomic region (1-based inclusive / exclusive). |
51
- | `brapi_find_genotype_calls` | Pull genotype calls via async-search polling. Upstream pull bounded by `BRAPI_GENOTYPE_CALLS_MAX_PULL` (default 100k, max 500k). |
52
-
53
- ### Analyze
94
+ ### `brapi_server_info` <sub>tool</sub>
54
95
 
55
- | Tool | Description |
56
- |:-----|:------------|
57
- | `brapi_dataframe_describe` | Start here after a spillover. Lists dataframes (or describes one) with column schema, row counts, and originating-source provenance. |
58
- | `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 }[]`). |
59
- | `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. |
60
- | `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. |
61
- | `brapi_build_phenotype_matrix` | Build a germplasm × trait matrix from one or more studies and materialize it as a canvas dataframe. Supports wide (pivot) or long shape with configurable per-cell aggregation. |
62
- | `brapi_germplasm_performance` | Per-variable performance aggregates (n, mean, median, sd, min, max, studyCount) for a single germplasm across all studies where it has observations. |
63
- | `brapi_export_genotype_matrix` | Export genotype calls for a variant set as a germplasm × variant canvas dataframe; also serializes to VCF-lite or PLINK `.ped`/`.map` text. Distinct-variant columns bounded by `BRAPI_GENOTYPE_MATRIX_MAX_COLUMNS` (default 10k, max 500k). |
96
+ - `alias` optional; `forceRefresh` (default `false`) refetches the capability profile instead of reading the cache
97
+ - Returns the same orientation envelope as `brapi_connect`
64
98
 
65
- ### Write (opt-in: `BRAPI_ENABLE_WRITES=true`)
99
+ ---
66
100
 
67
- | Tool | Description |
68
- |:-----|:------------|
69
- | `brapi_submit_observations` | Two-phase observation write — `mode: preview` validates; `mode: apply` asks the caller to confirm, then fans POST + PUT in parallel. Additive only — no destructive deletion. |
101
+ ### `brapi_describe_filters` <sub>tool</sub>
70
102
 
71
- ### Escape hatches
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
72
105
 
73
- | Tool | Description |
74
- |:-----|:------------|
75
- | `brapi_raw_get` | Passthrough to any BrAPI `GET /{path}` not covered by curated tools. Emits a routing nudge when one applies. |
76
- | `brapi_raw_search` | Passthrough to any `POST /search/{noun}` with async polling handled transparently. Same nudge pattern. |
106
+ ---
107
+
108
+ ### `brapi_find_studies` <sub>tool</sub>
109
+
110
+ - Filters: `crop`, `trialTypes`, `seasons`, `locations`, `programs`, `trials`, `studyNames`, `active`
111
+ - `distributions` over `programName`, `studyType`, `seasons`, `locationName`, `commonCropName`
112
+
113
+ ---
114
+
115
+ ### `brapi_get_study` <sub>tool</sub>
116
+
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
119
+
120
+ ---
121
+
122
+ ### `brapi_find_germplasm` <sub>tool</sub>
123
+
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`
126
+
127
+ ---
128
+
129
+ ### `brapi_get_germplasm` <sub>tool</sub>
130
+
131
+ - `germplasmDbId` required; returns `attributes` and direct `parents`; `germplasm_not_found` when the upstream has no such germplasm
132
+ - Companion counts `studyCount`, `directParentCount`, `directDescendantCount`
133
+
134
+ ---
135
+
136
+ ### `brapi_walk_pedigree` <sub>tool</sub>
137
+
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`
140
+
141
+ ---
142
+
143
+ ### `brapi_find_variables` <sub>tool</sub>
144
+
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`
148
+
149
+ ---
150
+
151
+ ### `brapi_find_observations` <sub>tool</sub>
152
+
153
+ - Filters: `studies`, `germplasm`, `variables`, `observationUnits`, `observations`, `seasons`, `programs`, `trials`, `observationLevels`, `timestampFrom` / `timestampTo`
154
+ - `distributions` over `observationVariableName`, `studyName`, `germplasmName`, `observationLevel`, `season`
155
+
156
+ ---
157
+
158
+ ### `brapi_find_images` <sub>tool</sub>
159
+
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`
162
+
163
+ ---
164
+
165
+ ### `brapi_get_image` <sub>tool</sub>
166
+
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
77
169
 
78
- > **Alias discovery.** Built-in and operator-configured aliases are appended to the `brapi_connect` description at server startup, so agents see the inventory on `tools/list`. Restart after env-var changes to refresh.
170
+ ---
171
+
172
+ ### `brapi_find_locations` <sub>tool</sub>
173
+
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]`
176
+
177
+ ---
178
+
179
+ ### `brapi_find_variants` <sub>tool</sub>
180
+
181
+ - Filters: `variantSets`, `variants`, `references`, and a genomic region of `referenceName` + `start` (inclusive) / `end` (exclusive), 1-based
182
+ - `distributions` over `variantType`, `referenceName`, `variantSetDbId`
183
+
184
+ ---
185
+
186
+ ### `brapi_find_genotype_calls` <sub>tool</sub>
187
+
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
191
+
192
+ ---
193
+
194
+ ### `brapi_dataframe_describe` <sub>tool</sub>
195
+
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
198
+
199
+ ---
200
+
201
+ ### `brapi_dataframe_query` <sub>tool</sub>
202
+
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
206
+
207
+ ---
208
+
209
+ ### `brapi_dataframe_drop` <sub>tool</sub>
210
+
211
+ - `dataframe` required; returns `dropped: false`, not an error, for an unknown name
212
+ - Registered only when `BRAPI_CANVAS_DROP_ENABLED=true`
79
213
 
80
214
  ---
81
215
 
82
- ## Resources
216
+ ### `brapi_dataframe_export` <sub>tool</sub>
217
+
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`
219
+ - Typed errors: `export_dir_unset`, `dataframe_not_found`, `invalid_filename`, `mutually_exclusive_projection`
220
+ - Registered only over stdio with `BRAPI_EXPORT_DIR` set
221
+
222
+ ---
223
+
224
+ ### `brapi_build_phenotype_matrix` <sub>tool</sub>
225
+
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
228
+
229
+ ---
230
+
231
+ ### `brapi_germplasm_performance` <sub>tool</sub>
232
+
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`
235
+
236
+ ---
237
+
238
+ ### `brapi_export_genotype_matrix` <sub>tool</sub>
239
+
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
243
+
244
+ ---
245
+
246
+ ### `brapi_submit_observations` <sub>tool</sub>
247
+
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
251
+
252
+ ---
253
+
254
+ ### `brapi_raw_get` <sub>tool</sub>
255
+
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
258
+
259
+ ---
83
260
 
84
- 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.
261
+ ### `brapi_raw_search` <sub>tool</sub>
85
262
 
86
- | URI template | Mirrors |
87
- |:-------------|:--------|
88
- | `brapi://server/info` | `brapi_server_info` (default connection) |
89
- | `brapi://calls` | Raw capability profile |
90
- | `brapi://study/{studyDbId}` | `brapi_get_study` |
91
- | `brapi://germplasm/{germplasmDbId}` | `brapi_get_germplasm` |
92
- | `brapi://filters/{endpoint}` | `brapi_describe_filters` |
93
- | `brapi://variable/{observationVariableDbId}` | Observation variable record (trait, scale, method, ontology) |
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
94
265
 
95
266
  ---
96
267
 
97
- ## Prompts
268
+ ### `brapi://server/info` <sub>resource</sub>
98
269
 
99
- Multi-step BrAPI workflow templates — pure user-message generators, no side effects.
270
+ - Orientation envelope for the `default` connection as `application/json`
271
+ - Same payload as `brapi_server_info` called with no arguments
100
272
 
101
- | Name | Args | Purpose |
102
- |:-----|:-----|:--------|
103
- | `brapi_eda_study` | `studyDbId`, `alias?` | EDA playbook for one study — orient, variables, coverage, missing data, outliers, pedigree, structured report. |
104
- | `brapi_meta_analysis` | `germplasmDbIds` (CSV), `traitName`, `alias?` | Cross-study meta-analysis — trait resolution, study discovery, harmonization, per-germplasm × per-study and across-study summaries. |
273
+ ---
274
+
275
+ ### `brapi://calls` <sub>resource</sub>
276
+
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
105
279
 
106
280
  ---
107
281
 
108
- ## Multi-agent workflows
282
+ ### `brapi://study/{studyDbId}` <sub>resource</sub>
109
283
 
110
- The server has two stateful layers and two scoping axes:
284
+ - Same payload as `brapi_get_study` on the `default` connection
285
+ - `study_not_found` when the upstream has no such study
111
286
 
112
- | Layer | Default scope | Why |
113
- |:------|:--------------|:----|
114
- | **Connection state** (aliases, exchanged tokens) | Tenant + session | Credentials and live tokens. Tenant gates by user (`jwt`/`oauth`) or collapses to `'default'` (`none`). Session sub-scope (`BRAPI_SESSION_ISOLATION=true`, default) prevents concurrent HTTP sessions in one tenant from sharing each other's tokens. |
115
- | **Dataframes** (`df_<uuid>` tables) | Tenant + session | Within one (tenant, session), agents share by `df_<uuid>` name — possession grants full read/write/drop, auto-expires in 24h, provenance recorded. The underlying canvas is tenant-gated by the framework; the session sub-scope is enforced by the bridge's keying. |
287
+ ---
116
288
 
117
- Within one (tenant, session), dataframes act as a self-cleaning shared notebook: hand the `df_<uuid>` name between parallel agents on the same MCP session, persist it across a multi-step workflow, query / project / aggregate / join from any position. Address-by-name, time-bounded, scoped to that session.
289
+ ### `brapi://germplasm/{germplasmDbId}` <sub>resource</sub>
118
290
 
119
- **Default (isolated) shape.** Under `MCP_AUTH_MODE=none` + HTTP stateful (the default), each MCP session carves its own connection state and its own canvas. Two researchers connected to the same host don't see each other's `brapi_connect` aliases, exchanged SGN/OAuth tokens, or spilled `df_<uuid>` rows. Stdio always behaves as one session (single-process, no concurrency).
291
+ - Same payload as `brapi_get_germplasm` on the `default` connection
292
+ - `germplasm_not_found` when the upstream has no such germplasm
120
293
 
121
- **Clients on MCP revision 2026-07-28.** That revision is session-less on every transport — requests carry no `Mcp-Session-Id` — so `ctx.sessionId` is undefined and a client negotiating it falls back to the shared tenant workspace even under `MCP_SESSION_MODE=stateful`. Session isolation applies to 2025-era clients; deployments that need a hard boundary for 2026-era clients should carve tenants with `MCP_AUTH_MODE=jwt`/`oauth`.
294
+ ---
122
295
 
123
- **Shared-workspace shape.** Set `BRAPI_SESSION_ISOLATION=false` for cross-session collaboration in one tenant — multiple MCP sessions then share connection state and one default canvas, the way pre-0.5.3 deployments behaved. Useful when planning, analysis, and writeup agents run as separate MCP clients but operate as one researcher on shared upstream credentials.
296
+ ### `brapi://filters/{endpoint}` <sub>resource</sub>
124
297
 
125
- **On privileged data.** The `df_<uuid>` name is a capability token within a canvas — not row-level access control. Anyone holding the name within the same (tenant, session) bucket can read its rows. Under default isolation, that bucket is one MCP session. Under `BRAPI_SESSION_ISOLATION=false`, the bucket widens to the whole tenant (all callers under `auth=none`, or one user's sessions under `jwt`/`oauth`). Treat dataframe names like authenticated share links — pass within the bucket, not externally. The 24h TTL caps blast radius; the provenance trail (originating tool, baseUrl, query) supports audit. Belt-and-braces: `brapi_dataframe_describe` requires an explicit `dataframe` name on shared-trust HTTP (no list-all enumeration), and `brapi_dataframe_query` rejects system-catalog reads (`information_schema`, `pg_catalog`, `sqlite_master`, `duckdb_*`) — so a caller without a known `df_<uuid>` name can't fish through either surface.
298
+ - Same payload as `brapi_describe_filters`; `unknown_endpoint` for an endpoint outside the catalog
299
+ - Listing `brapi://filters` returns one resource per endpoint
126
300
 
127
301
  ---
128
302
 
129
- ## BrAPI-specific features
303
+ ### `brapi://variable/{observationVariableDbId}` <sub>resource</sub>
304
+
305
+ - The `/variables/{id}` record (trait, scale, method, ontology) on the `default` connection
306
+ - `variable_not_found` when the upstream has no such variable
130
307
 
131
- - **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; session-scoped by default (tenant-scoped under `BRAPI_SESSION_ISOLATION=false`) — see [Multi-agent workflows](#multi-agent-workflows).
132
- - **Multi-server session** — `ServerRegistry` maps aliases to live BrAPI connections; one session can span Breedbase, T3, and Sweetpotatobase in parallel.
133
- - **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.
134
- - **Capability-aware calls** — `CapabilityRegistry` caches `/serverinfo` per connection and guards every tool call against unsupported endpoints. Falls back to `/calls` when `/serverinfo` is sparse.
135
- - **Dialect adaptation** — `spec` / `brapi-test` / `breedbase` / `cassavabase` / `bms` dialects translate v2.1 plural filter keys to the singular form each server family honors, drop filters known to be broken, normalize sparse-shape encodings, and escalate to POST `/search/{noun}` when GET would silently downcast multi-value filters. Detected from `/serverinfo` (server-name / organization-name); pin per-alias via `BRAPI_<ALIAS>_DIALECT`. Verified-vs-inferred mapping counts surface on the orientation envelope so agents see the confidence floor at a glance.
136
- - **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).
137
- - **Async-search transparency** — `brapi_find_genotype_calls` and `brapi_raw_search` handle the `POST /search/{noun}` → `GET /search/{noun}/{id}` 202-retry pattern automatically.
138
- - **Pedigree DAG walks** — `brapi_walk_pedigree` BFS-traverses ancestry / descendancy with cycle detection (BrAPI only exposes one generation per call); a 1,000-node safety cap bounds the walk and sets `truncated` when reached. Walks larger than `loadLimit` spill their node and edge sets to two JOINable canvas dataframes and return a bounded inline preview.
139
- - **Image content** — `brapi_get_image` fetches bytes inline as MCP `type: image` blocks, preferring `/images/{id}/imagecontent` with `imageURL` fallback.
140
- - **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`.
141
- - **Auth variants in one schema** — tagged-union covers `none` / `bearer` / `api_key` / `sgn` (session-token exchange) / `oauth2` (client-credentials).
142
- - **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.
308
+ ---
309
+
310
+ ### `brapi_eda_study` <sub>prompt</sub>
143
311
 
144
- 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.
312
+ - Arguments: `studyDbId` required; `alias` optional
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
145
314
 
146
315
  ---
147
316
 
148
- ## Working with dataframes
317
+ ### `brapi_meta_analysis` <sub>prompt</sub>
318
+
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
321
+
322
+ ## Features
323
+
324
+ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
325
+
326
+ BrAPI-specific:
149
327
 
150
- 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 — reserved words like `end`, digit-leading IDs — are sanitized for the dataframe, and a `columnLegend` on the handle maps each renamed column back to its original key. 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.
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
151
333
 
152
- Dataframe names are session-scoped capability tokens by default — pass `tableName` to any other agent on the same MCP session (or a downstream step in the same workflow) and they query the same workspace by name without re-pulling from the upstream. The `brapi_dataframe_*` tools offer SQL manipulation and more. See [Multi-agent workflows](#multi-agent-workflows) for cross-session / cross-tenant rules.
334
+ Agent-friendly output:
335
+
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`
340
+
341
+ ### Working with dataframes
342
+
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`.
153
344
 
154
345
  ```text
155
346
  1. brapi_find_observations { studies: ["s-422"] }
@@ -158,17 +349,30 @@ Dataframe names are session-scoped capability tokens by default — pass `tableN
158
349
  → schema + provenance (originating tool, baseUrl, query, expiry)
159
350
  3. brapi_dataframe_query { sql: "SELECT germplasmName, value FROM df_<uuid> WHERE observationVariableDbId = 'V1' LIMIT 100" }
160
351
  → typed columns + bounded rows
161
- 4. brapi_dataframe_query { sql: "SELECT COUNT(*) AS n, AVG(CAST(value AS DOUBLE)) AS mean FROM df_<uuid> WHERE observationVariableDbId = 'V1'" }
162
- → aggregate without round-tripping all rows
163
352
  ```
164
353
 
165
- 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.
166
-
167
- ---
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.
168
355
 
169
356
  ## Getting started
170
357
 
171
- Add to your MCP client config — pick one runner:
358
+ ### Public Hosted Instance
359
+
360
+ A public instance is available at `https://brapi.caseyjhand.com/mcp` — no installation required. Point any MCP client at it via Streamable HTTP:
361
+
362
+ ```json
363
+ {
364
+ "mcpServers": {
365
+ "brapi-mcp-server": {
366
+ "type": "streamable-http",
367
+ "url": "https://brapi.caseyjhand.com/mcp"
368
+ }
369
+ }
370
+ }
371
+ ```
372
+
373
+ ### Self-Hosted / Local
374
+
375
+ Add the following to your MCP client configuration file.
172
376
 
173
377
  ```json
174
378
  {
@@ -177,26 +381,87 @@ Add to your MCP client config — pick one runner:
177
381
  "type": "stdio",
178
382
  "command": "bunx",
179
383
  "args": ["@cyanheads/brapi-mcp-server@latest"],
180
- "env": { "MCP_TRANSPORT_TYPE": "stdio", "MCP_LOG_LEVEL": "info" }
384
+ "env": {
385
+ "MCP_TRANSPORT_TYPE": "stdio",
386
+ "MCP_LOG_LEVEL": "info"
387
+ }
181
388
  }
182
389
  }
183
390
  }
184
391
  ```
185
392
 
186
- Swap `command`/`args` for `npx -y @cyanheads/brapi-mcp-server@latest` (no Bun) or `docker run -i --rm -e MCP_TRANSPORT_TYPE=stdio ghcr.io/cyanheads/brapi-mcp-server:latest`.
393
+ Or with npx (no Bun required):
394
+
395
+ ```json
396
+ {
397
+ "mcpServers": {
398
+ "brapi-mcp-server": {
399
+ "type": "stdio",
400
+ "command": "npx",
401
+ "args": ["-y", "@cyanheads/brapi-mcp-server@latest"],
402
+ "env": {
403
+ "MCP_TRANSPORT_TYPE": "stdio",
404
+ "MCP_LOG_LEVEL": "info"
405
+ }
406
+ }
407
+ }
408
+ }
409
+ ```
410
+
411
+ Or with Docker:
412
+
413
+ ```json
414
+ {
415
+ "mcpServers": {
416
+ "brapi-mcp-server": {
417
+ "type": "stdio",
418
+ "command": "docker",
419
+ "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/brapi-mcp-server:latest"]
420
+ }
421
+ }
422
+ }
423
+ ```
187
424
 
188
- For Streamable HTTP:
425
+ For Streamable HTTP, set the transport and start the server:
189
426
 
190
427
  ```sh
191
428
  MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
192
429
  # Server listens at http://localhost:3010/mcp
193
430
  ```
194
431
 
195
- 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).
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)).
196
433
 
197
- **Prerequisites:** [Bun v1.4.0+](https://bun.sh/) or Node.js v24+. [`@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).
434
+ ### Prerequisites
198
435
 
199
- ---
436
+ - [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
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.
438
+
439
+ ### Installation
440
+
441
+ 1. **Clone the repository:**
442
+
443
+ ```sh
444
+ git clone https://github.com/cyanheads/brapi-mcp-server.git
445
+ ```
446
+
447
+ 2. **Navigate into the directory:**
448
+
449
+ ```sh
450
+ cd brapi-mcp-server
451
+ ```
452
+
453
+ 3. **Install dependencies:**
454
+
455
+ ```sh
456
+ bun install
457
+ ```
458
+
459
+ 4. **Configure environment:**
460
+
461
+ ```sh
462
+ cp .env.example .env
463
+ # edit .env if you need credentials or non-default settings
464
+ ```
200
465
 
201
466
  ## Configuration
202
467
 
@@ -204,43 +469,54 @@ Every variable is optional.
204
469
 
205
470
  | Variable | Description | Default |
206
471
  |:---------|:------------|:--------|
207
- | `BRAPI_DEFAULT_BASE_URL` | Default BrAPI v2 base URL (e.g. `https://test-server.brapi.org/brapi/v2`). | — |
208
- | `BRAPI_DEFAULT_USERNAME` / `_PASSWORD` | SGN session-token auth for the default connection. | — |
209
- | `BRAPI_DEFAULT_OAUTH_CLIENT_ID` / `_OAUTH_CLIENT_SECRET` | OAuth2 client-credentials for the default connection. | — |
210
- | `BRAPI_DEFAULT_API_KEY` / `_API_KEY_HEADER` | Static API key for the default connection. | header `Authorization` |
211
- | `BRAPI_BUILTIN_ALIASES_DISABLED` | Comma-separated alias names (case-insensitive) to remove from the built-in registry. | — |
212
- | `BRAPI_LOAD_LIMIT` | In-context row cap returned by `find_*` tools before spilling to a canvas dataframe. | `1000` |
213
- | `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` |
214
478
  | `BRAPI_MAX_CONCURRENT_REQUESTS` | Per-connection concurrency cap. | `4` |
215
- | `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` |
216
480
  | `BRAPI_REQUEST_TIMEOUT_MS` | Per-request HTTP timeout. | `30000` |
217
- | `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` |
218
- | `BRAPI_SEARCH_POLL_TIMEOUT_MS` / `_INTERVAL_MS` | Async `/search` polling budget + interval. | `60000` / `1000` |
219
- | `BRAPI_DATASET_TTL_SECONDS` | TTL for dataframe provenance metadata persisted alongside spilled rows. | `86400` |
220
- | `BRAPI_REFERENCE_CACHE_TTL_SECONDS` | TTL for programs / trials / locations / crops cache. | `3600` |
221
- | `BRAPI_ALLOW_PRIVATE_IPS` | Allow RFC 1918 / loopback targets. Dev-only. | `false` |
222
- | `BRAPI_ENABLE_WRITES` | Opt-in for `brapi_submit_observations` registration. | `false` |
223
- | `BRAPI_GENOTYPE_CALLS_MAX_PULL` | Upstream row ceiling per `brapi_find_genotype_calls` invocation. Max 500,000. | `100000` |
224
- | `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 (all scale with column count, independent of the row pull). A `maxColumns` input may lower it, not raise it. Max 500,000. | `10000` |
225
- | `BRAPI_CANVAS_DROP_ENABLED` | Opt-in for `brapi_dataframe_drop` registration. Off by default; dataframes expire via TTL when left unmanaged. | `false` |
226
- | `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. | — |
227
- | `BRAPI_CANVAS_MAX_ROWS` / `BRAPI_CANVAS_QUERY_TIMEOUT_MS` | Per-query response row cap and wall-clock timeout for `brapi_dataframe_query`. | `10000` / `30000` |
228
- | `MCP_TRANSPORT_TYPE` / `MCP_HTTP_PORT` / `MCP_SESSION_MODE` | Transport (`stdio` \| `http`), HTTP port, session mode: `stateful` retains 2025 client sessions for observation-write confirmation; `stateless` cannot perform that confirmation round; `auto` (framework default) resolves to stateful for HTTP. Docker and the env example pin `stateful`. | `stdio` / `3010` / `stateful` |
229
- | `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` |
230
- | `BRAPI_SESSION_ISOLATION` | When `true`, scope ServerRegistry connection state and the CanvasBridge default canvas to `ctx.sessionId` (HTTP stateful/auto). Concurrent callers under `MCP_AUTH_MODE=none` operate in isolated workspaces. Set `false` for the shared-workspace collaboration model. No effect on stdio. | `true` |
231
-
232
- Per-alias overrides follow the `BRAPI_<ALIAS>_*` pattern — see [`.env.example`](./.env.example) for every override and inline comments.
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` |
493
+ | `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
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` |
500
+
501
+ See [`.env.example`](./.env.example) for the full list of optional overrides.
233
502
 
234
503
  ### Per-alias credentials
235
504
 
236
- `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.
237
511
 
238
- 1. **Explicit agent input** — always wins.
239
- 2. **Per-alias env vars** — `BRAPI_<ALIAS>_*` (uppercased, hyphens → underscores: `my-server` → `BRAPI_MY_SERVER_*`).
240
- 3. **Built-in known-server registry** — see [Built-in aliases](#built-in-aliases).
241
- 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.
512
+ Env credentials go only to the server configured with them:
242
513
 
243
- Each alias carries **one** credential family — auth mode is derived from which fields are set:
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`.
516
+
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`.
244
520
 
245
521
  | Vars set | Resolved `mode` |
246
522
  |:---------|:----------------|
@@ -250,13 +526,13 @@ Each alias carries **one** credential family — auth mode is derived from which
250
526
  | `_OAUTH_CLIENT_ID` + `_OAUTH_CLIENT_SECRET` (+ optional `_OAUTH_TOKEN_URL`) | `oauth2` |
251
527
  | _(none set)_ | `none` |
252
528
 
253
- 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.
254
530
 
255
531
  ```sh
256
532
  # .env — attach write credentials to the built-in 'bti-cassava' alias
257
533
  BRAPI_BTI_CASSAVA_USERNAME=alice
258
534
  BRAPI_BTI_CASSAVA_PASSWORD=...
259
- # (BASE_URL omitted — built-in registry covers it)
535
+ # (BASE_URL omitted — the built-in registry covers it)
260
536
 
261
537
  # Static API key as alias 'prod'
262
538
  BRAPI_PROD_BASE_URL=https://my-brapi.example.com/brapi/v2
@@ -264,44 +540,50 @@ BRAPI_PROD_API_KEY=...
264
540
  BRAPI_PROD_API_KEY_HEADER=X-API-Key
265
541
  ```
266
542
 
267
- 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.
268
544
 
269
545
  ### Built-in aliases
270
546
 
271
- 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/)).
272
548
 
273
549
  | Alias | Upstream | Hosted by | Crop | Notes |
274
550
  |:------|:---------|:----------|:-----|:------|
275
551
  | `bti-cassava` | [cassavabase.org](https://cassavabase.org/) | Boyce Thompson Institute | Cassava | NextGen Cassava |
276
552
  | `bti-sweetpotato` | [sweetpotatobase.org](https://sweetpotatobase.org/) | Boyce Thompson Institute | Sweet potato | |
277
- | `bti-breedbase-demo` | [breedbase.org](https://breedbase.org/) | Boyce Thompson Institute | _Demo_ | Sample data only — onboarding + tests. |
278
- | `t3-wheat` | [wheat.triticeaetoolbox.org](https://wheat.triticeaetoolbox.org/) | Triticeae Toolbox (T3) | Wheat | Wheat CAP / IWYP. |
279
- | `t3-oat` | [oat.triticeaetoolbox.org](https://oat.triticeaetoolbox.org/) | Triticeae Toolbox (T3) | Oat | Global Oat Genetics Database. |
280
- | `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 |
281
554
 
282
- 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.
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)).
283
556
 
284
- **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).
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.
285
558
 
286
- ---
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).
287
560
 
288
561
  ## Running the server
289
562
 
290
- ```sh
291
- # Hot-reload dev (Bun runs TS directly)
292
- bun --watch src/index.ts
293
-
294
- # Production
295
- bun run rebuild
296
- bun run start # transport via MCP_TRANSPORT_TYPE (stdio default)
297
- bun run start:stdio # or pin explicitly
298
- bun run start:http
299
-
300
- # Checks
301
- bun run devcheck # lint + format + typecheck + security + changelog sync
302
- bun run test # Vitest
303
- bun run lint:mcp # validate MCP definitions
304
- ```
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
+ ```
305
587
 
306
588
  ### Docker
307
589
 
@@ -310,51 +592,52 @@ docker build -t brapi-mcp-server .
310
592
  docker run --rm -p 3010:3010 brapi-mcp-server
311
593
  ```
312
594
 
313
- 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.
314
596
 
315
597
  ### Deployment shapes
316
598
 
317
- `brapi-mcp-server` runs in three shapes — pick the one that matches your trust domain. The differentiator is what isolates **connection state** (registered aliases, cached upstream tokens) and **dataframes**: nothing, the MCP session, or the auth tenant.
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:
318
600
 
319
601
  | Shape | Settings | Isolation | Best for |
320
602
  |:------|:---------|:----------|:---------|
321
- | **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. |
322
- | **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 (Shibboleth, Okta, etc.) — strongest separation. |
323
- | **Shared workspace** | `MCP_AUTH_MODE=none` + `BRAPI_SESSION_ISOLATION=false` | All callers in one tenant share connection state and one canvas. Possession of a `df_<uuid>` name = full read/write across the workspace. | 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 |
324
606
 
325
- **Shape selection guide:**
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.
326
608
 
327
- - **Multi-user public/institutional HTTP, no SSO.** Use the per-session default. Each researcher's stateful HTTP session is isolated even though they all resolve to `tenantId='default'`.
328
- - **Multi-user with institutional SSO.** `MCP_AUTH_MODE=jwt` (HS256, `MCP_AUTH_SECRET_KEY`) or `oauth` (JWKS, `OAUTH_ISSUER_URL` + `OAUTH_AUDIENCE`). Each user's `tid` claim carves a tenant — the outer scope. `BRAPI_SESSION_ISOLATION=true` (default) then sub-scopes inside each tenant for users running parallel sessions, and JWT/OAuth identity binding gives real session-hijack protection on top.
329
- - **One researcher, parallel agents.** If multiple agents (planner, analyst, writeup) connect as separate MCP clients but should share one workspace, set `BRAPI_SESSION_ISOLATION=false` and rely on shared trust. This is the shared-workspace shape.
330
- - **Stdio.** Always one session; isolation is moot. The flag has no effect.
331
- - **Clients on MCP revision 2026-07-28.** Session-less by protocol, so they land in the shared tenant workspace whatever `BRAPI_SESSION_ISOLATION` says. Only the per-user-credentials shape isolates them.
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.
332
610
 
333
- **Belt-and-braces under shared trust.** Even with `BRAPI_SESSION_ISOLATION=false`, `brapi_dataframe_describe` requires an explicit `dataframe` name on HTTP (no list-all enumeration), and `brapi_dataframe_query` rejects system-catalog reads (`information_schema`, `pg_catalog`, `sqlite_master`, `duckdb_*`). The dataframe name is the capability token; possession proves it.
611
+ ## Project structure
334
612
 
335
- ---
613
+ | Directory | Purpose |
614
+ |:----------|:--------|
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. |
618
+ | `src/mcp-server/resources` | Resource definitions (`*.resource.ts`). |
619
+ | `src/mcp-server/prompts` | Prompt definitions (`*.prompt.ts`). |
620
+ | `src/services` | BrAPI client, dialect adapters, filter catalog, canvas bridge, capability registry, ISO country resolver, ontology resolver, reference-data cache, server registry. |
621
+ | `tests/` | Unit and integration tests mirroring `src/`. |
336
622
 
337
- ## Development
623
+ ## Development guide
338
624
 
339
- See [`CLAUDE.md`](./CLAUDE.md) for full architectural rules. Short version:
625
+ See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
340
626
 
341
627
  - Handlers throw, framework catches — no `try/catch` in tool logic
342
- - Use `ctx.log` for logging, `ctx.state` for storage — no `console`, no direct persistence
343
- - Register new tools in the `tools` array of `createApp()` in `src/index.ts`
628
+ - Use `ctx.log` for logging, `ctx.state` for tenant-scoped storage — no `console`, no direct persistence access
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
344
630
  - Wrap upstream calls: validate raw → normalize → return output schema; never fabricate missing fields
345
631
 
346
- ```sh
347
- git clone https://github.com/cyanheads/brapi-mcp-server.git
348
- cd brapi-mcp-server
349
- bun install
350
- cp .env.example .env # edit if you need credentials
351
- bun run devcheck && bun run test
352
- ```
632
+ ## Contributing
353
633
 
354
- PRs welcome.
634
+ Issues are welcome. Run checks and tests before submitting:
355
635
 
356
- ---
636
+ ```sh
637
+ bun run devcheck
638
+ bun run test
639
+ ```
357
640
 
358
641
  ## License
359
642
 
360
- Apache-2.0 — see [LICENSE](LICENSE).
643
+ Apache-2.0 — see [LICENSE](LICENSE) for details.