@cyanheads/brapi-mcp-server
This server has a remotely hosted endpoint. Open its connector listing for connection details, authentication requirements, and setup instructions:
Public Hosted Server: https://brapi.caseyjhand.com/mcp
Overview
Plant-breeding data from any BrAPI (Breeding API) v2 server — Breedbase instances like Cassavabase and Sweetpotatobase, the Triticeae Toolbox (T3), and more, several at once. Search studies, germplasm, observations, and genotypes, walk pedigrees, and build phenotype and genotype matrices, with large results queryable in SQL.
Tools
Tool | Description |
| Authenticate to a BrAPI v2 server, register it under an alias, and return the orientation envelope |
| Return the orientation envelope for an alias, optionally refreshing capabilities |
| List valid filter names for an endpoint, for use in any finder's |
| Find studies by crop, trial type, season, location, or program |
| Fetch a study with program, trial, and location resolved, plus companion counts |
| Find germplasm by name, synonym, accession, PUI, crop, or free text |
| Fetch a germplasm with attributes, direct parents, and companion counts |
| Walk ancestry or descendancy as a deduplicated DAG with cycle detection |
| Find observation variables by name, trait class, or ontology term, with free-text ranking |
| Pull observation records by study, germplasm, variable, season, or unit |
| Find image metadata by unit, observation, study, ontology term, or MIME type |
| Fetch up to 5 images inline as image content blocks |
| Find research stations by country, type, abbreviation, or bounding box |
| Find variants by variant set, reference, or genomic region |
| Pull genotype calls through async search, bounded by a deployment pull ceiling |
| List dataframes, or describe one with columns, row count, and provenance |
| Run read-only SQL across dataframes |
| Opt-in. Drop a dataframe by name |
| Opt-in, stdio-only. Export a dataframe to disk as CSV, Parquet, or JSON |
| Build a germplasm × trait matrix from one or more studies as a dataframe |
| Per-variable aggregates (n, mean, median, sd, min, max) for one germplasm across its studies |
| Pivot a variant set's calls into a germplasm × variant matrix, with VCF-lite or PLINK text |
| Opt-in. Two-phase observation write: |
| Passthrough to any |
| Passthrough to any |
Resources
Resource | Description |
| Orientation envelope for the default connection |
| Raw capability profile ( |
| One study with program, trial, and location resolved |
| One germplasm with attributes and parents |
| Filter catalog for one endpoint |
| One observation variable (trait, scale, method, ontology) |
Every resource reads the default connection and mirrors a tool; tool-only clients and multi-server workflows use the tools.
Prompts
Prompt | Description |
| Exploratory-data-analysis playbook for one study, ending in a structured report |
| Cross-study meta-analysis playbook for a germplasm × trait combination |
Related MCP server: andish-mcp
Capability reference
Identifier filters also accept their BrAPI *DbId / *DbIds spellings where equivalent: for example, imageDbId for imageDbIds, studyDbIds for studies, and observationVariableDbIds for variables. Array filters accept a lone string or multiple IDs. Supply one spelling per filter; conflicting spellings are rejected.
brapi_connect tool
baseUrlandauthare optional: omitted values come fromBRAPI_<ALIAS>_*env vars, the built-in aliases, thenBRAPI_DEFAULT_*(see Per-alias credentials);alias(defaultdefault, pattern^[a-zA-Z0-9_-]+$) keeps several servers registered at once;auth.modeisnone,bearer,api_key,sgn(Breedbase/tokenexchange), oroauth2(client credentials)Returns the orientation envelope:
serveridentity,authsummary,capabilities(supported,notableGaps), activedialect,contentcounts,attributionfor built-in servers, andnextToolSuggestionsnaming the entry-point finders this server can serveTyped errors:
auth_session_required,auth_base_url_mismatch,alias_base_url_unset,alias_base_url_invalid,private_address_blocked,auth_token_exchange_failed,auth_no_access_token,upstream_unauthorized,upstream_forbidden,capability_discovery_unavailable(retryable); a failed connect leaves any earlier registration under the alias intactOptional for an alias the server has a URL for (
defaultwhenBRAPI_DEFAULT_BASE_URLis set, a built-in, or aBRAPI_<ALIAS>_BASE_URLalias): any other tool or resource connects it on first use, asbrapi_connectwith nobaseUrlorauthwould, and concurrent first calls share one connect. An alias with no URL of its own still failsunknown_alias, even withBRAPI_DEFAULT_*set, so a mistyped alias never reaches the default server. Those tools and resources declare the connect failures they can hit (the token, discovery, auth-wall,alias_base_url_invalid, andprivate_address_blockederrors above), each with a recovery hint
brapi_server_info tool
aliasoptional;forceRefresh(defaultfalse) refetches the capability profile instead of reading the cacheReturns the same orientation envelope as
brapi_connect
brapi_describe_filters tool
endpointis one ofstudies,germplasm,observations,variables,images,variants,locations;unknown_endpointcarriesavailableEndpointsEach filter has
name,type,description, andexample; the catalog follows the v2.1 spec, and individual servers may honor a subset
brapi_find_studies tool
Filters:
crop,trialTypes,seasons,locations,programs,trials,studyNames,active; on SGN servers (CassavaBase, Breedbase, Sweetpotatobase), which ignore the location filter on/studies,locationsis applied client-side over the scanned studies, andtotalCountis the matched countdistributionsoverprogramName,studyType,seasons,locationName,commonCropNameSGN/Breedbase studies lift missing program IDs and names from
additionalInfo, preserving the raw metadata.
brapi_get_study tool
studyDbIdrequired; resolvesprogram,trial, andlocationinline;study_not_foundwhen the upstream has no such studyOn SGN/Breedbase servers, missing program fields come from
additionalInfo; when the study has no program ID, the fetched parent trial can supply it for program resolution.Companion counts
observationCount,observationUnitCount,variableCount; a count the server can't scope to the study is omitted with a warning, never reported as the server-wide total
brapi_find_germplasm tool
Filters:
names,germplasmDbIds,germplasmPUIs,accessionNumbers,crops,synonyms,collections,genus,species;textis a client-side substring match on name, PUI, accession, display name, and synonyms that drops non-matching rows. Its first request usesBRAPI_PAGE_SIZEindependently of inlineloadLimit; it scans every upstream row when the set fits one dataframe walk (50,000 rows at the default page size). Above that, a first-page miss stops after one request and reports the actual rows scanned; it does not establish absence from the catalog, so pair it with a server-side filter.distributionsovercommonCropName,genus,species,collection,countryOfOriginCode
brapi_get_germplasm tool
germplasmDbIdrequired; returnsattributesand directparents;germplasm_not_foundwhen the upstream has no such germplasmCompanion counts
studyCount,directParentCount,directDescendantCount
brapi_walk_pedigree tool
1–20 root
germplasmDbIds(germplasmDbIdalso accepted);directionisancestors(default),descendants, orboth;maxDepth1–10 (default 3); the walk stops at 1,000 nodes or 10,000 edges and setstruncatedbothis the union of the two walks: ancestors expand only through/pedigreeand descendants only through/progeny, so a descendant's other parent is not pulled inOne edge per parent–child pair;
cycleCountcounts the edges markedisCycle, where a germplasm becomes its own ancestor — shared ancestors and siblings are not cyclesleafCountcounts graph terminals, including depth-boundary nodes;confirmedLeafCountcounts non-root nodes whose relevant lookups successfully returned no relatives.frontier/frontierCountdistinguish depth-limited, safety-skipped, failed and unsupported directions;deadEndCountstill counts nodes with failed lookupsnodesandedgeseach fitloadLimitand a 24,000-byte budget for both array JSON and serialized rendered rows. Trimmed previews retain the discovered graph innodesDataframeandedgesDataframe; a first row over budget yields zero inline rows. Preview trimming never changes traversaltruncatedSafety-capped graph tables are marked
truncatedand contain only discovered nodes/edges.knownOmittedRelativeCountandomittedRelativesDataframeretain already-fetched omitted relatives withgermplasmDbId,expansionNodeDbId,sideandordinal; unrequested directions have unknown counts. Large frontiers also stage afrontierDataframeRetrieve omitted IDs with
SELECT germplasmDbId, expansionNodeDbId, side FROM <table> ORDER BY ordinal LIMIT 20 OFFSET 0, advance by rows actually shown, then walk those IDs in batches of up to 20 roots. If zero fit, narrow the projection or aggregate; these tables do not resume traversaldiagram: "mermaid" | "dot"adds the discovered graph (including rows beyond the preview), with partial coverage when safety-capped; edges are drawn parent → child and loop-closing edges dashed. Either format is left out with a warning past 50,000 characters, and Mermaid past 500 edges
brapi_find_variables tool
Filters:
variables,variableNames,variablePUIs,traitClasses,ontologies,studies,methods,scales,crop. Multiple distinct studies are unioned by variable ID withstudyDbIdsprovenance under one shared page, row and time budget.studyCoveragereports complete, partial, failed and unprocessed studies; successful studies remain available when another fails.textranks every fetched row and moves matches to the top without dropping the rest;ontologyCandidateslists the ranked matches, each withsource(puiMatch,nameMatch,synonymMatch,traitClassMatch)distributionsoverontologyDbId,traitClass,scaleName
brapi_find_observations tool
Filters:
studies,germplasm,variables,observationUnits,observations,seasons,programs,trials,observationLevels,timestampFrom/timestampToStudy-scoped SGN/Breedbase queries use advertised
/observationunits?includeObservations=true, preserving zero-valued measurements. Unit totals drive paging; child fields take precedence over inherited unit metadata, and repeated observation IDs are counted once. Observation-level filters are checked after flattening;appliedFiltersseparates upstream requests from client-side criteriaA GET query with no
studies,trials,observationUnits, orobservationsanchor runs a one-row count probe first and skips the bulk pull when the probe fails or reports more than 5,000 matches;warningssays sodistributionsoverobservationVariableName,studyName,germplasmName,observationLevel,season
brapi_find_images tool
Filters:
images,observationUnits,observations,studies,imageFileNames,mimeTypes,descriptiveOntologyTerms; returns metadata only, with bytes viabrapi_get_imagedistributionsovermimeType,studyName,observationUnitName,descriptiveOntologyTerms
brapi_get_image tool
1–5
imageDbIdsper call, up to 20 MB each;images_unsupportedwhen the server doesn't advertise/imagesEach image's
sourceisimagecontentor theimageURLfallback. Both sources require an effectiveimage/*MIME type (case and parameters normalized; metadata is used when the byte response has no type). Non-image responses and fetch failures land in per-imageerrors[]; successful siblings still returnBytes appear once as
content[]image blocks.images[]carries metadata, MIME, byte size and source only;format()renders metadata without repeating base64
brapi_find_locations tool
Filters:
locations,locationNames,countryCodes(alphabetic codes uppercased; numeric upstream codes preserved),countryNames(English names resolved to alpha-3),locationTypes,abbreviations; unresolved names fail unless an effective country-code restriction remains. Optionalbboxapplies after the fetch; a nonempty box requires all four ordered bounds (minLat,maxLat,minLon,maxLon) or fails before location retrieval. An empty box is omitted.distributionsovercountryCode,locationType;coordinateAxisOrder: "swapped"reports a server that stores coordinates as[lat, lon]
brapi_find_variants tool
Filters:
variantSets,variants,references, and a genomic region ofreferenceName+start(inclusive) /end(exclusive), 0-baseddistributionsovervariantType,referenceName,variantSetDbId
brapi_find_genotype_calls tool
Needs at least one of
variantSetDbId,variantSetDbIds,germplasmDbIds,callSetDbIds,variantDbIds(no_filtersotherwise); optionalcallFormat(VCF,FLAPJACK,DARTSEQ,JSON)Variant sets are listed by
brapi_raw_get({ path: "/variantsets" }), optionally withparams.studyDbId; on Breedbase-family servers they belong to genotyping projects thatbrapi_find_studiesdoes not listdistributionsovercallSetName,variantName,variantSetDbId, plus the server'scallFormatting;search_endpoint_disabledwhen the active dialect marksPOST /search/callsas deadThe upstream pull stops at
BRAPI_GENOTYPE_CALLS_MAX_PULL(default 100,000) and setstruncated; a pull the call's 50 s upstream deadline stops after the first page keeps the calls collected so far, setshasMore, and says so inwarnings;loadLimitand the 24,000-byte inline budget bound only the inline preview, reported throughshown/cap, with every collected call staged in a dataframe when the preview is cut
brapi_dataframe_describe tool
dataframeoptional: omit to list every dataframe, or name one for columns, row count, and provenance (originating tool,baseUrl, query, expiry), which only auto-registereddf_*tables carryOn an HTTP deployment where every caller shares the
defaulttenant, listing without a name works inside an MCP session whenBRAPI_SESSION_ISOLATION=true, and shows only that session's dataframes; a session-less request, or any request with isolation off, fails withlist_all_disabled_on_shared_http
brapi_dataframe_query tool
sqlis a singleSELECT; writes, DDL,COPY,PRAGMA,ATTACH, file reads (a file or URL named as a table included), system-catalog reads, andDESCRIBE/SUMMARIZE/SHOWfail assql_rejected, with the specific reason indata.gateReason(brapi_dataframe_describelists a dataframe's columns and types)A query that fails on the data (a cast that does not convert) fails as
sql_execution_error, a dataframe name that no longer exists asmissing_table, and aregisterAsname already in use asregister_as_clashReturns
rowCount, typedcolumns, and leadingrowsbounded bypreview,rowLimit, andBRAPI_CANVAS_MAX_ROWS, each clamped to the next, then 24,000 bytes on each serialized row array and rendered row section (escaping and markers included).shownis the delivered count;capremains the effective row ceiling. An oversized first row returns zero rows: use a narrower projection or aggregationregisterAs(identifier, ≤63 characters) saves the full result as a new dataframe for chainingPage persisted results with deterministic
ORDER BYandLIMIT/OFFSET, advancing the offset by rows actually returned, not the requested limit. WithoutregisterAs, rerun with it before paging a saved result
brapi_dataframe_drop tool
dataframerequired; returnsdropped: false, not an error, for an unknown nameRegistered only when
BRAPI_CANVAS_DROP_ENABLED=true
brapi_dataframe_export tool
formatiscsv,parquet, orjson; optionalcolumnsorsql(mutually exclusive) andfilename(no path separators or..; omit for a timestamp-suffixed default); returns the absolutepath,sizeBytes, androwCountTyped errors:
export_dir_unset,dataframe_not_found,invalid_filename,mutually_exclusive_projectionRegistered only over stdio with
BRAPI_EXPORT_DIRset
brapi_build_phenotype_matrix tool
studiesrequired (≥1), optionalvariables/germplasmsubsets;shapewide(default) orlong;aggregatemean(default),median,first, orall(always long form);loadLimitcaps matching observations per studyPages each study's observations against the upstream total, preferring embedded observation units on SGN/Breedbase when advertised so zeros are retained; unit totals count raw units, while
loadLimitcounts matching observations. Other servers retain the/observationsand fallback paths. Study / variable / germplasm subsets are checked on returned rowsextraFiltersare forwarded, then checked against the rows the matrix is built from, as onbrapi_find_observations:warningsnames a key the rows contradict and the keys no row can speak toReturns the matrix as a dataframe plus
observationCount,germplasmCount,variableCount, andvariableLegendmapping SQL-safe column names to variable names;truncated/capflag a study whose scan stopped before its total (atloadLimit, the page cap, a failed, empty, or repeated page, or the call deadline) and the notice names it with the rows examinedstudiesretains the requested IDs;coverage.studiesdistinguishescontributed,empty,failed,partial, andunprocessedoutcomes with collected counts.coverage.partialandtruncateddisclose failed or incomplete pulls on both output surfaces. A failed study does not prevent the others from building the matrix; the call fails only when every study failed.no_observation_pathwhen the server exposes neither observation route
brapi_germplasm_performance tool
germplasmDbIdrequired; SGN/Breedbase uses advertised germplasm-scoped embedded units, walking their raw total without the study-discovery cap. Other servers discover up to 200 studies. ExplicitstudyDbIdsskips discovery and restricts collection to those studies; optionalvariablessubset;germplasm_not_foundwhen the upstream has no such germplasmperVariablerows carryn,mean,median,sd(omitted when n < 2 or non-numeric),min,max,studyCount,studyDbIds, andseasonsstudyCount/studyDbIdsremain contributors only. Additivecoveragedistinguishes contributed, empty, failed, partial, and unprocessed studies; scoped-unit scans report raw units, pages, total when known, and completion. An incomplete unit walk cannot establish the unseen study count. Repeated observation IDs are counted once; distinct-ID replicates and zero values remain. The deployment observation limit applies per study, including on the germplasm-unit walk; page, observation, and deadline limits are disclosed, and deadline-skipped studies receive one count warning
brapi_export_genotype_matrix tool
variantSetDbIdrequired (no_filtersotherwise; list sets withbrapi_raw_get({ path: "/variantsets" })), optionalgermplasmDbIds;formatismatrix-json,vcf-lite(addsvcftext), orplink(addsped/maptext), and every format registers the germplasm × variant dataframevariantColumnLegendmaps SQL-safe column names back to variant IDs;search_endpoint_disabledwhen the active dialect marksPOST /search/callsas deadVCF POS and PLINK base-pair positions are one-based: valid zero-based BrAPI
startvalues 0 and 500 export as 1 and 501. Missing or invalid positions retain.(VCF) /0(PLINK) placeholders.maxCalls/maxColumnscan lowerBRAPI_GENOTYPE_CALLS_MAX_PULL/BRAPI_GENOTYPE_MATRIX_MAX_COLUMNSbut never raise them;truncatedmeans a ceiling fired or the call's 50 s upstream deadline ended the call pull early, andwarningsnames which
brapi_submit_observations tool
studyDbIdplus 1–5,000observations; a row withobservationDbIdupdates viaPUT, one without creates viaPOST, and nothing is deletedmode: "preview"(default) returnsvalid,invalid,routingcounts, andperRowWarningswithout writing;mode: "apply"asks the caller to confirm (force: trueskips it) and writes only on the round that redeems the server's record of that prompt, for the same caller, server, study, and rows — a pre-supplied or replayed answer is asked again — and returnsposted,updated, andstudyObservationCount; failures areobservations_unsupported,study_not_found,post_unsupported,put_unsupported, anduser_declinedRegistered only when
BRAPI_ENABLE_WRITES=true; requires thebrapi:write:observationsscope
brapi_raw_get tool
pathis a relative route such as/samples(a full URL fails ascross_origin_path); optionalparamsandloadLimitReturns the envelope (
url,metadata,result) plus asuggestionwhen a curated tool covers the endpoint or takes the IDs it lists. List previews (resultarrays orresult.data) fitloadLimitand 24,000 bytes on each serialized row array and rendered row section, markers and escaping included; non-list results and envelope siblings are preserved. Every fetched row omitted inline stays staged, including an oversized first row that leaves zero inline rowsparams.page/params.pageSizefetches only the requested upstream page; a trimmed page is staged without walking more pages. SQL offsets page that staged table independently of upstream pagination: use deterministicORDER BY, advance by actual rows shown, and use a narrower projection when zero rows fit
brapi_raw_search tool
noun(e.g.observations,calls,germplasm) and abodyposted verbatim toPOST /search/{noun}; async searches are polled to completionReturns
kind(syncorasync),searchResultsDbId,result, and asuggestion; previews and stages likebrapi_raw_get, withbody.page/body.pageSizerestricting collection to the requested upstream page;search_endpoint_disabledwhen the active dialect marks the route as dead
brapi://server/info resource
Orientation envelope for the
defaultconnection asapplication/jsonSame payload as
brapi_server_infocalled with no arguments
brapi://calls resource
Capability profile for the
defaultconnection: supported services with their HTTP methods and versions, plus cropsReflects what
/serverinfo+/callsreturned at the last load
brapi://study/{studyDbId} resource
Same payload as
brapi_get_studyon thedefaultconnectionstudy_not_foundwhen the upstream has no such study
brapi://germplasm/{germplasmDbId} resource
Same payload as
brapi_get_germplasmon thedefaultconnectiongermplasm_not_foundwhen the upstream has no such germplasm
brapi://filters/{endpoint} resource
Same payload as
brapi_describe_filters;unknown_endpointfor an endpoint outside the catalogListing
brapi://filtersreturns one resource per endpoint
brapi://variable/{observationVariableDbId} resource
The
/variables/{id}record (trait, scale, method, ontology) on thedefaultconnectionvariable_not_foundwhen the upstream has no such variable
brapi_eda_study prompt
Arguments:
studyDbIdrequired;aliasoptionalReturns one user message: a six-step playbook (orient, variables, coverage, missing data, IQR outliers, optional pedigree walk) ending in a markdown report with recommended next steps
brapi_meta_analysis prompt
Arguments:
germplasmDbIds(comma-separated) andtraitNamerequired;aliasoptional, run once per alias for multi-server analysesReturns one user message: a seven-step playbook (resolve the trait, discover studies, build the observation table, harmonize scales, summarize per study and across studies, optional pedigree walk) ending in a report that cites every dataframe handle or filter map used
Features
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:
Shared finder contract: the seven filter finders (studies, germplasm, variables, observations, images, locations, variants) take
alias,loadLimit, andextraFilters(keys frombrapi_describe_filters), and returnresults,hasMore,distributions, and an optionaldataframehandle;resultsstops atloadLimitor 24,000 bytes on either array JSON or serialized escaped row text, whichever binds first, retaining the first matched row even when it alone exceeds the budget.truncated/shown/capreport the cutDataframe spillover: when
resultsholds fewer rows than matched, finders stage the matched set (paging the rest of the result, up to 50 pages and 50,000 rows) in a DuckDBdf_<uuid>table and return its handleDialect adapters (
spec,brapi-test,breedbase,cassavabase,bms), detected per connection, translate v2.1 plural filters into the form each server family honors, drop filters it ignores, and switch toPOST /search/{noun}when aGETwould narrow a multi-value filter;BRAPI_<ALIAS>_DIALECTpins oneSeveral connections at once under named aliases, with three public Breedbase servers built in and credentials resolved per alias from env vars, so they stay out of the LLM context
Capability-aware calls: each connection's
/serverinfo+/callsprofile is cached and checked before a tool calls an endpoint; a per-connection concurrency cap and exponential-backoff retries cover 429/5xx
Agent-friendly output:
Upstream names, identifiers, and advisory fields render as literal text. Structured values and generated pedigree diagrams retain their original semantics
Typed failures: every connection-scoped tool and resource fails with
unknown_aliasfor an alias that is neither registered nor configured on the server (a configured alias connects on first use) and withprivate_address_blocked, caching and registering nothing, when its base URL is a private or reserved address the server may not reach; a filter finder fails withall_filters_droppedinstead of widening to an unfiltered pull when the dialect drops every filter supplied, andbrapi_build_phenotype_matrixfails withdialect_all_filters_droppedwhen the dialect drops the study filter for every requested studyAn empty or whitespace-only ID or filter value fails at the input schema instead of reaching the upstream, where a blank filter widens the pull and a blank ID malforms the request path; an empty
textstill means no text filterQuery echo on every finder:
totalCount,returnedCount, andappliedFilters; embedded observation pulls separate upstream requests from client-side criteria, andbrapi_find_studieslists client-sidelocationsseparately. ArefinementHintpoints at a complete dataframe or suggests narrower filters when the scan stopped short; empty results carry anotice, andwarningsdisclose limitationsGraceful partial failure:
brapi_get_imagereturns per-imageerrors[]andwarnings[]rows instead of failing the batchDiscriminated outputs:
brapi_submit_observationsreturns amode-discriminated result (preview/apply),brapi_raw_searchreportskind, andbrapi_get_imagereports each image'ssource
Working with dataframes
When a finder's inline results holds fewer rows than matched — loadLimit or the 24,000-byte inline budget bound first — the response carries a dataframe handle: tableName, rowCount, columns, createdAt, expiresAt, and the upstream totalCount when the server reports one, plus truncated (and maxRows when the row cap fired) when the dataframe holds fewer rows than the upstream total; warnings then says why the walk stopped. Columns renamed to pass the SQL identifier check map back to their upstream keys in columnLegend.
Every response that stages a dataframe — the finders, brapi_raw_get / brapi_raw_search when a row or byte bound trims a list, every matrix from brapi_build_phenotype_matrix and brapi_export_genotype_matrix, and brapi_walk_pedigree when graph/frontier previews are trimmed or fetched relatives are omitted — carries one notice naming each table, its row count, coverage, and brapi_dataframe_describe then brapi_dataframe_query. brapi_dataframe_export is named too where it is registered (stdio with BRAPI_EXPORT_DIR set). On a finder, the same notice says which bound cut results.
1. brapi_find_observations { studies: ["s-422"] }
→ leading rows inline + dataframe.tableName = "df_<uuid>" (when results holds fewer rows than matched)
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 rowsDataframe names are capability tokens, not row-level ACLs: anyone holding a name in the same session or tenant bucket (see Deployment shapes) can read its rows. Provenance lasts BRAPI_DATASET_TTL_SECONDS (default 24h); set BRAPI_CANVAS_DROP_ENABLED=true to expose brapi_dataframe_drop for explicit cleanup.
Getting started
Public Hosted Instance
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"
}
}
}Self-Hosted / Local
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/mcpNo env vars are required: the built-in aliases (bti-cassava, bti-sweetpotato, bti-breedbase-demo) connect as-is, and brapi_connect accepts any other BrAPI v2 URL at runtime. For servers that need a login, set credentials as env vars so passwords, tokens, and keys stay out of the LLM context (see Per-alias credentials).
Prerequisites
Bun v1.4.0 or higher (or Node.js v24+).
@duckdb/node-api, installed as a regular dependency, with prebuilt native bindings for macOS, Linux (glibc and musl), and Windows on x64 and arm64. Cloudflare Workers is not supported.
Installation
Clone the repository:
git clone https://github.com/cyanheads/brapi-mcp-server.gitNavigate into the directory:
cd brapi-mcp-serverInstall dependencies:
bun installConfigure environment:
cp .env.example .env
# edit .env if you need credentials or non-default settingsConfiguration
Every variable is optional.
Variable | Description | Default |
| Base URL for the | — |
| One credential family for the | — |
| API-key header for the |
|
| Comma-separated built-in aliases to remove (case-insensitive). | — |
| Default inline row cap for finders; a 24,000-byte budget also bounds the inline rows, and the matched set spills to a dataframe when either cuts them short. |
|
| Upstream |
|
| Per-connection concurrency cap. |
|
| Retries on 429/5xx and timeouts, and the exponential-backoff base delay. A timed-out request is retried only while the call's 50 s upstream deadline still fits another attempt. |
|
| Per-request HTTP timeout. Every upstream request one tool call makes also shares a 50 s deadline from the call's start, so a stalled server fails the call with a |
|
| Timeout for non-critical enrichment calls (FK lookups, count probes), which also skip retries. |
|
| Async |
|
| Lifetime of dataframe provenance (the handle's |
|
| TTL for cached capability profiles and reference data (programs, trials, locations, crops). |
|
| Allow RFC 1918 / loopback targets. Dev only. |
|
| Scope connections and the default canvas to the MCP session when one exists; |
|
| Feature flag. Registers |
|
| Feature flag. Registers |
|
| Feature flag. Output directory for | — |
| Response row cap and per-query timeout for |
|
| Upstream call ceiling per |
|
| Variant-column ceiling per |
|
| Transport: |
|
| HTTP server port. |
|
| HTTP session mode. This server requires |
|
| Authentication: |
|
| Secret of at least 32 bytes, the same on every instance, that seals the | — |
| Log level ( |
|
| Log each failed tool call's arguments and result (key-name redaction only). |
|
| Storage backend: |
|
| Enable OpenTelemetry. |
|
| Opt-in OTLP log export endpoint; the base | — |
See .env.example for the full list of optional overrides.
Per-alias credentials
brapi_connect fills baseUrl and auth from env vars when the agent omits them, in this order:
Agent input, within the pairing rules below.
Per-alias env vars:
BRAPI_<ALIAS>_*, uppercased with hyphens as underscores (my-server→BRAPI_MY_SERVER_*).Built-in aliases: see Built-in aliases.
BRAPI_DEFAULT_BASE_URL, for an alias with no URL or credentials of its own.
Env credentials go only to the server configured with them:
An alias's credentials pair with its
BRAPI_<ALIAS>_BASE_URL, else its enabled built-in URL. A callerbaseUrlthat points elsewhere fails withauth_base_url_mismatch. Credentials with no URL of their own, including those left behind by a built-in disabled viaBRAPI_BUILTIN_ALIASES_DISABLED, fail withalias_base_url_unsetand are never sent toBRAPI_DEFAULT_BASE_URL.BRAPI_DEFAULT_*credentials attach only when the resolved URL isBRAPI_DEFAULT_BASE_URL; an alias pointed anywhere else connects without auth unless it has credentials of its own. With noBRAPI_DEFAULT_BASE_URLset, default credentials fail withalias_base_url_unset.
URLs compare after normalizing host case, default ports, and trailing slashes. Caller-supplied auth is never mixed with env credentials.
Each alias carries one credential family, and the auth mode follows from which fields are set. Mixing families within an alias raises a ValidationError.
Vars set | Resolved |
|
|
|
|
|
|
|
|
(none set) |
|
BRAPI_<ALIAS>_DIALECT pins the dialect adapter (spec, brapi-test, breedbase, cassavabase, bms) when detection picks the wrong one; auto or unset detects it.
# .env — attach write credentials to the built-in 'bti-cassava' alias
BRAPI_BTI_CASSAVA_USERNAME=alice
BRAPI_BTI_CASSAVA_PASSWORD=...
# (BASE_URL omitted — the 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-KeyThe agent then calls brapi_connect({ alias: 'bti-cassava' }) with no baseUrl, no auth, and no secrets in the prompt, or skips that call: any tool given alias: 'bti-cassava' connects it the same way on first use.
Built-in aliases
These public BrAPI v2 endpoints connect with no configuration. Their orientation envelope carries license, citation, and homepage in an attribution block (Creative Commons Attribution).
Alias | Upstream | Hosted by | Crop | Notes |
| Boyce Thompson Institute | Cassava | NextGen Cassava | |
| Boyce Thompson Institute | Sweet potato | ||
| Boyce Thompson Institute | Demo | Sample data only, for onboarding and tests |
The registry holds only servers verified for anonymous reads. Servers that require login, including the Triticeae Toolbox (T3) wheat, oat, and barley hosts, connect through BRAPI_<ALIAS>_BASE_URL plus credentials (see .env.example).
BRAPI_<ALIAS>_BASE_URL overrides a built-in URL, e.g. to point bti-sweetpotato at a staging mirror via BRAPI_BTI_SWEETPOTATO_BASE_URL. BRAPI_<ALIAS>_USERNAME and friends attach credentials on top of the built-in URL; each Breedbase instance has its own user table, so write access needs a separate account on each. BRAPI_BUILTIN_ALIASES_DISABLED=bti-cassava,bti-breedbase-demo removes entries.
Citation: all three built-ins reference Morales et al. 2022, "Breedbase: a digital ecosystem for modern plant breeding." G3 12(7): jkac078. doi:10.1093/g3journal/jkac078.
Running the server
Local development
Build and run the production version:
# One-time build bun run rebuild # Run the built server bun run start # transport from MCP_TRANSPORT_TYPE (stdio default) bun run start:stdio bun run start:http # Or run from source with hot reload bun --watch src/index.tsRun checks and tests:
bun run devcheck # Lint, format, typecheck, security, changelog sync bun run test # Vitest suite bun run lint:mcp # Validate MCP definitions
Docker
docker build -t brapi-mcp-server .
docker run --rm -p 3010:3010 brapi-mcp-serverThe image defaults to HTTP transport, stateful session mode, and logs to /var/log/brapi-mcp-server. OpenTelemetry peer dependencies are installed by default; build with --build-arg OTEL_ENABLED=false to omit them.
Deployment shapes
Two kinds of state scope by tenant and, by default, by MCP session: connection state (registered aliases and exchanged upstream tokens) and dataframes (df_<uuid> tables, usable by anyone who holds the name within its bucket). Three configurations set where those buckets end:
Shape | Settings | Isolation | Best for |
Per-session (default) |
| Each MCP session gets its own connections and canvas. Requests without a session share one tenant-wide namespace, so | Multi-user hosting without SSO |
Per-user credentials |
| Each user's JWT | Multi-user hosting with institutional SSO; the strongest separation |
Shared workspace |
| All callers share one tenant's connections and canvas. | One researcher running parallel agents on shared upstream credentials |
Stdio is always a single session. Clients on MCP protocol revision 2026-07-28 carry no session on any transport, so outside the per-user-credentials shape they land in the shared tenant workspace, where re-registering an alias re-points every such caller's later calls to it.
On HTTP without per-user auth, brapi_dataframe_describe lists dataframes without a name only for a request with its own session-scoped canvas, and brapi_dataframe_query rejects system-catalog reads in every shape, so a caller without a known df_<uuid> name can't enumerate other callers' tables.
Project structure
Directory | Purpose |
|
|
| Server env parsing (Zod), per-alias credential resolution, and the built-in alias registry. |
| Tool definitions ( |
| Resource definitions ( |
| Prompt definitions ( |
| BrAPI client, dialect adapters, filter catalog, canvas bridge, capability registry, ISO country resolver, ontology resolver, reference-data cache, server registry. |
| Unit and integration tests mirroring |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
Handlers throw, framework catches — no
try/catchin tool logicUse
ctx.logfor logging,ctx.statefor tenant-scoped storage — noconsole, no direct persistence accessAdd new tools to the matching group in
src/mcp-server/tools/definitions/index.ts;src/index.tscomposes the groups behind their feature flagsWrap upstream calls: validate raw → normalize → return output schema; never fabricate missing fields
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run testLicense
Apache-2.0 — see LICENSE for details.
This server cannot be deployed
Maintenance
Related MCP Connectors
Workspace knowledge bases agents ingest, search, and query over MCP.
Query, browse, and automate OmegaAI workspaces from any MCP client. Streamable HTTP with OAuth 2.0.
Search and inspect source-pinned agent skills, plugins and tools through a free public MCP.
Create, browse, remix, collaborate on, and run durable AI workflow nodes from MCP hosts.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceMCP server for querying the GWAS Catalog (EBI/NHGRI), a curated catalog of genome-wide association studies. It enables AI agents to search and retrieve study data via natural language or direct tool calls.485 npmMIT
- AlicenseNot gradedqualityCmaintenanceEnables MCP-compatible clients to search and query a team knowledge graph, retrieve document context, upload files for indexing, explore entities and connections, and monitor workspace usage and server health.MIT
- AlicenseAqualityBmaintenanceEnables MCP clients to safely read, write, edit, search, and optionally run commands within one or more named project workspaces, while maintaining a work journal and tracking changes.19MIT
- AlicenseNot gradedqualityBmaintenanceEnables a self-hosted AI workspace with persistent sessions, agent run/step execution logs, unified search across conversations and knowledge, and discovery/invocation of local and remote MCP tools.MIT