Collaborative BrAPI v2.1 MCP workspace — studies, germplasm, genotypes across Breedbase, T3, more.
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.
Public Hosted Server: https://brapi.caseyjhand.com/mcp
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.
| Tool | Description |
|---|---|
brapi_connect | Authenticate to a BrAPI v2 server, register the connection under an alias, and return the full orientation envelope in one call. |
brapi_server_info | Re-fetch the orientation envelope for a registered alias, optionally forcing a capability refresh. |
brapi_describe_filters | List valid filter names for a BrAPI endpoint — companion lookup for extraFilters on any find_* tool. |
brapi_find_studies | Find studies by crop, trial type, season, location, or program, with distributions and dataframe spillover. |
brapi_get_study | Fetch a study with program/trial/location resolved and companion counts (observations, units, variables). |
brapi_find_germplasm | Find germplasm by name, synonym, accession, PUI, crop, or free text, with distributions and dataframe spillover. |
brapi_get_germplasm | Fetch a germplasm with attributes, direct parents, and companion counts (studies, parents, descendants). |
brapi_walk_pedigree | BFS-walk ancestry or descendancy as a deduplicated DAG with cycle detection and depth limits. |
brapi_find_variables | Find observation variables by name, trait class, ontology term, or free text, ranked via OntologyResolver. |
brapi_find_observations | Pull observation records by study, germplasm, variable, season, or unit, with dataframe spillover. |
brapi_find_images | Filter image metadata by unit, observation, study, ontology term, or MIME type. Bytes via brapi_get_image. |
brapi_get_image | Fetch image bytes for up to 5 imageDbIds inline as type: image content blocks. |
brapi_find_locations | Find research stations by country, type, abbreviation, or bounding box. |
brapi_find_variants | Find variant records by variant set, reference, or genomic region. |
brapi_find_genotype_calls | Pull genotype calls via async-search polling, bounded by an upstream pull ceiling. |
brapi_dataframe_describe | List dataframes (or describe one) with column schema, row counts, and originating-source provenance. |
brapi_dataframe_query | Run read-only SQL across in-memory dataframes (DuckDB-backed). |
brapi_dataframe_drop | Opt-in. Drop a dataframe by name. Idempotent. |
brapi_dataframe_export | Opt-in, stdio-only. Export a dataframe to disk as CSV, Parquet, or JSON. |
brapi_build_phenotype_matrix | Build a germplasm × trait matrix from one or more studies, materialized as a canvas dataframe. |
brapi_germplasm_performance | Per-variable performance aggregates (n, mean, median, sd, min, max) for a single germplasm across its studies. |
brapi_export_genotype_matrix | Export genotype calls for a variant set as a germplasm × variant matrix, plus VCF-lite / PLINK serialization. |
brapi_submit_observations | Opt-in. Two-phase observation write — preview validates, apply confirms and writes. |
brapi_raw_get | Passthrough to any BrAPI GET /{path} endpoint not covered by a curated tool. |
brapi_raw_search | Passthrough to any POST /search/{noun} endpoint, with async polling handled transparently. |
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.
| Resource | Description |
|---|---|
brapi://server/info | Orientation envelope for the default connection — mirrors brapi_server_info. |
brapi://calls | Raw capability profile (/serverinfo + /calls) for the default connection. |
brapi://study/{studyDbId} | Single study record with program/trial/location resolved — mirrors brapi_get_study. |
brapi://germplasm/{germplasmDbId} | Single germplasm record with attributes and parents — mirrors brapi_get_germplasm. |
brapi://filters/{endpoint} | Filter catalog for one endpoint — mirrors brapi_describe_filters. |
brapi://variable/{observationVariableDbId} | Single observation-variable record (trait, scale, method, ontology). |
| Prompt | Description |
|---|---|
brapi_eda_study | EDA playbook for one study — orient, variables, coverage, missing data, outliers, pedigree, then a structured report. Args: studyDbId, optional alias. |
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. |
brapi_connect toolbaseUrl 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 contextalias (default default, pattern ^[a-zA-Z0-9_-]+$) registers multiple concurrent connections in one sessionnone / bearer / api_key / sgn (Breedbase /token exchange) / oauth2 (client-credentials)auth_token_exchange_failed, auth_no_access_tokenbrapi_server_infobrapi_server_info toolalias optional (defaults to the connection registered under default); forceRefresh (default false) bypasses the cached capability profileunknown_aliasbrapi_connectbrapi_describe_filters toolendpoint required — one of studies, germplasm, observations, variables, images, variants, locationsname, type (string / integer / number / boolean / date / string[] / integer[]), description, and an example valueunknown_endpoint (response carries availableEndpoints as recovery data)brapi_find_studies toolcrop, trialTypes, seasons, locations, programs, trials, studyNames, active, plus extraFilters passthroughloadLimit caps in-context rows; beyond it the full result set materializes as a canvas dataframe (query with brapi_dataframe_query)distributions cover programName, studyType, seasons, locationName, commonCropNameunknown_alias, all_filters_dropped (every supplied filter was unsupported by the active dialect)totalCount, returnedCount, appliedFilters, refinementHint, notice, warningsbrapi_get_study toolstudyDbId required; resolves program, trial, and location FKs inlineobservationCount, observationUnitCount, variableCount — omitted (with a warning) rather than reported as a server-wide total when the upstream can't scope a count to the studyunknown_alias, study_not_foundbrapi_find_germplasm toolnames, germplasmDbIds, germplasmPUIs, accessionNumbers, crops, synonyms, collections, genus, species, plus extraFilterstext is a client-side substring match against germplasmName, accessionNumber, defaultDisplayName, and registered synonyms — combine with a server-side filter to narrow the upstream pull firstloadLimit caps in-context rows; beyond it the full result set materializes as a canvas dataframedistributions cover commonCropName, genus, species, collection, countryOfOriginCodeunknown_alias, all_filters_droppedbrapi_get_germplasm toolgermplasmDbId required; returns attributes (/germplasm/{id}/attributes) and direct parents (/germplasm/{id}/pedigree)studyCount, directParentCount, directDescendantCount (from /germplasm/{id}/progeny) — signals for pedigree depth and observation coverageunknown_alias, germplasm_not_foundbrapi_walk_pedigree toolgermplasmDbIds, walked concurrently; direction is ancestors (default), descendants, or both; maxDepth 1–10 (default 3)truncated when reacheddepthReached, rootCount, leafCount, cycleCount, deadEndCountloadLimit bounds the inline nodes/edges preview; beyond it both sets spill to JOINable canvas dataframes (nodesDataframe, edgesDataframe)unknown_aliasbrapi_find_variables toolvariables, variableNames, variablePUIs, traitClasses, ontologies, studies, methods, scales, crop, plus extraFilterstext 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 droppedontologyCandidates in the response carries the ranked matches with their match sourceloadLimit caps in-context rows; beyond it the full result set materializes as a canvas dataframeunknown_alias, all_filters_droppedbrapi_find_observations toolstudies, germplasm, variables, observationUnits, observations, seasons, programs, trials, observationLevels, timestampFrom/timestampTo, plus extraFiltersloadLimit caps in-context rows; beyond it the full result set materializes as a canvas dataframedistributions cover observationVariableName, studyName, germplasmName, observationLevel, seasonunknown_alias, all_filters_droppedbrapi_find_images toolimages, observationUnits, observations, studies, imageFileNames, mimeTypes, descriptiveOntologyTerms, plus extraFiltersbrapi_get_imageloadLimit caps in-context rows; beyond it the full result set materializes as a canvas dataframeunknown_alias, all_filters_droppedbrapi_get_image toolimageDbIds per call/images/{id}/imagecontent; falls back to the metadata imageURL — source on each payload names which path served iterrors[] 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 wholeunknown_alias, images_unsupported (server doesn't advertise /images)brapi_find_locations toollocations, locationNames, countryCodes (ISO 3166-1 alpha-3), countryNames (free-form English, resolved client-side to alpha-3), locationTypes, abbreviations, plus extraFiltersbbox (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"loadLimit caps in-context rows; beyond it the full result set materializes as a canvas dataframeunknown_alias, all_filters_droppedbrapi_find_variants toolvariantSets, variants, references, referenceName + start/end (1-based inclusive/exclusive genomic region), plus extraFiltersloadLimit caps in-context rows; beyond it the full result set materializes as a canvas dataframedistributions cover variantType, referenceName, variantSetDbIdunknown_alias, all_filters_droppedbrapi_find_genotype_calls toolvariantSetDbId, variantSetDbIds, germplasmDbIds, callSetDbIds, or variantDbIds — unfiltered pulls are rejectedBRAPI_GENOTYPE_CALLS_MAX_PULL (default 100,000, max 500,000) via the async POST /search/calls → GET /search/calls/{id} patternloadLimit bounds the inline preview; the full collected set materializes as a dataframe when it exceeds loadLimitunknown_alias, no_filters, search_endpoint_disabled (dialect marks this server's search route as known-dead)brapi_dataframe_describe tooldataframe optional — omit to list all, or name one for full detail (columns, row count, provenance)baseUrl, query, expiry) is present only for auto-registered df_* dataframes, not user-derived ones from registerAslist_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 workspacebrapi_dataframe_query toolsql 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 separatelyLIMIT/OFFSET is the paging idiom; projection and aggregation (COUNT, GROUP BY, AVG) summarize without materializing every rowregisterAs (letters/digits/underscore, ≤63 chars) persists the result as a new dataframe; preview (≤1000) and rowLimit bound what's returned inlinesql_rejected (carries the granular gate reason on data.gateReason)truncated, shown, cap, noticebrapi_dataframe_drop toolBRAPI_CANVAS_DROP_ENABLED=true — omitted from tools/list otherwisedropped: false (not an error) for an unknown namebrapi_dataframe_export toolBRAPI_EXPORT_DIR, stdio-only — omitted from tools/list under HTTP transport or when unsetformat is csv, parquet, or json; optional columns (thin projection) or sql (full SELECT, mutually exclusive with columns) materializes a temporary derived table firstfilename rejects path separators and .. segments; omit for a timestamp-suffixed defaultexport_dir_unset, dataframe_not_found, invalid_filename, mutually_exclusive_projectionbrapi_build_phenotype_matrix toolstudies required (≥1) — study-anchored to avoid full-table scans; optional variables/germplasm subsetsshape: 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")observationVariableDbId; variableLegend maps them back to display namesunknown_alias, all_filters_dropped, no_observation_path (neither /observations nor /observationunits returned data)truncated, shown, cap, noticebrapi_germplasm_performance toolgermplasmDbId required; discovers the germplasm's studies automatically (capped at 200) unless an explicit studyDbIds set is supplied, which skips discovery entirelyn, mean, median, sd (omitted when n < 2 or non-numeric), min/max, studyCount, studyDbIds, seasonsunknown_alias, germplasm_not_foundbrapi_export_genotype_matrix toolvariantSetDbId required; format is matrix-json (dataframe only), vcf-lite (VCF-subset text in vcf, plus dataframe), or plink (.ped/.map text, plus dataframe)maxCalls/maxColumns can only lower the deployment ceilings (BRAPI_GENOTYPE_CALLS_MAX_PULL, BRAPI_GENOTYPE_MATRIX_MAX_COLUMNS), never raise themvariantColumnLegend maps SQL-safe column names back to original variant IDs; truncated names which ceiling fired when the matrix is incompleteunknown_alias, no_filters, search_endpoint_disabledbrapi_submit_observations toolstudyDbId required; 1–5,000 observation rows; observationDbId presence on a row routes it to PUT, absence to POSTmode: "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 probeforce: true skips the confirmation round — only for out-of-band-authorized writesBRAPI_ENABLE_WRITES=true to register; scoped to brapi:write:observationsunknown_alias, observations_unsupported, study_not_found, post_unsupported, put_unsupported, user_declinedbrapi_raw_get toolpath (relative BrAPI route, e.g. /samples) + optional params; last-resort escape hatch for endpoints no curated tool coverssuggestion when a curated tool exists for the same endpointloadLimit and the result is a list shape; skipped when the caller drives paging via params.page/params.pageSizeunknown_alias, cross_origin_path (a full URL was passed instead of a relative route)brapi_raw_search toolnoun (e.g. observations, calls, germplasm) + body posted verbatim to POST /search/{noun}; async polling resolved transparently, kind reports sync or asyncsuggestion when a curated tool covers the same nounbrapi_raw_getunknown_alias, search_endpoint_disabledbrapi://server/info resourcedefault connectionunknown_aliasbrapi://calls resource/serverinfo + /calls profile (server identity, crops, supported services) for the default connectionunknown_aliasbrapi://study/{studyDbId} resourcebrapi_get_study, addressed by URI on the default connectionunknown_alias, study_not_foundbrapi://germplasm/{germplasmDbId} resourcebrapi_get_germplasm, addressed by URI on the default connectionunknown_alias, germplasm_not_foundbrapi://filters/{endpoint} resourcebrapi_describe_filters; listing the resource collection returns one entry per supported endpointunknown_endpointbrapi://variable/{observationVariableDbId} resource/variables/{id} record (trait, scale, method, ontology) on the default connection — the single-record counterpart to brapi_find_variablesunknown_alias, variable_not_foundbrapi_eda_study promptstudyDbId required; alias optionalbrapi_get_study, enumerate variables, pull observation coverage, quantify missing data, flag numeric outliers (IQR), and an optional pedigree walk on the top-observed germplasmbrapi_meta_analysis promptgermplasmDbIds (comma-separated) and traitName required; alias optional (run once per alias for multi-server analyses)Built on @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.
BrAPI-specific:
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_queryspec / 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 downcastServerRegistry holds live connections under named aliases; six public Breedbase/T3 endpoints resolve out-of-the-box with no env varsCapabilityRegistry caches /serverinfo and guards every call against unsupported endpoints; a per-connection concurrency cap and exponential-backoff retry cover 429/5xxnone / bearer / api_key / sgn session-token exchange / oauth2 client-credentials), resolved per alias from env vars so credentials never enter the LLM contextAgent-friendly output:
brapi_dataframe_describe reports the originating tool, baseUrl, and query for every auto-registered df_<uuid> tablebrapi_get_image returns per-item errors[] and warnings[] rows instead of failing the whole batch when some images can't be loadedbrapi_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 stringsfind_* 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 doesWhen 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.
1. brapi_find_observations { studies: ["s-422"] }
→ first-page rows inline + dataframe.tableName = "df_<uuid>" (when totalCount > loadLimit)
2. brapi_dataframe_describe { dataframe: "df_<uuid>" }
→ schema + provenance (originating tool, baseUrl, query, expiry)
3. brapi_dataframe_query { sql: "SELECT germplasmName, value FROM df_<uuid> WHERE observationVariableDbId = 'V1' LIMIT 100" }
→ typed columns + bounded rows
Dataframe names are capability tokens, not row-level ACLs — anyone holding the name within the same session or tenant bucket (see 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.
A public instance is available at https://brapi.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"brapi-mcp-server": {
"type": "streamable-http",
"url": "https://brapi.caseyjhand.com/mcp"
}
}
}
Add the following to your MCP client configuration file.
{
"mcpServers": {
"brapi-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/brapi-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"brapi-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/brapi-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"brapi-mcp-server": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/brapi-mcp-server:latest"]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
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.
@duckdb/node-api is a required dependency — supported on Linux/macOS/Windows × x64 plus Linux/macOS arm64 (no Windows arm64, no Cloudflare Workers).git clone https://github.com/cyanheads/brapi-mcp-server.git
cd brapi-mcp-server
bun install
cp .env.example .env
# edit .env if you need credentials or non-default settings
Every variable is optional.
| Variable | Description | Default |
|---|---|---|
BRAPI_DEFAULT_BASE_URL | Default BrAPI v2 base URL (e.g. https://test-server.brapi.org/brapi/v2). | — |
BRAPI_DEFAULT_USERNAME / _PASSWORD | SGN session-token auth for the default connection. | — |
BRAPI_DEFAULT_OAUTH_CLIENT_ID / _OAUTH_CLIENT_SECRET | OAuth2 client-credentials for the default connection. | — |
BRAPI_DEFAULT_API_KEY / _API_KEY_HEADER | Static API key for the default connection. | header Authorization |
BRAPI_BUILTIN_ALIASES_DISABLED | Comma-separated alias names (case-insensitive) to remove from the built-in registry. | — |
BRAPI_LOAD_LIMIT | In-context row cap returned by find_* tools before spilling to a canvas dataframe. | 1000 |
BRAPI_PAGE_SIZE | Upstream pageSize used during canvas spillover walks (decoupled from BRAPI_LOAD_LIMIT). Dataframe ceiling = pageSize × 50. | 1000 |
BRAPI_MAX_CONCURRENT_REQUESTS | Per-connection concurrency cap. | 4 |
BRAPI_RETRY_MAX_ATTEMPTS / BRAPI_RETRY_BASE_DELAY_MS | Retry policy for 429/5xx with exponential backoff. | 3 / 500 |
BRAPI_REQUEST_TIMEOUT_MS | Per-request HTTP timeout. | 30000 |
BRAPI_COMPANION_TIMEOUT_MS | Tighter timeout for non-critical companion enrichments (FK lookups, count probes); companions also bypass the retry budget. | 8000 |
BRAPI_SEARCH_POLL_TIMEOUT_MS / _INTERVAL_MS | Async /search polling budget + interval. | 60000 / 1000 |
BRAPI_DATASET_TTL_SECONDS | TTL for dataframe provenance metadata persisted alongside spilled rows. | 86400 |
BRAPI_REFERENCE_CACHE_TTL_SECONDS | TTL for programs / trials / locations / crops cache. | 3600 |
BRAPI_ALLOW_PRIVATE_IPS | Allow RFC 1918 / loopback targets. Dev-only. | false |
BRAPI_ENABLE_WRITES | Feature flag. Registers brapi_submit_observations when true. | false |
BRAPI_GENOTYPE_CALLS_MAX_PULL | Upstream row ceiling per brapi_find_genotype_calls invocation. Max 500000. | 100000 |
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 |
BRAPI_CANVAS_DROP_ENABLED | Feature flag. Registers brapi_dataframe_drop when true; dataframes still expire via TTL when left unmanaged. | false |
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. | — |
BRAPI_CANVAS_MAX_ROWS / BRAPI_CANVAS_QUERY_TIMEOUT_MS | Per-query response row cap and wall-clock timeout for brapi_dataframe_query. | 10000 / 30000 |
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 |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for HTTP server. | 3010 |
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 |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
STORAGE_PROVIDER_TYPE | Storage backend. | in-memory |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
Per-alias overrides follow the BRAPI_<ALIAS>_* pattern. See .env.example for the full list of optional overrides.
brapi_connect resolves baseUrl and auth from env vars when the agent omits them — credentials never enter the LLM context. Four layers of precedence:
BRAPI_<ALIAS>_* (uppercased, hyphens → underscores: my-server → BRAPI_MY_SERVER_*).BRAPI_DEFAULT_*, only when the alias differs from default. Not layered on top of a built-in URL — defaults belong to the default server.Each alias carries one credential family — auth mode is derived from which fields are set:
| Vars set | Resolved mode |
|---|---|
_USERNAME + _PASSWORD | sgn (Breedbase /token exchange) |
_BEARER_TOKEN | bearer |
_API_KEY (+ optional _API_KEY_HEADER) | api_key |
_OAUTH_CLIENT_ID + _OAUTH_CLIENT_SECRET (+ optional _OAUTH_TOKEN_URL) | oauth2 |
| (none set) | none |
Mixing families within an alias raises a ValidationError.
# .env — attach write credentials to the built-in 'bti-cassava' alias
BRAPI_BTI_CASSAVA_USERNAME=alice
BRAPI_BTI_CASSAVA_PASSWORD=...
# (BASE_URL omitted — built-in registry covers it)
# Static API key as alias 'prod'
BRAPI_PROD_BASE_URL=https://my-brapi.example.com/brapi/v2
BRAPI_PROD_API_KEY=...
BRAPI_PROD_API_KEY_HEADER=X-API-Key
Then the agent calls brapi_connect({ alias: 'bti-cassava' }) — no baseUrl, no auth, no secrets in the prompt.
The server ships with a curated registry of public BrAPI v2 endpoints. Each resolves out-of-the-box; the orientation envelope surfaces license, citation, and homepage in its attribution block under Creative Commons Attribution.
| Alias | Upstream | Hosted by | Crop | Notes |
|---|---|---|---|---|
bti-cassava | cassavabase.org | Boyce Thompson Institute | Cassava | NextGen Cassava |
bti-sweetpotato | sweetpotatobase.org | Boyce Thompson Institute | Sweet potato | |
bti-breedbase-demo | breedbase.org | Boyce Thompson Institute | Demo | Sample data only — onboarding + tests. |
t3-wheat | wheat.triticeaetoolbox.org | Triticeae Toolbox (T3) | Wheat | Wheat CAP / IWYP. |
t3-oat | oat.triticeaetoolbox.org | Triticeae Toolbox (T3) | Oat | Global Oat Genetics Database. |
t3-barley | barley.triticeaetoolbox.org | Triticeae Toolbox (T3) | Barley | T-CAP / US Wheat & Barley Scab Initiative. |
Set BRAPI_<ALIAS>_BASE_URL to repoint at a staging mirror or fork (env wins over the built-in URL — hyphens in the alias become underscores in the env var, so t3-wheat → BRAPI_T3_WHEAT_BASE_URL). Set BRAPI_<ALIAS>_USERNAME etc. to attach credentials on top of the built-in URL — each Breedbase instance has its own user table, so write access requires separate registration on each upstream. Use BRAPI_BUILTIN_ALIASES_DISABLED=bti-cassava,t3-wheat to strip specific entries.
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.
# Hot-reload dev (Bun runs TS directly)
bun --watch src/index.ts
# Production
bun run rebuild
bun run start # transport via MCP_TRANSPORT_TYPE (stdio default)
bun run start:stdio # or pin explicitly
bun run start:http
# Checks
bun run devcheck # lint + format + typecheck + security + changelog sync
bun run test # Vitest
bun run lint:mcp # validate MCP definitions
docker build -t brapi-mcp-server .
docker run --rm -p 3010:3010 brapi-mcp-server
Defaults to HTTP transport, stateful session mode (engages the mcp-session-id lifecycle — precondition for BRAPI_SESSION_ISOLATION=true; hijack protection requires layering MCP_AUTH_MODE=jwt|oauth on top), logs to /var/log/brapi-mcp-server. OTel peer deps are installed by default — --build-arg OTEL_ENABLED=false to omit.
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:
| Shape | Settings | Isolation | Best for |
|---|---|---|---|
| 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. |
| 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. |
| 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. |
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.
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.
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools/resources/prompts and inits services. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools | Tool definitions (*.tool.ts). Twenty-five tools across connection, retrieval, analysis, write, and raw-passthrough. |
src/mcp-server/resources | Resource definitions (*.resource.ts). |
src/mcp-server/prompts | Prompt definitions (*.prompt.ts). |
src/services | Domain service integrations — BrAPI client, dialect adapters, canvas bridge, capability registry, ontology resolver, reference-data cache, server registry. |
tests/ | Unit and integration tests mirroring src/. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for logging, ctx.state for tenant-scoped storage — no console, no direct persistence accesstools array of createApp() in src/index.tsIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/brapi-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cyanheads-brapi-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/brapi-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceio.github.cyanheads/brapi-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.