Use case
The alias system already supports multiple registered connections (CassavaBase + T3 + GnpIS, etc.). Real federation workflows — "find drought-tolerance trials across all my registered breedbases" — currently require N sequential find_* calls and ad-hoc client-side merge. Identity reconciliation is fuzzy across servers but a best-effort merge is still better than the agent inventing one each session.
Previously deferred pending brapi_build_phenotype_matrix (#8) and brapi_germplasm_performance (#9) — both shipped, so the per-alias find_* plumbing this issue would build on now exists and is exercised in production.
Proposed behavior
brapi_federated_find({
aliases: string[], // 2+ registered aliases
resource: "studies" | "germplasm" | "observations" | …,
filters: Record<string, unknown>, // forwarded to per-alias find_*
loadLimit?: number,
mergeOn?: "pui" | "name" | "dbid+source",
}) → {
results: Row[], // merged
perSource: Record<string, { count, errors? }>,
conflicts?: Array<{ key, sources, fields }>, // same identity, different fields
dataframe?: DataframeHandle, // present when the merged result exceeds loadLimit
}
Per-alias find_* runs in parallel; merge is best-effort; conflicts surface explicitly rather than being silently chosen between. A per-alias failure (auth expired, timeout, unreachable server) must not fail the whole call — it should be captured under perSource[alias].errors while the other aliases still return results.
Alternatives considered
Agent runs each find_* per alias and merges client-side — feasible but error-prone, and the merge logic deserves a single source of truth.
The maintainer's own deferral comment raised a second alternative worth weighing now that #8/#9 have landed: rather than a fully generic orchestrator over every find_* resource, the federated surface could instead be a wrapper specifically around brapi_build_phenotype_matrix (the dominant cross-study/cross-germplasm workflow). That's a product-scope call for the implementer/maintainer, not resolved here — the generic resource-keyed design below is still coherent and matches what the issue originally proposed.
Additional context
PUIs aren't universally adopted, so mergeOn: "name" is the realistic default for germplasm. For studies, dbid+source is a safe identity. This is the most ambitious of the proposed tools — the value of formalizing the merge logic justifies it, but the cross-server identity problem is genuinely hard.
Scope
- v1 resource coverage: confirm which of the eight existing
find_* domains (studies, germplasm, observations, images, locations, variants, variables, genotype calls) are in scope for the first cut — the code sample's … leaves this open. Starting with the three named (studies, germplasm, observations) and treating the rest as follow-on is reasonable given they cover the stated use case.
- Per-alias dialect and capability handling reuses the existing per-connection machinery verbatim (
resolveDialect(connection, ctx) in src/services/brapi-dialect/index.ts, getCapabilityRegistry().ensure(...)) — no new dialect-detection logic.
Out of scope
- Live conflict resolution / auto-merge heuristics beyond surfacing
conflicts[] — the agent (or user) decides how to reconcile, this tool does not guess.
- Cross-alias identity resolution for resources without a natural key (this issue's
mergeOn options only cover germplasm and studies; other resources may need their own identity strategy, deferred to a follow-up).
Touchpoints
- New tool file:
src/mcp-server/tools/definitions/brapi-federated-find.tool.ts; register in src/index.ts's tools array (tool count moves from 25 upward — update README.md's count).
- Per-alias fan-out must run under
Promise.allSettled, not Promise.all — the proposed perSource[alias].errors field only makes sense if one alias's rejection doesn't abort the others.
- Reuse each resource's actual
find_* tool logic, not just the low-level route resolver. resolveFindRoute / loadInitialFindPage / maybeSpill in src/mcp-server/tools/shared/find-helpers.ts handle GET-vs-POST/search routing and dialect filter adaptation, but the per-resource find_* tools (e.g. brapi-find-observations.tool.ts) layer additional guards on top — notably the SGN/Breedbase study-anchoring preflight bailout (PREFLIGHT_BULK_THRESHOLD, probeObservationCount in brapi-find-observations.tool.ts) that prevents an unanchored germplasm-only observations query from stalling on CassavaBase/Sweetpotatobase-family servers. A federated observations call built only on the low-level helpers would skip that guard per-alias.
filters: Record<string, unknown> is a raw passthrough, unlike the named-param + extraFilters merge (mergeFilters, applyDialectFiltersOrFail) every sibling find_* tool uses. Decide whether brapi_federated_find accepts the same named-param shape per resource (more consistent, more schema surface) or stays fully generic (simpler schema, weaker validation) — either way it should route through mergeFilters/applyDialectFiltersOrFail per alias so a dialect that drops every filter is caught the same way all_filters_dropped catches it on the single-alias tools.
- Merged-result spillover: every other
find_* tool spills results beyond loadLimit to a canvas dataframe (DataframeHandleSchema, maybeSpill in find-helpers.ts). The result shape above should carry the same optional dataframe handle for consistency, rather than returning an unbounded inline results[].
- New merge/identity module (no existing precedent in the codebase) — a Zod schema for the
conflicts[] row shape ({ key: string, sources: string[], fields: Record<string, unknown> } or similar) and the pui/name/dbid+source matching logic, most naturally as src/mcp-server/tools/shared/federated-merge.ts.
- Tests: new
brapi-federated-find.tool.test.ts covering a 2-alias merge with no conflicts, a merge with a genuine field conflict, one alias failing while the other succeeds (perSource[alias].errors populated, call still returns results), and each mergeOn mode.
- Docs: README tool-count line,
docs/tree.md, changelog entry.
Use case
The alias system already supports multiple registered connections (CassavaBase + T3 + GnpIS, etc.). Real federation workflows — "find drought-tolerance trials across all my registered breedbases" — currently require N sequential
find_*calls and ad-hoc client-side merge. Identity reconciliation is fuzzy across servers but a best-effort merge is still better than the agent inventing one each session.Previously deferred pending
brapi_build_phenotype_matrix(#8) andbrapi_germplasm_performance(#9) — both shipped, so the per-aliasfind_*plumbing this issue would build on now exists and is exercised in production.Proposed behavior
Per-alias
find_*runs in parallel; merge is best-effort; conflicts surface explicitly rather than being silently chosen between. A per-alias failure (auth expired, timeout, unreachable server) must not fail the whole call — it should be captured underperSource[alias].errorswhile the other aliases still return results.Alternatives considered
Agent runs each
find_*per alias and merges client-side — feasible but error-prone, and the merge logic deserves a single source of truth.The maintainer's own deferral comment raised a second alternative worth weighing now that #8/#9 have landed: rather than a fully generic orchestrator over every
find_*resource, the federated surface could instead be a wrapper specifically aroundbrapi_build_phenotype_matrix(the dominant cross-study/cross-germplasm workflow). That's a product-scope call for the implementer/maintainer, not resolved here — the genericresource-keyed design below is still coherent and matches what the issue originally proposed.Additional context
PUIs aren't universally adopted, so
mergeOn: "name"is the realistic default for germplasm. For studies,dbid+sourceis a safe identity. This is the most ambitious of the proposed tools — the value of formalizing the merge logic justifies it, but the cross-server identity problem is genuinely hard.Scope
find_*domains (studies, germplasm, observations, images, locations, variants, variables, genotype calls) are in scope for the first cut — the code sample's…leaves this open. Starting with the three named (studies, germplasm, observations) and treating the rest as follow-on is reasonable given they cover the stated use case.resolveDialect(connection, ctx)insrc/services/brapi-dialect/index.ts,getCapabilityRegistry().ensure(...)) — no new dialect-detection logic.Out of scope
conflicts[]— the agent (or user) decides how to reconcile, this tool does not guess.mergeOnoptions only cover germplasm and studies; other resources may need their own identity strategy, deferred to a follow-up).Touchpoints
src/mcp-server/tools/definitions/brapi-federated-find.tool.ts; register insrc/index.ts'stoolsarray (tool count moves from 25 upward — updateREADME.md's count).Promise.allSettled, notPromise.all— the proposedperSource[alias].errorsfield only makes sense if one alias's rejection doesn't abort the others.find_*tool logic, not just the low-level route resolver.resolveFindRoute/loadInitialFindPage/maybeSpillinsrc/mcp-server/tools/shared/find-helpers.tshandle GET-vs-POST/search routing and dialect filter adaptation, but the per-resourcefind_*tools (e.g.brapi-find-observations.tool.ts) layer additional guards on top — notably the SGN/Breedbase study-anchoring preflight bailout (PREFLIGHT_BULK_THRESHOLD,probeObservationCountinbrapi-find-observations.tool.ts) that prevents an unanchoredgermplasm-only observations query from stalling on CassavaBase/Sweetpotatobase-family servers. A federatedobservationscall built only on the low-level helpers would skip that guard per-alias.filters: Record<string, unknown>is a raw passthrough, unlike the named-param +extraFiltersmerge (mergeFilters,applyDialectFiltersOrFail) every siblingfind_*tool uses. Decide whetherbrapi_federated_findaccepts the same named-param shape per resource (more consistent, more schema surface) or stays fully generic (simpler schema, weaker validation) — either way it should route throughmergeFilters/applyDialectFiltersOrFailper alias so a dialect that drops every filter is caught the same wayall_filters_droppedcatches it on the single-alias tools.find_*tool spills results beyondloadLimitto a canvas dataframe (DataframeHandleSchema,maybeSpillinfind-helpers.ts). The result shape above should carry the same optionaldataframehandle for consistency, rather than returning an unbounded inlineresults[].conflicts[]row shape ({ key: string, sources: string[], fields: Record<string, unknown> }or similar) and thepui/name/dbid+sourcematching logic, most naturally assrc/mcp-server/tools/shared/federated-merge.ts.brapi-federated-find.tool.test.tscovering a 2-alias merge with no conflicts, a merge with a genuine field conflict, one alias failing while the other succeeds (perSource[alias].errorspopulated, call still returns results), and eachmergeOnmode.docs/tree.md, changelog entry.