@cyanheads/brapi-mcp-server 0.7.11 → 0.7.13

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 (135) hide show
  1. package/AGENTS.md +453 -0
  2. package/CLAUDE.md +24 -16
  3. package/README.md +453 -136
  4. package/changelog/0.7.x/0.7.12.md +35 -0
  5. package/changelog/0.7.x/0.7.13.md +38 -0
  6. package/changelog/template.md +9 -26
  7. package/dist/config/alias-credentials.d.ts +1 -1
  8. package/dist/config/alias-credentials.d.ts.map +1 -1
  9. package/dist/config/alias-credentials.js +24 -8
  10. package/dist/config/alias-credentials.js.map +1 -1
  11. package/dist/config/server-config.d.ts +1 -1
  12. package/dist/config/server-config.d.ts.map +1 -1
  13. package/dist/config/server-config.js +3 -1
  14. package/dist/config/server-config.js.map +1 -1
  15. package/dist/index.js +7 -0
  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 +2 -1
  20. package/dist/mcp-server/resources/definitions/brapi-calls.resource.js.map +1 -1
  21. package/dist/mcp-server/resources/definitions/brapi-filters.resource.js +1 -1
  22. package/dist/mcp-server/resources/definitions/brapi-germplasm.resource.d.ts +1 -0
  23. package/dist/mcp-server/resources/definitions/brapi-germplasm.resource.d.ts.map +1 -1
  24. package/dist/mcp-server/resources/definitions/brapi-germplasm.resource.js +2 -1
  25. package/dist/mcp-server/resources/definitions/brapi-germplasm.resource.js.map +1 -1
  26. package/dist/mcp-server/resources/definitions/brapi-server-info.resource.d.ts +1 -0
  27. package/dist/mcp-server/resources/definitions/brapi-server-info.resource.d.ts.map +1 -1
  28. package/dist/mcp-server/resources/definitions/brapi-server-info.resource.js +2 -1
  29. package/dist/mcp-server/resources/definitions/brapi-server-info.resource.js.map +1 -1
  30. package/dist/mcp-server/resources/definitions/brapi-study.resource.d.ts +1 -0
  31. package/dist/mcp-server/resources/definitions/brapi-study.resource.d.ts.map +1 -1
  32. package/dist/mcp-server/resources/definitions/brapi-study.resource.js +2 -1
  33. package/dist/mcp-server/resources/definitions/brapi-study.resource.js.map +1 -1
  34. package/dist/mcp-server/resources/definitions/brapi-variable.resource.d.ts +1 -0
  35. package/dist/mcp-server/resources/definitions/brapi-variable.resource.d.ts.map +1 -1
  36. package/dist/mcp-server/resources/definitions/brapi-variable.resource.js +2 -1
  37. package/dist/mcp-server/resources/definitions/brapi-variable.resource.js.map +1 -1
  38. package/dist/mcp-server/tools/definitions/brapi-build-phenotype-matrix.tool.d.ts +2 -0
  39. package/dist/mcp-server/tools/definitions/brapi-build-phenotype-matrix.tool.d.ts.map +1 -1
  40. package/dist/mcp-server/tools/definitions/brapi-build-phenotype-matrix.tool.js +2 -0
  41. package/dist/mcp-server/tools/definitions/brapi-build-phenotype-matrix.tool.js.map +1 -1
  42. package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.d.ts +1 -0
  43. package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.d.ts.map +1 -1
  44. package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.js +1 -0
  45. package/dist/mcp-server/tools/definitions/brapi-dataframe-export.tool.js.map +1 -1
  46. package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.d.ts +1 -0
  47. package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.d.ts.map +1 -1
  48. package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.js +1 -0
  49. package/dist/mcp-server/tools/definitions/brapi-export-genotype-matrix.tool.js.map +1 -1
  50. package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.d.ts +1 -0
  51. package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.d.ts.map +1 -1
  52. package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.js +1 -0
  53. package/dist/mcp-server/tools/definitions/brapi-find-genotype-calls.tool.js.map +1 -1
  54. package/dist/mcp-server/tools/definitions/brapi-find-germplasm.tool.d.ts +2 -0
  55. package/dist/mcp-server/tools/definitions/brapi-find-germplasm.tool.d.ts.map +1 -1
  56. package/dist/mcp-server/tools/definitions/brapi-find-germplasm.tool.js +2 -0
  57. package/dist/mcp-server/tools/definitions/brapi-find-germplasm.tool.js.map +1 -1
  58. package/dist/mcp-server/tools/definitions/brapi-find-images.tool.d.ts +2 -0
  59. package/dist/mcp-server/tools/definitions/brapi-find-images.tool.d.ts.map +1 -1
  60. package/dist/mcp-server/tools/definitions/brapi-find-images.tool.js +2 -0
  61. package/dist/mcp-server/tools/definitions/brapi-find-images.tool.js.map +1 -1
  62. package/dist/mcp-server/tools/definitions/brapi-find-locations.tool.d.ts +2 -0
  63. package/dist/mcp-server/tools/definitions/brapi-find-locations.tool.d.ts.map +1 -1
  64. package/dist/mcp-server/tools/definitions/brapi-find-locations.tool.js +2 -0
  65. package/dist/mcp-server/tools/definitions/brapi-find-locations.tool.js.map +1 -1
  66. package/dist/mcp-server/tools/definitions/brapi-find-observations.tool.d.ts +2 -0
  67. package/dist/mcp-server/tools/definitions/brapi-find-observations.tool.d.ts.map +1 -1
  68. package/dist/mcp-server/tools/definitions/brapi-find-observations.tool.js +2 -0
  69. package/dist/mcp-server/tools/definitions/brapi-find-observations.tool.js.map +1 -1
  70. package/dist/mcp-server/tools/definitions/brapi-find-studies.tool.d.ts +2 -0
  71. package/dist/mcp-server/tools/definitions/brapi-find-studies.tool.d.ts.map +1 -1
  72. package/dist/mcp-server/tools/definitions/brapi-find-studies.tool.js +2 -0
  73. package/dist/mcp-server/tools/definitions/brapi-find-studies.tool.js.map +1 -1
  74. package/dist/mcp-server/tools/definitions/brapi-find-variables.tool.d.ts +2 -0
  75. package/dist/mcp-server/tools/definitions/brapi-find-variables.tool.d.ts.map +1 -1
  76. package/dist/mcp-server/tools/definitions/brapi-find-variables.tool.js +2 -0
  77. package/dist/mcp-server/tools/definitions/brapi-find-variables.tool.js.map +1 -1
  78. package/dist/mcp-server/tools/definitions/brapi-find-variants.tool.d.ts +2 -0
  79. package/dist/mcp-server/tools/definitions/brapi-find-variants.tool.d.ts.map +1 -1
  80. package/dist/mcp-server/tools/definitions/brapi-find-variants.tool.js +2 -0
  81. package/dist/mcp-server/tools/definitions/brapi-find-variants.tool.js.map +1 -1
  82. package/dist/mcp-server/tools/definitions/brapi-germplasm-performance.tool.d.ts +1 -0
  83. package/dist/mcp-server/tools/definitions/brapi-germplasm-performance.tool.d.ts.map +1 -1
  84. package/dist/mcp-server/tools/definitions/brapi-germplasm-performance.tool.js +1 -0
  85. package/dist/mcp-server/tools/definitions/brapi-germplasm-performance.tool.js.map +1 -1
  86. package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.d.ts +1 -0
  87. package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.d.ts.map +1 -1
  88. package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.js +1 -0
  89. package/dist/mcp-server/tools/definitions/brapi-get-germplasm.tool.js.map +1 -1
  90. package/dist/mcp-server/tools/definitions/brapi-get-image.tool.d.ts +1 -0
  91. package/dist/mcp-server/tools/definitions/brapi-get-image.tool.d.ts.map +1 -1
  92. package/dist/mcp-server/tools/definitions/brapi-get-image.tool.js +1 -0
  93. package/dist/mcp-server/tools/definitions/brapi-get-image.tool.js.map +1 -1
  94. package/dist/mcp-server/tools/definitions/brapi-get-study.tool.d.ts +1 -0
  95. package/dist/mcp-server/tools/definitions/brapi-get-study.tool.d.ts.map +1 -1
  96. package/dist/mcp-server/tools/definitions/brapi-get-study.tool.js +1 -0
  97. package/dist/mcp-server/tools/definitions/brapi-get-study.tool.js.map +1 -1
  98. package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.d.ts +1 -0
  99. package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.d.ts.map +1 -1
  100. package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.js +1 -0
  101. package/dist/mcp-server/tools/definitions/brapi-raw-get.tool.js.map +1 -1
  102. package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.d.ts +1 -0
  103. package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.d.ts.map +1 -1
  104. package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.js +1 -0
  105. package/dist/mcp-server/tools/definitions/brapi-raw-search.tool.js.map +1 -1
  106. package/dist/mcp-server/tools/definitions/brapi-server-info.tool.d.ts +1 -0
  107. package/dist/mcp-server/tools/definitions/brapi-server-info.tool.d.ts.map +1 -1
  108. package/dist/mcp-server/tools/definitions/brapi-server-info.tool.js +1 -0
  109. package/dist/mcp-server/tools/definitions/brapi-server-info.tool.js.map +1 -1
  110. package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.d.ts +1 -0
  111. package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.d.ts.map +1 -1
  112. package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.js +1 -0
  113. package/dist/mcp-server/tools/definitions/brapi-submit-observations.tool.js.map +1 -1
  114. package/dist/mcp-server/tools/definitions/brapi-walk-pedigree.tool.d.ts +1 -0
  115. package/dist/mcp-server/tools/definitions/brapi-walk-pedigree.tool.d.ts.map +1 -1
  116. package/dist/mcp-server/tools/definitions/brapi-walk-pedigree.tool.js +1 -0
  117. package/dist/mcp-server/tools/definitions/brapi-walk-pedigree.tool.js.map +1 -1
  118. package/dist/mcp-server/tools/definitions/index.d.ts +28 -0
  119. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
  120. package/dist/services/brapi-client/brapi-client.d.ts +7 -0
  121. package/dist/services/brapi-client/brapi-client.d.ts.map +1 -1
  122. package/dist/services/brapi-client/brapi-client.js +21 -12
  123. package/dist/services/brapi-client/brapi-client.js.map +1 -1
  124. package/dist/services/brapi-client/types.d.ts +1 -1
  125. package/dist/services/brapi-dialect/detect.d.ts +1 -1
  126. package/dist/services/brapi-dialect/index.d.ts +2 -2
  127. package/dist/services/brapi-dialect/index.js +1 -1
  128. package/dist/services/capability-registry/capability-registry.d.ts +1 -1
  129. package/dist/services/capability-registry/capability-registry.js +1 -1
  130. package/dist/services/reference-data-cache/reference-data-cache.d.ts +2 -2
  131. package/dist/services/reference-data-cache/reference-data-cache.js +1 -1
  132. package/dist/services/server-registry/types.d.ts +1 -1
  133. package/manifest.json +8 -6
  134. package/package.json +22 -10
  135. 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.11-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.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)
11
11
 
12
12
  </div>
13
13
 
@@ -19,137 +19,376 @@
19
19
 
20
20
  </div>
21
21
 
22
- ---
22
+ <div align="center">
23
+
24
+ **Public Hosted Server:** [https://brapi.caseyjhand.com/mcp](https://brapi.caseyjhand.com/mcp)
23
25
 
24
- ## Tools
26
+ </div>
25
27
 
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.
28
+ ---
27
29
 
28
- ### Orient
30
+ ## Overview
29
31
 
30
- | 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. |
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.
35
33
 
36
- ### Retrieve
34
+ ### Tools
37
35
 
38
36
  | 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. |
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. |
43
44
  | `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). |
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. |
63
+
64
+ ### Resources
52
65
 
53
- ### Analyze
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.
54
67
 
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). |
68
+ | Resource | Description |
69
+ |:---|:---|
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). |
64
76
 
65
- ### Write (opt-in: `BRAPI_ENABLE_WRITES=true`)
77
+ ### Prompts
66
78
 
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. |
79
+ | Prompt | Description |
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`. |
70
83
 
71
- ### Escape hatches
84
+ ## Capability reference
72
85
 
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. |
86
+ ### `brapi_connect` <sub>tool</sub>
77
87
 
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.
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`
79
93
 
80
94
  ---
81
95
 
82
- ## Resources
96
+ ### `brapi_server_info` <sub>tool</sub>
83
97
 
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.
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`
101
+
102
+ ---
103
+
104
+ ### `brapi_describe_filters` <sub>tool</sub>
85
105
 
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) |
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
94
110
 
95
111
  ---
96
112
 
97
- ## Prompts
113
+ ### `brapi_find_studies` <sub>tool</sub>
114
+
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`
98
120
 
99
- Multi-step BrAPI workflow templates — pure user-message generators, no side effects.
121
+ ---
122
+
123
+ ### `brapi_get_study` <sub>tool</sub>
100
124
 
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. |
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`
105
128
 
106
129
  ---
107
130
 
108
- ## Multi-agent workflows
131
+ ### `brapi_find_germplasm` <sub>tool</sub>
109
132
 
110
- The server has two stateful layers and two scoping axes:
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`
111
138
 
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. |
139
+ ---
116
140
 
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.
141
+ ### `brapi_get_germplasm` <sub>tool</sub>
118
142
 
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).
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`
120
146
 
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`.
147
+ ---
122
148
 
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.
149
+ ### `brapi_walk_pedigree` <sub>tool</sub>
124
150
 
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.
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`
126
156
 
127
157
  ---
128
158
 
129
- ## BrAPI-specific features
159
+ ### `brapi_find_variables` <sub>tool</sub>
160
+
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`
166
+
167
+ ---
130
168
 
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.
169
+ ### `brapi_find_observations` <sub>tool</sub>
143
170
 
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.
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`
145
175
 
146
176
  ---
147
177
 
148
- ## Working with dataframes
178
+ ### `brapi_find_images` <sub>tool</sub>
179
+
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`
184
+
185
+ ---
186
+
187
+ ### `brapi_get_image` <sub>tool</sub>
188
+
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`)
193
+
194
+ ---
195
+
196
+ ### `brapi_find_locations` <sub>tool</sub>
197
+
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`
202
+
203
+ ---
204
+
205
+ ### `brapi_find_variants` <sub>tool</sub>
206
+
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`
211
+
212
+ ---
213
+
214
+ ### `brapi_find_genotype_calls` <sub>tool</sub>
215
+
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)
220
+
221
+ ---
222
+
223
+ ### `brapi_dataframe_describe` <sub>tool</sub>
224
+
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
228
+
229
+ ---
230
+
231
+ ### `brapi_dataframe_query` <sub>tool</sub>
232
+
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`
238
+
239
+ ---
240
+
241
+ ### `brapi_dataframe_drop` <sub>tool</sub>
242
+
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
246
+
247
+ ---
248
+
249
+ ### `brapi_dataframe_export` <sub>tool</sub>
250
+
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
254
+ - Typed errors: `export_dir_unset`, `dataframe_not_found`, `invalid_filename`, `mutually_exclusive_projection`
255
+
256
+ ---
257
+
258
+ ### `brapi_build_phenotype_matrix` <sub>tool</sub>
259
+
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`
265
+
266
+ ---
267
+
268
+ ### `brapi_germplasm_performance` <sub>tool</sub>
269
+
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`
273
+
274
+ ---
275
+
276
+ ### `brapi_export_genotype_matrix` <sub>tool</sub>
277
+
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`
282
+
283
+ ---
284
+
285
+ ### `brapi_submit_observations` <sub>tool</sub>
286
+
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`
293
+
294
+ ---
295
+
296
+ ### `brapi_raw_get` <sub>tool</sub>
297
+
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)
302
+
303
+ ---
304
+
305
+ ### `brapi_raw_search` <sub>tool</sub>
306
+
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`
311
+
312
+ ---
313
+
314
+ ### `brapi://server/info` <sub>resource</sub>
315
+
316
+ - No parameters — reads the cached capability profile for the `default` connection
317
+ - Typed error: `unknown_alias`
318
+
319
+ ---
320
+
321
+ ### `brapi://calls` <sub>resource</sub>
322
+
323
+ - No parameters — raw `/serverinfo` + `/calls` profile (server identity, crops, supported services) for the `default` connection
324
+ - Typed error: `unknown_alias`
325
+
326
+ ---
327
+
328
+ ### `brapi://study/{studyDbId}` <sub>resource</sub>
329
+
330
+ - Same payload as `brapi_get_study`, addressed by URI on the default connection
331
+ - Typed errors: `unknown_alias`, `study_not_found`
332
+
333
+ ---
334
+
335
+ ### `brapi://germplasm/{germplasmDbId}` <sub>resource</sub>
336
+
337
+ - Same payload as `brapi_get_germplasm`, addressed by URI on the default connection
338
+ - Typed errors: `unknown_alias`, `germplasm_not_found`
339
+
340
+ ---
341
+
342
+ ### `brapi://filters/{endpoint}` <sub>resource</sub>
343
+
344
+ - Same payload as `brapi_describe_filters`; listing the resource collection returns one entry per supported endpoint
345
+ - Typed error: `unknown_endpoint`
346
+
347
+ ---
348
+
349
+ ### `brapi://variable/{observationVariableDbId}` <sub>resource</sub>
350
+
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`
353
+
354
+ ---
355
+
356
+ ### `brapi_eda_study` <sub>prompt</sub>
357
+
358
+ - 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
361
+
362
+ ---
363
+
364
+ ### `brapi_meta_analysis` <sub>prompt</sub>
149
365
 
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.
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
151
369
 
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.
370
+ ## Features
371
+
372
+ 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.
373
+
374
+ BrAPI-specific:
375
+
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
381
+
382
+ Agent-friendly output:
383
+
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
388
+
389
+ ## Working with dataframes
390
+
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.
153
392
 
154
393
  ```text
155
394
  1. brapi_find_observations { studies: ["s-422"] }
@@ -158,17 +397,30 @@ Dataframe names are session-scoped capability tokens by default — pass `tableN
158
397
  → schema + provenance (originating tool, baseUrl, query, expiry)
159
398
  3. brapi_dataframe_query { sql: "SELECT germplasmName, value FROM df_<uuid> WHERE observationVariableDbId = 'V1' LIMIT 100" }
160
399
  → 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
400
  ```
164
401
 
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
- ---
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.
168
403
 
169
404
  ## Getting started
170
405
 
171
- Add to your MCP client config — pick one runner:
406
+ ### Public Hosted Instance
407
+
408
+ A public instance is available at `https://brapi.caseyjhand.com/mcp` — no installation required. Point any MCP client at it via Streamable HTTP:
409
+
410
+ ```json
411
+ {
412
+ "mcpServers": {
413
+ "brapi-mcp-server": {
414
+ "type": "streamable-http",
415
+ "url": "https://brapi.caseyjhand.com/mcp"
416
+ }
417
+ }
418
+ }
419
+ ```
420
+
421
+ ### Self-Hosted / Local
422
+
423
+ Add the following to your MCP client configuration file.
172
424
 
173
425
  ```json
174
426
  {
@@ -177,26 +429,87 @@ Add to your MCP client config — pick one runner:
177
429
  "type": "stdio",
178
430
  "command": "bunx",
179
431
  "args": ["@cyanheads/brapi-mcp-server@latest"],
180
- "env": { "MCP_TRANSPORT_TYPE": "stdio", "MCP_LOG_LEVEL": "info" }
432
+ "env": {
433
+ "MCP_TRANSPORT_TYPE": "stdio",
434
+ "MCP_LOG_LEVEL": "info"
435
+ }
181
436
  }
182
437
  }
183
438
  }
184
439
  ```
185
440
 
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`.
441
+ Or with npx (no Bun required):
187
442
 
188
- For Streamable HTTP:
443
+ ```json
444
+ {
445
+ "mcpServers": {
446
+ "brapi-mcp-server": {
447
+ "type": "stdio",
448
+ "command": "npx",
449
+ "args": ["-y", "@cyanheads/brapi-mcp-server@latest"],
450
+ "env": {
451
+ "MCP_TRANSPORT_TYPE": "stdio",
452
+ "MCP_LOG_LEVEL": "info"
453
+ }
454
+ }
455
+ }
456
+ }
457
+ ```
458
+
459
+ Or with Docker:
460
+
461
+ ```json
462
+ {
463
+ "mcpServers": {
464
+ "brapi-mcp-server": {
465
+ "type": "stdio",
466
+ "command": "docker",
467
+ "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/brapi-mcp-server:latest"]
468
+ }
469
+ }
470
+ }
471
+ ```
472
+
473
+ For Streamable HTTP, set the transport and start the server:
189
474
 
190
475
  ```sh
191
476
  MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
192
477
  # Server listens at http://localhost:3010/mcp
193
478
  ```
194
479
 
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).
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).
196
481
 
197
- **Prerequisites:** [Bun v1.3.11+](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).
482
+ ### Prerequisites
198
483
 
199
- ---
484
+ - [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).
486
+
487
+ ### Installation
488
+
489
+ 1. **Clone the repository:**
490
+
491
+ ```sh
492
+ git clone https://github.com/cyanheads/brapi-mcp-server.git
493
+ ```
494
+
495
+ 2. **Navigate into the directory:**
496
+
497
+ ```sh
498
+ cd brapi-mcp-server
499
+ ```
500
+
501
+ 3. **Install dependencies:**
502
+
503
+ ```sh
504
+ bun install
505
+ ```
506
+
507
+ 4. **Configure environment:**
508
+
509
+ ```sh
510
+ cp .env.example .env
511
+ # edit .env if you need credentials or non-default settings
512
+ ```
200
513
 
201
514
  ## Configuration
202
515
 
@@ -214,22 +527,27 @@ Every variable is optional.
214
527
  | `BRAPI_MAX_CONCURRENT_REQUESTS` | Per-connection concurrency cap. | `4` |
215
528
  | `BRAPI_RETRY_MAX_ATTEMPTS` / `BRAPI_RETRY_BASE_DELAY_MS` | Retry policy for 429/5xx with exponential backoff. | `3` / `500` |
216
529
  | `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` |
530
+ | `BRAPI_COMPANION_TIMEOUT_MS` | Tighter timeout for non-critical companion enrichments (FK lookups, count probes); companions also bypass the retry budget. | `8000` |
218
531
  | `BRAPI_SEARCH_POLL_TIMEOUT_MS` / `_INTERVAL_MS` | Async `/search` polling budget + interval. | `60000` / `1000` |
219
532
  | `BRAPI_DATASET_TTL_SECONDS` | TTL for dataframe provenance metadata persisted alongside spilled rows. | `86400` |
220
533
  | `BRAPI_REFERENCE_CACHE_TTL_SECONDS` | TTL for programs / trials / locations / crops cache. | `3600` |
221
534
  | `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. | — |
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. | — |
227
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` |
228
- | `MCP_TRANSPORT_TYPE` / `MCP_HTTP_PORT` / `MCP_SESSION_MODE` | Transport (`stdio` \| `http`), HTTP port, session mode (`stateful` \| `stateless` \| `auto`; `auto` resolves to stateful for HTTP). | `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` |
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` |
542
+ | `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` |
231
549
 
232
- Per-alias overrides follow the `BRAPI_<ALIAS>_*` pattern — see [`.env.example`](./.env.example) for every override and inline comments.
550
+ Per-alias overrides follow the `BRAPI_<ALIAS>_*` pattern. See [`.env.example`](./.env.example) for the full list of optional overrides.
233
551
 
234
552
  ### Per-alias credentials
235
553
 
@@ -283,8 +601,6 @@ Set `BRAPI_<ALIAS>_BASE_URL` to repoint at a staging mirror or fork (env wins ov
283
601
 
284
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).
285
603
 
286
- ---
287
-
288
604
  ## Running the server
289
605
 
290
606
  ```sh
@@ -314,47 +630,48 @@ Defaults to HTTP transport, stateful session mode (engages the `mcp-session-id`
314
630
 
315
631
  ### Deployment shapes
316
632
 
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.
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:
318
634
 
319
635
  | Shape | Settings | Isolation | Best for |
320
636
  |:------|:---------|:----------|:---------|
321
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. |
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. |
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. |
324
640
 
325
- **Shape selection guide:**
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.
326
642
 
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.
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.
332
644
 
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.
645
+ ## Project structure
334
646
 
335
- ---
647
+ | Directory | Purpose |
648
+ |:----------|:--------|
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. |
652
+ | `src/mcp-server/resources` | Resource definitions (`*.resource.ts`). |
653
+ | `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. |
655
+ | `tests/` | Unit and integration tests mirroring `src/`. |
336
656
 
337
- ## Development
657
+ ## Development guide
338
658
 
339
- See [`CLAUDE.md`](./CLAUDE.md) for full architectural rules. Short version:
659
+ See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
340
660
 
341
661
  - 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
662
+ - Use `ctx.log` for logging, `ctx.state` for tenant-scoped storage — no `console`, no direct persistence access
343
663
  - Register new tools in the `tools` array of `createApp()` in `src/index.ts`
344
664
  - Wrap upstream calls: validate raw → normalize → return output schema; never fabricate missing fields
345
665
 
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
- ```
666
+ ## Contributing
353
667
 
354
- PRs welcome.
668
+ Issues are welcome. Run checks and tests before submitting:
355
669
 
356
- ---
670
+ ```sh
671
+ bun run devcheck
672
+ bun run test
673
+ ```
357
674
 
358
675
  ## License
359
676
 
360
- Apache-2.0 — see [LICENSE](LICENSE).
677
+ Apache-2.0 — see [LICENSE](LICENSE) for details.