Skip to content

Add brapi_federated_find tool — query multiple aliases in parallel and merge #11

Description

@cyanheads

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions