{
  "markdown": "<div align=\"center\">\n  <h1>@cyanheads/brapi-mcp-server</h1>\n  <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>\n  <div>25 Tools • 6 Resources • 2 Prompts • Multi-agent collaboration</div>\n  </p>\n</div>\n\n<div align=\"center\">\n\n[![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)\n\n</div>\n\n<div align=\"center\">\n\n[![Install in Claude Desktop](https://img.shields.io/badge/Install_in-Claude_Desktop-D97757?style=for-the-badge&logo=anthropic&logoColor=white)](https://github.com/cyanheads/brapi-mcp-server/releases/latest/download/brapi-mcp-server.mcpb) [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=brapi-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvYnJhcGktbWNwLXNlcnZlciJdfQ==) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=for-the-badge&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22brapi-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads/brapi-mcp-server%22%5D%7D)\n\n[![Framework](https://img.shields.io/badge/Built%20on-@cyanheads/mcp--ts--core-67E8F9?style=flat-square)](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)\n\n</div>\n\n---\n\n## Tools\n\n25 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.\n\n### Orient\n\n| Tool | Description |\n|:-----|:------------|\n| `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. |\n| `brapi_server_info` | Re-fetch the orientation envelope for a registered alias — identity, auth, capabilities, content counts, attribution, notes. |\n| `brapi_describe_filters` | Static BrAPI v2.1 filter catalog for any endpoint — powers `extraFilters` discovery on every `find_*` tool. |\n\n### Retrieve\n\n| Tool | Description |\n|:-----|:------------|\n| `brapi_find_studies` | Find studies by crop / trial type / season / location / program. Distributions + dataframe spillover. |\n| `brapi_get_study` | Fetch a study with program / trial / location FKs resolved and companion counts (observations, units, variables). |\n| `brapi_find_germplasm` | Find germplasm by name, synonym, accession, PUI, crop, or free-text. Distributions + dataframe spillover. |\n| `brapi_get_germplasm` | Fetch a germplasm with attributes, direct parents, and companion counts (studies, parents, descendants). |\n| `brapi_walk_pedigree` | BFS-walk ancestry / descendancy as a deduplicated DAG with cycle detection, depth limits, and traversal stats. |\n| `brapi_find_variables` | Find observation variables by name / class / ontology / free-text; ranked client-side via `OntologyResolver` when `text` is supplied. |\n| `brapi_find_observations` | Pull observation records by study / germplasm / variable / season / unit / timestamp. Dataframe spillover. |\n| `brapi_find_images` | Filter image metadata by unit / study / ontology / MIME type. Bytes via `brapi_get_image`. |\n| `brapi_get_image` | Fetch image bytes for up to 5 imageDbIds inline as `type: image` blocks. Prefers `/imagecontent`, falls back to `imageURL`. |\n| `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. |\n| `brapi_find_variants` | Find variant records by variant set, reference, or genomic region (1-based inclusive / exclusive). |\n| `brapi_find_genotype_calls` | Pull genotype calls via async-search polling. Upstream pull bounded by `BRAPI_GENOTYPE_CALLS_MAX_PULL` (default 100k, max 500k). |\n\n### Analyze\n\n| Tool | Description |\n|:-----|:------------|\n| `brapi_dataframe_describe` | Start here after a spillover. Lists dataframes (or describes one) with column schema, row counts, and originating-source provenance. |\n| `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 }[]`). |\n| `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. |\n| `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. |\n| `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. |\n| `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. |\n| `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). |\n\n### Write (opt-in: `BRAPI_ENABLE_WRITES=true`)\n\n| Tool | Description |\n|:-----|:------------|\n| `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. |\n\n### Escape hatches\n\n| Tool | Description |\n|:-----|:------------|\n| `brapi_raw_get` | Passthrough to any BrAPI `GET /{path}` not covered by curated tools. Emits a routing nudge when one applies. |\n| `brapi_raw_search` | Passthrough to any `POST /search/{noun}` with async polling handled transparently. Same nudge pattern. |\n\n> **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.\n\n---\n\n## Resources\n\nURI-addressable mirrors of the curated tool surface for clients that prefer resources. All resources use the default connection — multi-server workflows route through tools.\n\n| URI template | Mirrors |\n|:-------------|:--------|\n| `brapi://server/info` | `brapi_server_info` (default connection) |\n| `brapi://calls` | Raw capability profile |\n| `brapi://study/{studyDbId}` | `brapi_get_study` |\n| `brapi://germplasm/{germplasmDbId}` | `brapi_get_germplasm` |\n| `brapi://filters/{endpoint}` | `brapi_describe_filters` |\n| `brapi://variable/{observationVariableDbId}` | Observation variable record (trait, scale, method, ontology) |\n\n---\n\n## Prompts\n\nMulti-step BrAPI workflow templates — pure user-message generators, no side effects.\n\n| Name | Args | Purpose |\n|:-----|:-----|:--------|\n| `brapi_eda_study` | `studyDbId`, `alias?` | EDA playbook for one study — orient, variables, coverage, missing data, outliers, pedigree, structured report. |\n| `brapi_meta_analysis` | `germplasmDbIds` (CSV), `traitName`, `alias?` | Cross-study meta-analysis — trait resolution, study discovery, harmonization, per-germplasm × per-study and across-study summaries. |\n\n---\n\n## Multi-agent workflows\n\nThe server has two stateful layers and two scoping axes:\n\n| Layer | Default scope | Why |\n|:------|:--------------|:----|\n| **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. |\n| **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. |\n\nWithin 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.\n\n**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).\n\n**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`.\n\n**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.\n\n**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.\n\n---\n\n## BrAPI-specific features\n\n- **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).\n- **Multi-server session** — `ServerRegistry` maps aliases to live BrAPI connections; one session can span Breedbase, T3, and Sweetpotatobase in parallel.\n- **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.\n- **Capability-aware calls** — `CapabilityRegistry` caches `/serverinfo` per connection and guards every tool call against unsupported endpoints. Falls back to `/calls` when `/serverinfo` is sparse.\n- **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.\n- **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).\n- **Async-search transparency** — `brapi_find_genotype_calls` and `brapi_raw_search` handle the `POST /search/{noun}` → `GET /search/{noun}/{id}` 202-retry pattern automatically.\n- **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.\n- **Image content** — `brapi_get_image` fetches bytes inline as MCP `type: image` blocks, preferring `/images/{id}/imagecontent` with `imageURL` fallback.\n- **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`.\n- **Auth variants in one schema** — tagged-union covers `none` / `bearer` / `api_key` / `sgn` (session-token exchange) / `oauth2` (client-credentials).\n- **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.\n\nBuilt 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.\n\n---\n\n## Working with dataframes\n\nWhen 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.\n\nDataframe 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.\n\n```text\n1. brapi_find_observations { studies: [\"s-422\"] }\n   → first-page rows inline + dataframe.tableName = \"df_<uuid>\" (when totalCount > loadLimit)\n2. brapi_dataframe_describe { dataframe: \"df_<uuid>\" }\n   → schema + provenance (originating tool, baseUrl, query, expiry)\n3. brapi_dataframe_query { sql: \"SELECT germplasmName, value FROM df_<uuid> WHERE observationVariableDbId = 'V1' LIMIT 100\" }\n   → typed columns + bounded rows\n4. brapi_dataframe_query { sql: \"SELECT COUNT(*) AS n, AVG(CAST(value AS DOUBLE)) AS mean FROM df_<uuid> WHERE observationVariableDbId = 'V1'\" }\n   → aggregate without round-tripping all rows\n```\n\nDataframes auto-expire via TTL (`BRAPI_DATASET_TTL_SECONDS`, default 24h). Set `BRAPI_CANVAS_DROP_ENABLED=true` to expose `brapi_dataframe_drop` for explicit cleanup.\n\n---\n\n## Getting started\n\nAdd to your MCP client config — pick one runner:\n\n```json\n{\n  \"mcpServers\": {\n    \"brapi-mcp-server\": {\n      \"type\": \"stdio\",\n      \"command\": \"bunx\",\n      \"args\": [\"@cyanheads/brapi-mcp-server@latest\"],\n      \"env\": { \"MCP_TRANSPORT_TYPE\": \"stdio\", \"MCP_LOG_LEVEL\": \"info\" }\n    }\n  }\n}\n```\n\nSwap `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`.\n\nFor Streamable HTTP:\n\n```sh\nMCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http\n# Server listens at http://localhost:3010/mcp\n```\n\nNo 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).\n\n**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).\n\n---\n\n## Configuration\n\nEvery variable is optional.\n\n| Variable | Description | Default |\n|:---------|:------------|:--------|\n| `BRAPI_DEFAULT_BASE_URL` | Default BrAPI v2 base URL (e.g. `https://test-server.brapi.org/brapi/v2`). | — |\n| `BRAPI_DEFAULT_USERNAME` / `_PASSWORD` | SGN session-token auth for the default connection. | — |\n| `BRAPI_DEFAULT_OAUTH_CLIENT_ID` / `_OAUTH_CLIENT_SECRET` | OAuth2 client-credentials for the default connection. | — |\n| `BRAPI_DEFAULT_API_KEY` / `_API_KEY_HEADER` | Static API key for the default connection. | header `Authorization` |\n| `BRAPI_BUILTIN_ALIASES_DISABLED` | Comma-separated alias names (case-insensitive) to remove from the built-in registry. | — |\n| `BRAPI_LOAD_LIMIT` | In-context row cap returned by `find_*` tools before spilling to a canvas dataframe. | `1000` |\n| `BRAPI_PAGE_SIZE` | Upstream `pageSize` used during canvas spillover walks (decoupled from `BRAPI_LOAD_LIMIT`). Dataframe ceiling = `pageSize × 50`. | `1000` |\n| `BRAPI_MAX_CONCURRENT_REQUESTS` | Per-connection concurrency cap. | `4` |\n| `BRAPI_RETRY_MAX_ATTEMPTS` / `BRAPI_RETRY_BASE_DELAY_MS` | Retry policy for 429/5xx with exponential backoff. | `3` / `500` |\n| `BRAPI_REQUEST_TIMEOUT_MS` | Per-request HTTP timeout. | `30000` |\n| `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` |\n| `BRAPI_SEARCH_POLL_TIMEOUT_MS` / `_INTERVAL_MS` | Async `/search` polling budget + interval. | `60000` / `1000` |\n| `BRAPI_DATASET_TTL_SECONDS` | TTL for dataframe provenance metadata persisted alongside spilled rows. | `86400` |\n| `BRAPI_REFERENCE_CACHE_TTL_SECONDS` | TTL for programs / trials / locations / crops cache. | `3600` |\n| `BRAPI_ALLOW_PRIVATE_IPS` | Allow RFC 1918 / loopback targets. Dev-only. | `false` |\n| `BRAPI_ENABLE_WRITES` | Opt-in for `brapi_submit_observations` registration. | `false` |\n| `BRAPI_GENOTYPE_CALLS_MAX_PULL` | Upstream row ceiling per `brapi_find_genotype_calls` invocation. Max 500,000. | `100000` |\n| `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` |\n| `BRAPI_CANVAS_DROP_ENABLED` | Opt-in for `brapi_dataframe_drop` registration. Off by default; dataframes expire via TTL when left unmanaged. | `false` |\n| `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. | — |\n| `BRAPI_CANVAS_MAX_ROWS` / `BRAPI_CANVAS_QUERY_TIMEOUT_MS` | Per-query response row cap and wall-clock timeout for `brapi_dataframe_query`. | `10000` / `30000` |\n| `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` |\n| `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` |\n| `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` |\n\nPer-alias overrides follow the `BRAPI_<ALIAS>_*` pattern — see [`.env.example`](./.env.example) for every override and inline comments.\n\n### Per-alias credentials\n\n`brapi_connect` resolves `baseUrl` and `auth` from env vars when the agent omits them — credentials never enter the LLM context. Four layers of precedence:\n\n1. **Explicit agent input** — always wins.\n2. **Per-alias env vars** — `BRAPI_<ALIAS>_*` (uppercased, hyphens → underscores: `my-server` → `BRAPI_MY_SERVER_*`).\n3. **Built-in known-server registry** — see [Built-in aliases](#built-in-aliases).\n4. **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.\n\nEach alias carries **one** credential family — auth mode is derived from which fields are set:\n\n| Vars set | Resolved `mode` |\n|:---------|:----------------|\n| `_USERNAME` + `_PASSWORD` | `sgn` (Breedbase `/token` exchange) |\n| `_BEARER_TOKEN` | `bearer` |\n| `_API_KEY` (+ optional `_API_KEY_HEADER`) | `api_key` |\n| `_OAUTH_CLIENT_ID` + `_OAUTH_CLIENT_SECRET` (+ optional `_OAUTH_TOKEN_URL`) | `oauth2` |\n| _(none set)_ | `none` |\n\nMixing families within an alias raises a `ValidationError`.\n\n```sh\n# .env — attach write credentials to the built-in 'bti-cassava' alias\nBRAPI_BTI_CASSAVA_USERNAME=alice\nBRAPI_BTI_CASSAVA_PASSWORD=...\n# (BASE_URL omitted — built-in registry covers it)\n\n# Static API key as alias 'prod'\nBRAPI_PROD_BASE_URL=https://my-brapi.example.com/brapi/v2\nBRAPI_PROD_API_KEY=...\nBRAPI_PROD_API_KEY_HEADER=X-API-Key\n```\n\nThen the agent calls `brapi_connect({ alias: 'bti-cassava' })` — no `baseUrl`, no `auth`, no secrets in the prompt.\n\n### Built-in aliases\n\nThe 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/).\n\n| Alias | Upstream | Hosted by | Crop | Notes |\n|:------|:---------|:----------|:-----|:------|\n| `bti-cassava` | [cassavabase.org](https://cassavabase.org/) | Boyce Thompson Institute | Cassava | NextGen Cassava |\n| `bti-sweetpotato` | [sweetpotatobase.org](https://sweetpotatobase.org/) | Boyce Thompson Institute | Sweet potato | |\n| `bti-breedbase-demo` | [breedbase.org](https://breedbase.org/) | Boyce Thompson Institute | _Demo_ | Sample data only — onboarding + tests. |\n| `t3-wheat` | [wheat.triticeaetoolbox.org](https://wheat.triticeaetoolbox.org/) | Triticeae Toolbox (T3) | Wheat | Wheat CAP / IWYP. |\n| `t3-oat` | [oat.triticeaetoolbox.org](https://oat.triticeaetoolbox.org/) | Triticeae Toolbox (T3) | Oat | Global Oat Genetics Database. |\n| `t3-barley` | [barley.triticeaetoolbox.org](https://barley.triticeaetoolbox.org/) | Triticeae Toolbox (T3) | Barley | T-CAP / US Wheat & Barley Scab Initiative. |\n\nSet `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.\n\n**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).\n\n---\n\n## Running the server\n\n```sh\n# Hot-reload dev (Bun runs TS directly)\nbun --watch src/index.ts\n\n# Production\nbun run rebuild\nbun run start            # transport via MCP_TRANSPORT_TYPE (stdio default)\nbun run start:stdio      # or pin explicitly\nbun run start:http\n\n# Checks\nbun run devcheck         # lint + format + typecheck + security + changelog sync\nbun run test             # Vitest\nbun run lint:mcp         # validate MCP definitions\n```\n\n### Docker\n\n```sh\ndocker build -t brapi-mcp-server .\ndocker run --rm -p 3010:3010 brapi-mcp-server\n```\n\nDefaults 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.\n\n### Deployment shapes\n\n`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.\n\n| Shape | Settings | Isolation | Best for |\n|:------|:---------|:----------|:---------|\n| **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. |\n| **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. |\n| **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. |\n\n**Shape selection guide:**\n\n- **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'`.\n- **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.\n- **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.\n- **Stdio.** Always one session; isolation is moot. The flag has no effect.\n- **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.\n\n**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.\n\n---\n\n## Development\n\nSee [`CLAUDE.md`](./CLAUDE.md) for full architectural rules. Short version:\n\n- Handlers throw, framework catches — no `try/catch` in tool logic\n- Use `ctx.log` for logging, `ctx.state` for storage — no `console`, no direct persistence\n- Register new tools in the `tools` array of `createApp()` in `src/index.ts`\n- Wrap upstream calls: validate raw → normalize → return output schema; never fabricate missing fields\n\n```sh\ngit clone https://github.com/cyanheads/brapi-mcp-server.git\ncd brapi-mcp-server\nbun install\ncp .env.example .env       # edit if you need credentials\nbun run devcheck && bun run test\n```\n\nPRs welcome.\n\n---\n\n## License\n\nApache-2.0 — see [LICENSE](LICENSE).\n",
  "bytes": 30499,
  "sha": "61e8003964b3947ed39b832626a3e03b665cc664ef0ee67d67ce29459ee9b077",
  "repo_slug": "cyanheads/brapi-mcp-server",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_cyanheads_brapi_mcp_server_27672d5a/readme"
}